thingsboard-memoizeit

Rule Chains UI

3/14/2018 3:43:56 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 77953c9..f59ec63 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
@@ -46,6 +46,7 @@ import org.thingsboard.server.dao.device.DeviceService;
 import org.thingsboard.server.dao.event.EventService;
 import org.thingsboard.server.dao.plugin.PluginService;
 import org.thingsboard.server.dao.relation.RelationService;
+import org.thingsboard.server.dao.rule.RuleChainService;
 import org.thingsboard.server.dao.rule.RuleService;
 import org.thingsboard.server.dao.tenant.TenantService;
 import org.thingsboard.server.dao.timeseries.TimeseriesService;
@@ -97,6 +98,9 @@ public class ActorSystemContext {
     @Getter private RuleService ruleService;
 
     @Autowired
+    @Getter private RuleChainService ruleChainService;
+
+    @Autowired
     @Getter private PluginService pluginService;
 
     @Autowired
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java
index ce95ee4..10f16ce 100644
--- a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java
@@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.common.data.plugin.PluginMetaData;
 import org.thingsboard.server.common.data.relation.EntityRelation;
 import org.thingsboard.server.common.data.relation.RelationTypeGroup;
+import org.thingsboard.server.common.data.rule.RuleChain;
 import org.thingsboard.server.common.data.rule.RuleMetaData;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
@@ -330,6 +331,9 @@ public final class PluginProcessingContext implements PluginContext {
                 case RULE:
                     validateRule(ctx, entityId, callback);
                     return;
+                case RULE_CHAIN:
+                    validateRuleChain(ctx, entityId, callback);
+                    return;
                 case PLUGIN:
                     validatePlugin(ctx, entityId, callback);
                     return;
@@ -411,6 +415,28 @@ public final class PluginProcessingContext implements PluginContext {
         }
     }
 
+    private void validateRuleChain(final PluginApiCallSecurityContext ctx, EntityId entityId, ValidationCallback callback) {
+        if (ctx.isCustomerUser()) {
+            callback.onSuccess(this, ValidationResult.accessDenied(CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
+        } else {
+            ListenableFuture<RuleChain> ruleChainFuture = pluginCtx.ruleChainService.findRuleChainByIdAsync(new RuleChainId(entityId.getId()));
+            Futures.addCallback(ruleChainFuture, getCallback(callback, ruleChain -> {
+                if (ruleChain == null) {
+                    return ValidationResult.entityNotFound("Rule chain with requested id wasn't found!");
+                } else {
+                    if (ctx.isTenantAdmin() && !ruleChain.getTenantId().equals(ctx.getTenantId())) {
+                        return ValidationResult.accessDenied("Rule chain doesn't belong to the current Tenant!");
+                    } else if (ctx.isSystemAdmin() && !ruleChain.getTenantId().isNullUid()) {
+                        return ValidationResult.accessDenied("Rule chain is not in system scope!");
+                    } else {
+                        return ValidationResult.ok();
+                    }
+                }
+            }));
+        }
+    }
+
+
     private void validatePlugin(final PluginApiCallSecurityContext ctx, EntityId entityId, ValidationCallback callback) {
         if (ctx.isCustomerUser()) {
             callback.onSuccess(this, ValidationResult.accessDenied(CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java b/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java
index 06138a3..1828970 100644
--- a/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java
@@ -32,6 +32,7 @@ import org.thingsboard.server.dao.customer.CustomerService;
 import org.thingsboard.server.dao.device.DeviceService;
 import org.thingsboard.server.dao.plugin.PluginService;
 import org.thingsboard.server.dao.relation.RelationService;
+import org.thingsboard.server.dao.rule.RuleChainService;
 import org.thingsboard.server.dao.rule.RuleService;
 import org.thingsboard.server.dao.tenant.TenantService;
 import org.thingsboard.server.dao.timeseries.TimeseriesService;
@@ -56,6 +57,7 @@ public final class SharedPluginProcessingContext {
     final AssetService assetService;
     final DeviceService deviceService;
     final RuleService ruleService;
+    final RuleChainService ruleChainService;
     final PluginService pluginService;
     final CustomerService customerService;
     final TenantService tenantService;
@@ -84,6 +86,7 @@ public final class SharedPluginProcessingContext {
         this.rpcService = sysContext.getRpcService();
         this.routingService = sysContext.getRoutingService();
         this.ruleService = sysContext.getRuleService();
+        this.ruleChainService = sysContext.getRuleChainService();
         this.pluginService = sysContext.getPluginService();
         this.customerService = sysContext.getCustomerService();
         this.tenantService = sysContext.getTenantService();
diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
index 83b304f..2a4ba71 100644
--- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
@@ -550,6 +550,8 @@ public abstract class BaseController {
                 throw new ThingsboardException(YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION,
                         ThingsboardErrorCode.PERMISSION_DENIED);
 
+            } else if (tenantId.getId().equals(ModelConstants.NULL_UUID)) {
+                ruleChain.setConfiguration(null);
             }
         }
         return ruleChain;
diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
index 012e077..f24646b 100644
--- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
@@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
 import org.thingsboard.server.common.data.plugin.PluginMetaData;
 import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
 import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.dao.model.ModelConstants;
 import org.thingsboard.server.exception.ThingsboardException;
@@ -54,6 +55,21 @@ public class RuleChainController extends BaseController {
     }
 
     @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+    @RequestMapping(value = "/ruleChain/{ruleChainId}/metadata", method = RequestMethod.GET)
+    @ResponseBody
+    public RuleChainMetaData getRuleChainMetaData(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
+        checkParameter(RULE_CHAIN_ID, strRuleChainId);
+        try {
+            RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId));
+            checkRuleChain(ruleChainId);
+            return ruleChainService.loadRuleChainMetaData(ruleChainId);
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
     @RequestMapping(value = "/ruleChain", method = RequestMethod.POST)
     @ResponseBody
     public RuleChain saveRuleChain(@RequestBody RuleChain ruleChain) throws ThingsboardException {
@@ -77,6 +93,28 @@ public class RuleChainController extends BaseController {
     }
 
     @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+    @RequestMapping(value = "/ruleChain/metadata", method = RequestMethod.POST)
+    @ResponseBody
+    public RuleChainMetaData saveRuleChainMetaData(@RequestBody RuleChainMetaData ruleChainMetaData) throws ThingsboardException {
+        try {
+            RuleChain ruleChain = checkRuleChain(ruleChainMetaData.getRuleChainId());
+            RuleChainMetaData savedRuleChainMetaData = checkNotNull(ruleChainService.saveRuleChainMetaData(ruleChainMetaData));
+
+            logEntityAction(ruleChain.getId(), ruleChain,
+                    null,
+                    ActionType.UPDATED, null, ruleChainMetaData);
+
+            return savedRuleChainMetaData;
+        } catch (Exception e) {
+
+            logEntityAction(emptyId(EntityType.RULE_CHAIN), null,
+                    null, ActionType.UPDATED, e, ruleChainMetaData);
+
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
     @RequestMapping(value = "/ruleChains", params = {"limit"}, method = RequestMethod.GET)
     @ResponseBody
     public TextPageData<RuleChain> getRuleChains(
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
index 23fadeb..2a49130 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
@@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.id.*;
 import org.thingsboard.server.common.data.kv.AttributeKvEntry;
 import org.thingsboard.server.common.data.page.TimePageData;
 import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
 import org.thingsboard.server.common.data.security.DeviceCredentials;
 import org.thingsboard.server.dao.audit.sink.AuditLogSink;
 import org.thingsboard.server.dao.entity.EntityService;
@@ -158,11 +159,20 @@ public class AuditLogServiceImpl implements AuditLogService {
         switch(actionType) {
             case ADDED:
             case UPDATED:
-                ObjectNode entityNode = objectMapper.valueToTree(entity);
-                if (entityId.getEntityType() == EntityType.DASHBOARD) {
-                    entityNode.put("configuration", "");
+                if (entity != null) {
+                    ObjectNode entityNode = objectMapper.valueToTree(entity);
+                    if (entityId.getEntityType() == EntityType.DASHBOARD) {
+                        entityNode.put("configuration", "");
+                    }
+                    actionData.set("entity", entityNode);
+                }
+                if (entityId.getEntityType() == EntityType.RULE_CHAIN) {
+                    RuleChainMetaData ruleChainMetaData = extractParameter(RuleChainMetaData.class, additionalInfo);
+                    if (ruleChainMetaData != null) {
+                        ObjectNode ruleChainMetaDataNode = objectMapper.valueToTree(ruleChainMetaData);
+                        actionData.set("metadata", ruleChainMetaDataNode);
+                    }
                 }
-                actionData.set("entity", entityNode);
                 break;
             case DELETED:
             case ACTIVATED:
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 1e79163..04207ec 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
@@ -16,6 +16,7 @@
 
 package org.thingsboard.server.dao.rule;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -219,6 +220,12 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
     }
 
     @Override
+    public ListenableFuture<RuleChain> findRuleChainByIdAsync(RuleChainId ruleChainId) {
+        Validator.validateId(ruleChainId, "Incorrect rule chain id for search request.");
+        return ruleChainDao.findByIdAsync(ruleChainId.getId());
+    }
+
+    @Override
     public RuleChain getRootTenantRuleChain(TenantId tenantId) {
         Validator.validateId(tenantId, "Incorrect tenant id for search request.");
         List<EntityRelation> relations = relationService.findByFrom(tenantId, RelationTypeGroup.RULE_CHAIN);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java
index 6c44090..e5f2840 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java
@@ -16,6 +16,7 @@
 
 package org.thingsboard.server.dao.rule;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import org.thingsboard.server.common.data.id.RuleChainId;
 import org.thingsboard.server.common.data.id.RuleNodeId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -41,6 +42,8 @@ public interface RuleChainService {
 
     RuleChain findRuleChainById(RuleChainId ruleChainId);
 
+    ListenableFuture<RuleChain> findRuleChainByIdAsync(RuleChainId ruleChainId);
+
     RuleChain getRootTenantRuleChain(TenantId tenantId);
 
     List<RuleNode> getRuleChainNodes(RuleChainId ruleChainId);
diff --git a/ui/src/app/api/rule-chain.service.js b/ui/src/app/api/rule-chain.service.js
new file mode 100644
index 0000000..16afa74
--- /dev/null
+++ b/ui/src/app/api/rule-chain.service.js
@@ -0,0 +1,150 @@
+/*
+ * 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.
+ */
+export default angular.module('thingsboard.api.ruleChain', [])
+    .factory('ruleChainService', RuleChainService).name;
+
+/*@ngInject*/
+function RuleChainService($http, $q) {
+
+    var service = {
+        getSystemRuleChains: getSystemRuleChains,
+        getTenantRuleChains: getTenantRuleChains,
+        getRuleChains: getRuleChains,
+        getRuleChain: getRuleChain,
+        saveRuleChain: saveRuleChain,
+        deleteRuleChain: deleteRuleChain,
+        getRuleChainMetaData: getRuleChainMetaData,
+        saveRuleChainMetaData: saveRuleChainMetaData
+    };
+
+    return service;
+
+    function getSystemRuleChains (pageLink, config) {
+        var deferred = $q.defer();
+        var url = '/api/system/ruleChains?limit=' + pageLink.limit;
+        if (angular.isDefined(pageLink.textSearch)) {
+            url += '&textSearch=' + pageLink.textSearch;
+        }
+        if (angular.isDefined(pageLink.idOffset)) {
+            url += '&idOffset=' + pageLink.idOffset;
+        }
+        if (angular.isDefined(pageLink.textOffset)) {
+            url += '&textOffset=' + pageLink.textOffset;
+        }
+        $http.get(url, config).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function getTenantRuleChains (pageLink, config) {
+        var deferred = $q.defer();
+        var url = '/api/tenant/ruleChains?limit=' + pageLink.limit;
+        if (angular.isDefined(pageLink.textSearch)) {
+            url += '&textSearch=' + pageLink.textSearch;
+        }
+        if (angular.isDefined(pageLink.idOffset)) {
+            url += '&idOffset=' + pageLink.idOffset;
+        }
+        if (angular.isDefined(pageLink.textOffset)) {
+            url += '&textOffset=' + pageLink.textOffset;
+        }
+        $http.get(url, config).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function getRuleChains (pageLink, config) {
+        var deferred = $q.defer();
+        var url = '/api/ruleChains?limit=' + pageLink.limit;
+        if (angular.isDefined(pageLink.textSearch)) {
+            url += '&textSearch=' + pageLink.textSearch;
+        }
+        if (angular.isDefined(pageLink.idOffset)) {
+            url += '&idOffset=' + pageLink.idOffset;
+        }
+        if (angular.isDefined(pageLink.textOffset)) {
+            url += '&textOffset=' + pageLink.textOffset;
+        }
+        $http.get(url, config).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function getRuleChain(ruleChainId, config) {
+        var deferred = $q.defer();
+        var url = '/api/ruleChain/' + ruleChainId;
+        $http.get(url, config).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function saveRuleChain(ruleChain) {
+        var deferred = $q.defer();
+        var url = '/api/ruleChain';
+        $http.post(url, ruleChain).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function deleteRuleChain(ruleChainId) {
+        var deferred = $q.defer();
+        var url = '/api/ruleChain/' + ruleChainId;
+        $http.delete(url).then(function success() {
+            deferred.resolve();
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function getRuleChainMetaData(ruleChainId, config) {
+        var deferred = $q.defer();
+        var url = '/api/ruleChain/' + ruleChainId + '/metadata';
+        $http.get(url, config).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function saveRuleChainMetaData(ruleChainMetaData) {
+        var deferred = $q.defer();
+        var url = '/api/ruleChain/metadata';
+        $http.post(url, ruleChainMetaData).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+}
diff --git a/ui/src/app/app.js b/ui/src/app/app.js
index 0b6a141..138b230 100644
--- a/ui/src/app/app.js
+++ b/ui/src/app/app.js
@@ -73,6 +73,7 @@ import thingsboardApiAttribute from './api/attribute.service';
 import thingsboardApiEntity from './api/entity.service';
 import thingsboardApiAlarm from './api/alarm.service';
 import thingsboardApiAuditLog from './api/audit-log.service';
+import thingsboardApiRuleChain from './api/rule-chain.service';
 
 import 'typeface-roboto';
 import 'font-awesome/css/font-awesome.min.css';
@@ -135,6 +136,7 @@ angular.module('thingsboard', [
     thingsboardApiEntity,
     thingsboardApiAlarm,
     thingsboardApiAuditLog,
+    thingsboardApiRuleChain,
     uiRouter])
     .config(AppConfig)
     .factory('globalInterceptor', GlobalInterceptor)
diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index ef6ffde..77c21e1 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -294,7 +294,8 @@ export default angular.module('thingsboard.types', [])
                 customer: "CUSTOMER",
                 user: "USER",
                 dashboard: "DASHBOARD",
-                alarm: "ALARM"
+                alarm: "ALARM",
+                rulechain: "RULE_CHAIN"
             },
             aliasEntityType: {
                 current_customer: "CURRENT_CUSTOMER"
@@ -354,6 +355,12 @@ export default angular.module('thingsboard.types', [])
                     list: 'entity.list-of-alarms',
                     nameStartsWith: 'entity.alarm-name-starts-with'
                 },
+                "RULE_CHAIN": {
+                    type: 'entity.type-rulechain',
+                    typePlural: 'entity.type-rulechains',
+                    list: 'entity.list-of-rulechains',
+                    nameStartsWith: 'entity.rulechain-name-starts-with'
+                },
                 "CURRENT_CUSTOMER": {
                     type: 'entity.type-current-customer',
                     list: 'entity.type-current-customer'
diff --git a/ui/src/app/import-export/import-export.service.js b/ui/src/app/import-export/import-export.service.js
index 6071fd2..19c249f 100644
--- a/ui/src/app/import-export/import-export.service.js
+++ b/ui/src/app/import-export/import-export.service.js
@@ -26,7 +26,7 @@ import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html';
 /*@ngInject*/
 export default function ImportExport($log, $translate, $q, $mdDialog, $document, $http, itembuffer, utils, types,
                                      dashboardUtils, entityService, dashboardService, pluginService, ruleService,
-                                     widgetService, toast, attributeService) {
+                                     ruleChainService, widgetService, toast, attributeService) {
 
 
     var service = {
@@ -38,6 +38,8 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
         importPlugin: importPlugin,
         exportRule: exportRule,
         importRule: importRule,
+        exportRuleChain: exportRuleChain,
+        importRuleChain: importRuleChain,
         exportWidgetType: exportWidgetType,
         importWidgetType: importWidgetType,
         exportWidgetsBundle: exportWidgetsBundle,
@@ -275,6 +277,61 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
         return true;
     }
 
+    // Rule chain functions
+
+    function exportRuleChain(ruleChainId) {
+        ruleChainService.getRuleChain(ruleChainId).then(
+            function success(ruleChain) {
+                var name = ruleChain.name;
+                name = name.toLowerCase().replace(/\W/g,"_");
+                exportToPc(prepareExport(ruleChain), name + '.json');
+                //TODO: metadata
+            },
+            function fail(rejection) {
+                var message = rejection;
+                if (!message) {
+                    message = $translate.instant('error.unknown-error');
+                }
+                toast.showError($translate.instant('rulechain.export-failed-error', {error: message}));
+            }
+        );
+    }
+
+    function importRuleChain($event) {
+        var deferred = $q.defer();
+        openImportDialog($event, 'rulechain.import', 'rulechain.rulechain-file').then(
+            function success(ruleChain) {
+                if (!validateImportedRuleChain(ruleChain)) {
+                    toast.showError($translate.instant('rulechain.invalid-rulechain-file-error'));
+                    deferred.reject();
+                } else {
+                    //TODO: rulechain metadata
+                    ruleChainService.saveRuleChain(ruleChain).then(
+                        function success() {
+                            deferred.resolve();
+                        },
+                        function fail() {
+                            deferred.reject();
+                        }
+                    );
+                }
+            },
+            function fail() {
+                deferred.reject();
+            }
+        );
+        return deferred.promise;
+    }
+
+    function validateImportedRuleChain(ruleChain) {
+        //TODO: rulechain metadata
+        if (angular.isUndefined(ruleChain.name))
+        {
+            return false;
+        }
+        return true;
+    }
+
     // Plugin functions
 
     function exportPlugin(pluginId) {
diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js
index e5ca958..e90334b 100644
--- a/ui/src/app/layout/index.js
+++ b/ui/src/app/layout/index.js
@@ -49,6 +49,7 @@ import thingsboardWidgetLibrary from '../widget';
 import thingsboardDashboard from '../dashboard';
 import thingsboardPlugin from '../plugin';
 import thingsboardRule from '../rule';
+import thingsboardRuleChain from '../rulechain';
 
 import thingsboardJsonForm from '../jsonform';
 
@@ -81,6 +82,7 @@ export default angular.module('thingsboard.home', [
     thingsboardDashboard,
     thingsboardPlugin,
     thingsboardRule,
+    thingsboardRuleChain,
     thingsboardJsonForm,
     thingsboardApiDevice,
     thingsboardApiLogin,
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index 252884d..ea576ec 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -745,6 +745,10 @@ export default angular.module('thingsboard.locale', [])
                     "type-alarms": "Alarms",
                     "list-of-alarms": "{ count, select, 1 {One alarms} other {List of # alarms} }",
                     "alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'",
+                    "type-rulechain": "Rule chain",
+                    "type-rulechains": "Rule chains",
+                    "list-of-rulechains": "{ count, select, 1 {One rule chain} other {List of # rule chains} }",
+                    "rulechain-name-starts-with": "Rule chains whose names start with '{{prefix}}'",
                     "type-current-customer": "Current Customer",
                     "search": "Search entities",
                     "selected-entities": "{ count, select, 1 {1 entity} other {# entities} } selected",
@@ -1133,6 +1137,38 @@ export default angular.module('thingsboard.locale', [])
                     "no-rules-matching": "No rules matching '{{entity}}' were found.",
                     "rule-required": "Rule is required"
                 },
+                "rulechain": {
+                    "rulechain": "Rule chain",
+                    "rulechains": "Rule chains",
+                    "delete": "Delete rule chain",
+                    "name": "Name",
+                    "name-required": "Name is required.",
+                    "description": "Description",
+                    "add": "Add Rule Chain",
+                    "delete-rulechain-title": "Are you sure you want to delete the rule chain '{{ruleChainName}}'?",
+                    "delete-rulechain-text": "Be careful, after the confirmation the rule chain and all related data will become unrecoverable.",
+                    "delete-rulechains-title": "Are you sure you want to delete { count, select, 1 {1 rule chain} other {# rule chains} }?",
+                    "delete-rulechains-action-title": "Delete { count, select, 1 {1 rule chain} other {# rule chains} }",
+                    "delete-rulechains-text": "Be careful, after the confirmation all selected rule chains will be removed and all related data will become unrecoverable.",
+                    "add-rulechain-text": "Add new rule chain",
+                    "no-rulechains-text": "No rule chains found",
+                    "rulechain-details": "Rule chain details",
+                    "details": "Details",
+                    "events": "Events",
+                    "system": "System",
+                    "import": "Import rule chain",
+                    "export": "Export rule chain",
+                    "export-failed-error": "Unable to export rule chain: {{error}}",
+                    "create-new-rulechain": "Create new rule chain",
+                    "rule-file": "Rule chain file",
+                    "invalid-rulechain-file-error": "Unable to import rule chain: Invalid rule chain data structure.",
+                    "copyId": "Copy rule chain Id",
+                    "idCopiedMessage": "Rule chain Id has been copied to clipboard",
+                    "select-rulechain": "Select rule chain",
+                    "no-rulechains-matching": "No rule chains matching '{{entity}}' were found.",
+                    "rulechain-required": "Rule chain is required",
+                    "management": "Rules management"
+                },
                 "rule-plugin": {
                     "management": "Rules and plugins management"
                 },
diff --git a/ui/src/app/rulechain/add-rulechain.tpl.html b/ui/src/app/rulechain/add-rulechain.tpl.html
new file mode 100644
index 0000000..44d0ec3
--- /dev/null
+++ b/ui/src/app/rulechain/add-rulechain.tpl.html
@@ -0,0 +1,48 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'rulechain.add' | translate }}" tb-help="'rulechains'" help-container-id="help-container">
+    <form name="theForm" ng-submit="vm.add()">
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2 translate>rulechain.add</h2>
+                <span flex></span>
+                <div id="help-container"></div>
+                <md-button class="md-icon-button" ng-click="vm.cancel()">
+                    <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
+        <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
+        <md-dialog-content>
+            <div class="md-dialog-content">
+                <tb-rule-chain rule-chain="vm.item" is-edit="true" the-form="theForm"></tb-rule-chain>
+            </div>
+        </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <span flex></span>
+            <md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
+                       class="md-raised md-primary">
+                {{ 'action.add' | translate }}
+            </md-button>
+            <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+                translate }}
+            </md-button>
+        </md-dialog-actions>
+    </form>
+</md-dialog>
diff --git a/ui/src/app/rulechain/index.js b/ui/src/app/rulechain/index.js
new file mode 100644
index 0000000..faecc03
--- /dev/null
+++ b/ui/src/app/rulechain/index.js
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+import RuleChainRoutes from './rulechain.routes';
+import RuleChainController from './rulechain.controller';
+import RuleChainDirective from './rulechain.directive';
+
+export default angular.module('thingsboard.ruleChain', [])
+    .config(RuleChainRoutes)
+    .controller('RuleChainController', RuleChainController)
+    .directive('tbRuleChain', RuleChainDirective)
+    .name;
diff --git a/ui/src/app/rulechain/rulechain.controller.js b/ui/src/app/rulechain/rulechain.controller.js
new file mode 100644
index 0000000..475ef4e
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain.controller.js
@@ -0,0 +1,172 @@
+/*
+ * 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 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"
+        }
+    ];
+
+    var vm = this;
+
+    vm.types = types;
+
+    vm.ruleChainGridConfig = {
+
+        refreshParamsFunc: null,
+
+        deleteItemTitleFunc: deleteRuleChainTitle,
+        deleteItemContentFunc: deleteRuleChainText,
+        deleteItemsTitleFunc: deleteRuleChainsTitle,
+        deleteItemsActionTitleFunc: deleteRuleChainsActionTitle,
+        deleteItemsContentFunc: deleteRuleChainsText,
+
+        fetchItemsFunc: fetchRuleChains,
+        saveItemFunc: saveRuleChain,
+        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 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/rulechain.directive.js b/ui/src/app/rulechain/rulechain.directive.js
new file mode 100644
index 0000000..b23cd98
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain.directive.js
@@ -0,0 +1,47 @@
+/*
+ * 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 ruleChainFieldsetTemplate from './rulechain-fieldset.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleChainDirective($compile, $templateCache, $mdDialog, $document, $q, $translate, types, toast) {
+    var linker = function (scope, element) {
+        var template = $templateCache.get(ruleChainFieldsetTemplate);
+        element.html(template);
+
+        scope.onRuleChainIdCopied = function() {
+            toast.showSuccess($translate.instant('rulechain.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
+        };
+
+        $compile(element.contents())(scope);
+    }
+    return {
+        restrict: "E",
+        link: linker,
+        scope: {
+            ruleChain: '=',
+            isEdit: '=',
+            isReadOnly: '=',
+            theForm: '=',
+            onExportRuleChain: '&',
+            onDeleteRuleChain: '&'
+        }
+    };
+}
diff --git a/ui/src/app/rulechain/rulechain.routes.js b/ui/src/app/rulechain/rulechain.routes.js
new file mode 100644
index 0000000..e8d6be4
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain.routes.js
@@ -0,0 +1,46 @@
+/*
+ * 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 ruleChainsTemplate from './rulechains.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleChainRoutes($stateProvider) {
+
+    $stateProvider
+        .state('home.ruleChains', {
+            url: '/ruleChains',
+            params: {'topIndex': 0},
+            module: 'private',
+            auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
+            views: {
+                "content@home": {
+                    templateUrl: ruleChainsTemplate,
+                    controllerAs: 'vm',
+                    controller: 'RuleChainController'
+                }
+            },
+            data: {
+                searchEnabled: true,
+                pageTitle: 'rulechain.rulechains'
+            },
+            ncyBreadcrumb: {
+                label: '{"icon": "settings_ethernet", "label": "rulechain.rulechains"}'
+            }
+        });
+}
diff --git a/ui/src/app/rulechain/rulechain-card.tpl.html b/ui/src/app/rulechain/rulechain-card.tpl.html
new file mode 100644
index 0000000..48a572c
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain-card.tpl.html
@@ -0,0 +1,18 @@
+<!--
+
+    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 class="tb-uppercase" ng-if="item && parentCtl.types.id.nullUid === item.tenantId.id" translate>rulechain.system</div>
diff --git a/ui/src/app/rulechain/rulechain-fieldset.tpl.html b/ui/src/app/rulechain/rulechain-fieldset.tpl.html
new file mode 100644
index 0000000..ec68c57
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain-fieldset.tpl.html
@@ -0,0 +1,49 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<md-button ng-click="onExportRuleChain({event: $event})"
+           ng-show="!isEdit"
+           class="md-raised md-primary">{{ 'rulechain.export' | translate }}</md-button>
+<md-button ng-click="onDeleteRuleChain({event: $event})"
+           ng-show="!isEdit && !isReadOnly"
+           class="md-raised md-primary">{{ 'rulechain.delete' | translate }}</md-button>
+
+<div layout="row">
+    <md-button ngclipboard data-clipboard-action="copy"
+               ngclipboard-success="onRuleChainIdCopied(e)"
+               data-clipboard-text="{{ruleChain.id.id}}" ng-show="!isEdit"
+               class="md-raised">
+        <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
+        <span translate>rulechain.copyId</span>
+    </md-button>
+</div>
+
+<md-content class="md-padding tb-rulechain" layout="column">
+    <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
+        <md-input-container class="md-block">
+            <label translate>rulechain.name</label>
+            <input required name="name" ng-model="ruleChain.name">
+            <div ng-messages="theForm.name.$error">
+                <div translate ng-message="required">rulechain.name-required</div>
+            </div>
+        </md-input-container>
+        <md-input-container class="md-block">
+            <label translate>rulechain.description</label>
+            <textarea ng-model="ruleChain.additionalInfo.description" rows="2"></textarea>
+        </md-input-container>
+    </fieldset>
+</md-content>
diff --git a/ui/src/app/rulechain/rulechains.tpl.html b/ui/src/app/rulechain/rulechains.tpl.html
new file mode 100644
index 0000000..a4fbd79
--- /dev/null
+++ b/ui/src/app/rulechain/rulechains.tpl.html
@@ -0,0 +1,75 @@
+<!--
+
+    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-grid grid-configuration="vm.ruleChainGridConfig">
+    <details-buttons tb-help="'rulechains'" help-container-id="help-container">
+        <div id="help-container"></div>
+    </details-buttons>
+    <md-tabs ng-class="{'tb-headless': (vm.grid.detailsConfig.isDetailsEditMode || !vm.isRuleChainEditable(vm.grid.operatingItem()))}"
+             id="tabs" md-border-bottom flex class="tb-absolute-fill">
+        <md-tab label="{{ 'rulechain.details' | translate }}">
+            <tb-rule-chain rule-chain="vm.grid.operatingItem()"
+                     is-edit="vm.grid.detailsConfig.isDetailsEditMode"
+                     is-read-only="vm.grid.isDetailsReadOnly(vm.grid.operatingItem())"
+                     the-form="vm.grid.detailsForm"
+                     on-export-rule-chain="vm.exportRuleChain(event, vm.grid.detailsConfig.currentItem)"
+                     on-delete-rule-chain="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-rule-chain>
+        </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.attributes' | translate }}">
+            <tb-attribute-table flex
+                                entity-id="vm.grid.operatingItem().id.id"
+                                entity-type="{{vm.types.entityType.rulechain}}"
+                                entity-name="vm.grid.operatingItem().name"
+                                default-attribute-scope="{{vm.types.attributesScope.server.value}}">
+            </tb-attribute-table>
+        </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.latest-telemetry' | translate }}">
+            <tb-attribute-table flex
+                                entity-id="vm.grid.operatingItem().id.id"
+                                entity-type="{{vm.types.entityType.rulechain}}"
+                                entity-name="vm.grid.operatingItem().name"
+                                default-attribute-scope="{{vm.types.latestTelemetry.value}}"
+                                disable-attribute-scope-selection="true">
+            </tb-attribute-table>
+        </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'alarm.alarms' | translate }}">
+            <tb-alarm-table flex entity-type="vm.types.entityType.rulechain"
+                            entity-id="vm.grid.operatingItem().id.id">
+            </tb-alarm-table>
+        </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'rulechain.events' | translate }}">
+            <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}}">
+            </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 }}">
+            <tb-relation-table flex
+                               entity-id="vm.grid.operatingItem().id.id"
+                               entity-type="{{vm.types.entityType.rulechain}}">
+            </tb-relation-table>
+        </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem()) && vm.grid.isTenantAdmin()"
+                md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
+            <tb-audit-log-table flex entity-type="vm.types.entityType.rulechain"
+                                entity-id="vm.grid.operatingItem().id.id"
+                                audit-log-mode="{{vm.types.auditLogMode.entity}}">
+            </tb-audit-log-table>
+        </md-tab>
+    </md-tabs>
+</tb-grid>
diff --git a/ui/src/app/services/menu.service.js b/ui/src/app/services/menu.service.js
index 9dbddd9..5d97ea6 100644
--- a/ui/src/app/services/menu.service.js
+++ b/ui/src/app/services/menu.service.js
@@ -79,6 +79,12 @@ function Menu(userService, $state, $rootScope) {
                             icon: 'settings_ethernet'
                         },
                         {
+                            name: 'rulechain.rulechains',
+                            type: 'link',
+                            state: 'home.ruleChains',
+                            icon: 'settings_ethernet'
+                        },
+                        {
                             name: 'tenant.tenants',
                             type: 'link',
                             state: 'home.tenants',
@@ -128,6 +134,16 @@ function Menu(userService, $state, $rootScope) {
                             ]
                         },
                         {
+                            name: 'rulechain.management',
+                            places: [
+                                {
+                                    name: 'rulechain.rulechains',
+                                    icon: 'settings_ethernet',
+                                    state: 'home.ruleChains'
+                                }
+                            ]
+                        },
+                        {
                             name: 'tenant.management',
                             places: [
                                 {
@@ -183,6 +199,12 @@ function Menu(userService, $state, $rootScope) {
                             icon: 'settings_ethernet'
                         },
                         {
+                            name: 'rulechain.rulechains',
+                            type: 'link',
+                            state: 'home.ruleChains',
+                            icon: 'settings_ethernet'
+                        },
+                        {
                             name: 'customer.customers',
                             type: 'link',
                             state: 'home.customers',
@@ -236,6 +258,16 @@ function Menu(userService, $state, $rootScope) {
                             ]
                         },
                         {
+                            name: 'rulechain.management',
+                            places: [
+                                {
+                                    name: 'rulechain.rulechains',
+                                    icon: 'settings_ethernet',
+                                    state: 'home.ruleChains'
+                                }
+                            ]
+                        },
+                        {
                             name: 'customer.management',
                             places: [
                                 {