Commit ae45a62f2fa1d4667dd31e65e6be4d62515a94b9

Authored by Igor Kulikov
1 parent b0eb57bb

Rule Chains UI

... ... @@ -46,6 +46,7 @@ import org.thingsboard.server.dao.device.DeviceService;
46 46 import org.thingsboard.server.dao.event.EventService;
47 47 import org.thingsboard.server.dao.plugin.PluginService;
48 48 import org.thingsboard.server.dao.relation.RelationService;
  49 +import org.thingsboard.server.dao.rule.RuleChainService;
49 50 import org.thingsboard.server.dao.rule.RuleService;
50 51 import org.thingsboard.server.dao.tenant.TenantService;
51 52 import org.thingsboard.server.dao.timeseries.TimeseriesService;
... ... @@ -97,6 +98,9 @@ public class ActorSystemContext {
97 98 @Getter private RuleService ruleService;
98 99
99 100 @Autowired
  101 + @Getter private RuleChainService ruleChainService;
  102 +
  103 + @Autowired
100 104 @Getter private PluginService pluginService;
101 105
102 106 @Autowired
... ...
... ... @@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.page.TextPageLink;
36 36 import org.thingsboard.server.common.data.plugin.PluginMetaData;
37 37 import org.thingsboard.server.common.data.relation.EntityRelation;
38 38 import org.thingsboard.server.common.data.relation.RelationTypeGroup;
  39 +import org.thingsboard.server.common.data.rule.RuleChain;
39 40 import org.thingsboard.server.common.data.rule.RuleMetaData;
40 41 import org.thingsboard.server.common.msg.cluster.ServerAddress;
41 42 import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
... ... @@ -330,6 +331,9 @@ public final class PluginProcessingContext implements PluginContext {
330 331 case RULE:
331 332 validateRule(ctx, entityId, callback);
332 333 return;
  334 + case RULE_CHAIN:
  335 + validateRuleChain(ctx, entityId, callback);
  336 + return;
333 337 case PLUGIN:
334 338 validatePlugin(ctx, entityId, callback);
335 339 return;
... ... @@ -411,6 +415,28 @@ public final class PluginProcessingContext implements PluginContext {
411 415 }
412 416 }
413 417
  418 + private void validateRuleChain(final PluginApiCallSecurityContext ctx, EntityId entityId, ValidationCallback callback) {
  419 + if (ctx.isCustomerUser()) {
  420 + callback.onSuccess(this, ValidationResult.accessDenied(CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
  421 + } else {
  422 + ListenableFuture<RuleChain> ruleChainFuture = pluginCtx.ruleChainService.findRuleChainByIdAsync(new RuleChainId(entityId.getId()));
  423 + Futures.addCallback(ruleChainFuture, getCallback(callback, ruleChain -> {
  424 + if (ruleChain == null) {
  425 + return ValidationResult.entityNotFound("Rule chain with requested id wasn't found!");
  426 + } else {
  427 + if (ctx.isTenantAdmin() && !ruleChain.getTenantId().equals(ctx.getTenantId())) {
  428 + return ValidationResult.accessDenied("Rule chain doesn't belong to the current Tenant!");
  429 + } else if (ctx.isSystemAdmin() && !ruleChain.getTenantId().isNullUid()) {
  430 + return ValidationResult.accessDenied("Rule chain is not in system scope!");
  431 + } else {
  432 + return ValidationResult.ok();
  433 + }
  434 + }
  435 + }));
  436 + }
  437 + }
  438 +
  439 +
414 440 private void validatePlugin(final PluginApiCallSecurityContext ctx, EntityId entityId, ValidationCallback callback) {
415 441 if (ctx.isCustomerUser()) {
416 442 callback.onSuccess(this, ValidationResult.accessDenied(CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
... ...
... ... @@ -32,6 +32,7 @@ import org.thingsboard.server.dao.customer.CustomerService;
32 32 import org.thingsboard.server.dao.device.DeviceService;
33 33 import org.thingsboard.server.dao.plugin.PluginService;
34 34 import org.thingsboard.server.dao.relation.RelationService;
  35 +import org.thingsboard.server.dao.rule.RuleChainService;
35 36 import org.thingsboard.server.dao.rule.RuleService;
36 37 import org.thingsboard.server.dao.tenant.TenantService;
37 38 import org.thingsboard.server.dao.timeseries.TimeseriesService;
... ... @@ -56,6 +57,7 @@ public final class SharedPluginProcessingContext {
56 57 final AssetService assetService;
57 58 final DeviceService deviceService;
58 59 final RuleService ruleService;
  60 + final RuleChainService ruleChainService;
59 61 final PluginService pluginService;
60 62 final CustomerService customerService;
61 63 final TenantService tenantService;
... ... @@ -84,6 +86,7 @@ public final class SharedPluginProcessingContext {
84 86 this.rpcService = sysContext.getRpcService();
85 87 this.routingService = sysContext.getRoutingService();
86 88 this.ruleService = sysContext.getRuleService();
  89 + this.ruleChainService = sysContext.getRuleChainService();
87 90 this.pluginService = sysContext.getPluginService();
88 91 this.customerService = sysContext.getCustomerService();
89 92 this.tenantService = sysContext.getTenantService();
... ...
... ... @@ -550,6 +550,8 @@ public abstract class BaseController {
550 550 throw new ThingsboardException(YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION,
551 551 ThingsboardErrorCode.PERMISSION_DENIED);
552 552
  553 + } else if (tenantId.getId().equals(ModelConstants.NULL_UUID)) {
  554 + ruleChain.setConfiguration(null);
553 555 }
554 556 }
555 557 return ruleChain;
... ...
... ... @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.page.TextPageLink;
28 28 import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
29 29 import org.thingsboard.server.common.data.plugin.PluginMetaData;
30 30 import org.thingsboard.server.common.data.rule.RuleChain;
  31 +import org.thingsboard.server.common.data.rule.RuleChainMetaData;
31 32 import org.thingsboard.server.common.data.security.Authority;
32 33 import org.thingsboard.server.dao.model.ModelConstants;
33 34 import org.thingsboard.server.exception.ThingsboardException;
... ... @@ -54,6 +55,21 @@ public class RuleChainController extends BaseController {
54 55 }
55 56
56 57 @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
  58 + @RequestMapping(value = "/ruleChain/{ruleChainId}/metadata", method = RequestMethod.GET)
  59 + @ResponseBody
  60 + public RuleChainMetaData getRuleChainMetaData(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
  61 + checkParameter(RULE_CHAIN_ID, strRuleChainId);
  62 + try {
  63 + RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId));
  64 + checkRuleChain(ruleChainId);
  65 + return ruleChainService.loadRuleChainMetaData(ruleChainId);
  66 + } catch (Exception e) {
  67 + throw handleException(e);
  68 + }
  69 + }
  70 +
  71 +
  72 + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
57 73 @RequestMapping(value = "/ruleChain", method = RequestMethod.POST)
58 74 @ResponseBody
59 75 public RuleChain saveRuleChain(@RequestBody RuleChain ruleChain) throws ThingsboardException {
... ... @@ -77,6 +93,28 @@ public class RuleChainController extends BaseController {
77 93 }
78 94
79 95 @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
  96 + @RequestMapping(value = "/ruleChain/metadata", method = RequestMethod.POST)
  97 + @ResponseBody
  98 + public RuleChainMetaData saveRuleChainMetaData(@RequestBody RuleChainMetaData ruleChainMetaData) throws ThingsboardException {
  99 + try {
  100 + RuleChain ruleChain = checkRuleChain(ruleChainMetaData.getRuleChainId());
  101 + RuleChainMetaData savedRuleChainMetaData = checkNotNull(ruleChainService.saveRuleChainMetaData(ruleChainMetaData));
  102 +
  103 + logEntityAction(ruleChain.getId(), ruleChain,
  104 + null,
  105 + ActionType.UPDATED, null, ruleChainMetaData);
  106 +
  107 + return savedRuleChainMetaData;
  108 + } catch (Exception e) {
  109 +
  110 + logEntityAction(emptyId(EntityType.RULE_CHAIN), null,
  111 + null, ActionType.UPDATED, e, ruleChainMetaData);
  112 +
  113 + throw handleException(e);
  114 + }
  115 + }
  116 +
  117 + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
80 118 @RequestMapping(value = "/ruleChains", params = {"limit"}, method = RequestMethod.GET)
81 119 @ResponseBody
82 120 public TextPageData<RuleChain> getRuleChains(
... ...
... ... @@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.id.*;
38 38 import org.thingsboard.server.common.data.kv.AttributeKvEntry;
39 39 import org.thingsboard.server.common.data.page.TimePageData;
40 40 import org.thingsboard.server.common.data.page.TimePageLink;
  41 +import org.thingsboard.server.common.data.rule.RuleChainMetaData;
41 42 import org.thingsboard.server.common.data.security.DeviceCredentials;
42 43 import org.thingsboard.server.dao.audit.sink.AuditLogSink;
43 44 import org.thingsboard.server.dao.entity.EntityService;
... ... @@ -158,11 +159,20 @@ public class AuditLogServiceImpl implements AuditLogService {
158 159 switch(actionType) {
159 160 case ADDED:
160 161 case UPDATED:
161   - ObjectNode entityNode = objectMapper.valueToTree(entity);
162   - if (entityId.getEntityType() == EntityType.DASHBOARD) {
163   - entityNode.put("configuration", "");
  162 + if (entity != null) {
  163 + ObjectNode entityNode = objectMapper.valueToTree(entity);
  164 + if (entityId.getEntityType() == EntityType.DASHBOARD) {
  165 + entityNode.put("configuration", "");
  166 + }
  167 + actionData.set("entity", entityNode);
  168 + }
  169 + if (entityId.getEntityType() == EntityType.RULE_CHAIN) {
  170 + RuleChainMetaData ruleChainMetaData = extractParameter(RuleChainMetaData.class, additionalInfo);
  171 + if (ruleChainMetaData != null) {
  172 + ObjectNode ruleChainMetaDataNode = objectMapper.valueToTree(ruleChainMetaData);
  173 + actionData.set("metadata", ruleChainMetaDataNode);
  174 + }
164 175 }
165   - actionData.set("entity", entityNode);
166 176 break;
167 177 case DELETED:
168 178 case ACTIVATED:
... ...
... ... @@ -16,6 +16,7 @@
16 16
17 17 package org.thingsboard.server.dao.rule;
18 18
  19 +import com.google.common.util.concurrent.ListenableFuture;
19 20 import lombok.extern.slf4j.Slf4j;
20 21 import org.apache.commons.lang3.StringUtils;
21 22 import org.springframework.beans.factory.annotation.Autowired;
... ... @@ -219,6 +220,12 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
219 220 }
220 221
221 222 @Override
  223 + public ListenableFuture<RuleChain> findRuleChainByIdAsync(RuleChainId ruleChainId) {
  224 + Validator.validateId(ruleChainId, "Incorrect rule chain id for search request.");
  225 + return ruleChainDao.findByIdAsync(ruleChainId.getId());
  226 + }
  227 +
  228 + @Override
222 229 public RuleChain getRootTenantRuleChain(TenantId tenantId) {
223 230 Validator.validateId(tenantId, "Incorrect tenant id for search request.");
224 231 List<EntityRelation> relations = relationService.findByFrom(tenantId, RelationTypeGroup.RULE_CHAIN);
... ...
... ... @@ -16,6 +16,7 @@
16 16
17 17 package org.thingsboard.server.dao.rule;
18 18
  19 +import com.google.common.util.concurrent.ListenableFuture;
19 20 import org.thingsboard.server.common.data.id.RuleChainId;
20 21 import org.thingsboard.server.common.data.id.RuleNodeId;
21 22 import org.thingsboard.server.common.data.id.TenantId;
... ... @@ -41,6 +42,8 @@ public interface RuleChainService {
41 42
42 43 RuleChain findRuleChainById(RuleChainId ruleChainId);
43 44
  45 + ListenableFuture<RuleChain> findRuleChainByIdAsync(RuleChainId ruleChainId);
  46 +
44 47 RuleChain getRootTenantRuleChain(TenantId tenantId);
45 48
46 49 List<RuleNode> getRuleChainNodes(RuleChainId ruleChainId);
... ...
  1 +/*
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +export default angular.module('thingsboard.api.ruleChain', [])
  17 + .factory('ruleChainService', RuleChainService).name;
  18 +
  19 +/*@ngInject*/
  20 +function RuleChainService($http, $q) {
  21 +
  22 + var service = {
  23 + getSystemRuleChains: getSystemRuleChains,
  24 + getTenantRuleChains: getTenantRuleChains,
  25 + getRuleChains: getRuleChains,
  26 + getRuleChain: getRuleChain,
  27 + saveRuleChain: saveRuleChain,
  28 + deleteRuleChain: deleteRuleChain,
  29 + getRuleChainMetaData: getRuleChainMetaData,
  30 + saveRuleChainMetaData: saveRuleChainMetaData
  31 + };
  32 +
  33 + return service;
  34 +
  35 + function getSystemRuleChains (pageLink, config) {
  36 + var deferred = $q.defer();
  37 + var url = '/api/system/ruleChains?limit=' + pageLink.limit;
  38 + if (angular.isDefined(pageLink.textSearch)) {
  39 + url += '&textSearch=' + pageLink.textSearch;
  40 + }
  41 + if (angular.isDefined(pageLink.idOffset)) {
  42 + url += '&idOffset=' + pageLink.idOffset;
  43 + }
  44 + if (angular.isDefined(pageLink.textOffset)) {
  45 + url += '&textOffset=' + pageLink.textOffset;
  46 + }
  47 + $http.get(url, config).then(function success(response) {
  48 + deferred.resolve(response.data);
  49 + }, function fail() {
  50 + deferred.reject();
  51 + });
  52 + return deferred.promise;
  53 + }
  54 +
  55 + function getTenantRuleChains (pageLink, config) {
  56 + var deferred = $q.defer();
  57 + var url = '/api/tenant/ruleChains?limit=' + pageLink.limit;
  58 + if (angular.isDefined(pageLink.textSearch)) {
  59 + url += '&textSearch=' + pageLink.textSearch;
  60 + }
  61 + if (angular.isDefined(pageLink.idOffset)) {
  62 + url += '&idOffset=' + pageLink.idOffset;
  63 + }
  64 + if (angular.isDefined(pageLink.textOffset)) {
  65 + url += '&textOffset=' + pageLink.textOffset;
  66 + }
  67 + $http.get(url, config).then(function success(response) {
  68 + deferred.resolve(response.data);
  69 + }, function fail() {
  70 + deferred.reject();
  71 + });
  72 + return deferred.promise;
  73 + }
  74 +
  75 + function getRuleChains (pageLink, config) {
  76 + var deferred = $q.defer();
  77 + var url = '/api/ruleChains?limit=' + pageLink.limit;
  78 + if (angular.isDefined(pageLink.textSearch)) {
  79 + url += '&textSearch=' + pageLink.textSearch;
  80 + }
  81 + if (angular.isDefined(pageLink.idOffset)) {
  82 + url += '&idOffset=' + pageLink.idOffset;
  83 + }
  84 + if (angular.isDefined(pageLink.textOffset)) {
  85 + url += '&textOffset=' + pageLink.textOffset;
  86 + }
  87 + $http.get(url, config).then(function success(response) {
  88 + deferred.resolve(response.data);
  89 + }, function fail() {
  90 + deferred.reject();
  91 + });
  92 + return deferred.promise;
  93 + }
  94 +
  95 + function getRuleChain(ruleChainId, config) {
  96 + var deferred = $q.defer();
  97 + var url = '/api/ruleChain/' + ruleChainId;
  98 + $http.get(url, config).then(function success(response) {
  99 + deferred.resolve(response.data);
  100 + }, function fail() {
  101 + deferred.reject();
  102 + });
  103 + return deferred.promise;
  104 + }
  105 +
  106 + function saveRuleChain(ruleChain) {
  107 + var deferred = $q.defer();
  108 + var url = '/api/ruleChain';
  109 + $http.post(url, ruleChain).then(function success(response) {
  110 + deferred.resolve(response.data);
  111 + }, function fail() {
  112 + deferred.reject();
  113 + });
  114 + return deferred.promise;
  115 + }
  116 +
  117 + function deleteRuleChain(ruleChainId) {
  118 + var deferred = $q.defer();
  119 + var url = '/api/ruleChain/' + ruleChainId;
  120 + $http.delete(url).then(function success() {
  121 + deferred.resolve();
  122 + }, function fail() {
  123 + deferred.reject();
  124 + });
  125 + return deferred.promise;
  126 + }
  127 +
  128 + function getRuleChainMetaData(ruleChainId, config) {
  129 + var deferred = $q.defer();
  130 + var url = '/api/ruleChain/' + ruleChainId + '/metadata';
  131 + $http.get(url, config).then(function success(response) {
  132 + deferred.resolve(response.data);
  133 + }, function fail() {
  134 + deferred.reject();
  135 + });
  136 + return deferred.promise;
  137 + }
  138 +
  139 + function saveRuleChainMetaData(ruleChainMetaData) {
  140 + var deferred = $q.defer();
  141 + var url = '/api/ruleChain/metadata';
  142 + $http.post(url, ruleChainMetaData).then(function success(response) {
  143 + deferred.resolve(response.data);
  144 + }, function fail() {
  145 + deferred.reject();
  146 + });
  147 + return deferred.promise;
  148 + }
  149 +
  150 +}
... ...
... ... @@ -73,6 +73,7 @@ import thingsboardApiAttribute from './api/attribute.service';
73 73 import thingsboardApiEntity from './api/entity.service';
74 74 import thingsboardApiAlarm from './api/alarm.service';
75 75 import thingsboardApiAuditLog from './api/audit-log.service';
  76 +import thingsboardApiRuleChain from './api/rule-chain.service';
76 77
77 78 import 'typeface-roboto';
78 79 import 'font-awesome/css/font-awesome.min.css';
... ... @@ -135,6 +136,7 @@ angular.module('thingsboard', [
135 136 thingsboardApiEntity,
136 137 thingsboardApiAlarm,
137 138 thingsboardApiAuditLog,
  139 + thingsboardApiRuleChain,
138 140 uiRouter])
139 141 .config(AppConfig)
140 142 .factory('globalInterceptor', GlobalInterceptor)
... ...
... ... @@ -294,7 +294,8 @@ export default angular.module('thingsboard.types', [])
294 294 customer: "CUSTOMER",
295 295 user: "USER",
296 296 dashboard: "DASHBOARD",
297   - alarm: "ALARM"
  297 + alarm: "ALARM",
  298 + rulechain: "RULE_CHAIN"
298 299 },
299 300 aliasEntityType: {
300 301 current_customer: "CURRENT_CUSTOMER"
... ... @@ -354,6 +355,12 @@ export default angular.module('thingsboard.types', [])
354 355 list: 'entity.list-of-alarms',
355 356 nameStartsWith: 'entity.alarm-name-starts-with'
356 357 },
  358 + "RULE_CHAIN": {
  359 + type: 'entity.type-rulechain',
  360 + typePlural: 'entity.type-rulechains',
  361 + list: 'entity.list-of-rulechains',
  362 + nameStartsWith: 'entity.rulechain-name-starts-with'
  363 + },
357 364 "CURRENT_CUSTOMER": {
358 365 type: 'entity.type-current-customer',
359 366 list: 'entity.type-current-customer'
... ...
... ... @@ -26,7 +26,7 @@ import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html';
26 26 /*@ngInject*/
27 27 export default function ImportExport($log, $translate, $q, $mdDialog, $document, $http, itembuffer, utils, types,
28 28 dashboardUtils, entityService, dashboardService, pluginService, ruleService,
29   - widgetService, toast, attributeService) {
  29 + ruleChainService, widgetService, toast, attributeService) {
30 30
31 31
32 32 var service = {
... ... @@ -38,6 +38,8 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
38 38 importPlugin: importPlugin,
39 39 exportRule: exportRule,
40 40 importRule: importRule,
  41 + exportRuleChain: exportRuleChain,
  42 + importRuleChain: importRuleChain,
41 43 exportWidgetType: exportWidgetType,
42 44 importWidgetType: importWidgetType,
43 45 exportWidgetsBundle: exportWidgetsBundle,
... ... @@ -275,6 +277,61 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
275 277 return true;
276 278 }
277 279
  280 + // Rule chain functions
  281 +
  282 + function exportRuleChain(ruleChainId) {
  283 + ruleChainService.getRuleChain(ruleChainId).then(
  284 + function success(ruleChain) {
  285 + var name = ruleChain.name;
  286 + name = name.toLowerCase().replace(/\W/g,"_");
  287 + exportToPc(prepareExport(ruleChain), name + '.json');
  288 + //TODO: metadata
  289 + },
  290 + function fail(rejection) {
  291 + var message = rejection;
  292 + if (!message) {
  293 + message = $translate.instant('error.unknown-error');
  294 + }
  295 + toast.showError($translate.instant('rulechain.export-failed-error', {error: message}));
  296 + }
  297 + );
  298 + }
  299 +
  300 + function importRuleChain($event) {
  301 + var deferred = $q.defer();
  302 + openImportDialog($event, 'rulechain.import', 'rulechain.rulechain-file').then(
  303 + function success(ruleChain) {
  304 + if (!validateImportedRuleChain(ruleChain)) {
  305 + toast.showError($translate.instant('rulechain.invalid-rulechain-file-error'));
  306 + deferred.reject();
  307 + } else {
  308 + //TODO: rulechain metadata
  309 + ruleChainService.saveRuleChain(ruleChain).then(
  310 + function success() {
  311 + deferred.resolve();
  312 + },
  313 + function fail() {
  314 + deferred.reject();
  315 + }
  316 + );
  317 + }
  318 + },
  319 + function fail() {
  320 + deferred.reject();
  321 + }
  322 + );
  323 + return deferred.promise;
  324 + }
  325 +
  326 + function validateImportedRuleChain(ruleChain) {
  327 + //TODO: rulechain metadata
  328 + if (angular.isUndefined(ruleChain.name))
  329 + {
  330 + return false;
  331 + }
  332 + return true;
  333 + }
  334 +
278 335 // Plugin functions
279 336
280 337 function exportPlugin(pluginId) {
... ...
... ... @@ -49,6 +49,7 @@ import thingsboardWidgetLibrary from '../widget';
49 49 import thingsboardDashboard from '../dashboard';
50 50 import thingsboardPlugin from '../plugin';
51 51 import thingsboardRule from '../rule';
  52 +import thingsboardRuleChain from '../rulechain';
52 53
53 54 import thingsboardJsonForm from '../jsonform';
54 55
... ... @@ -81,6 +82,7 @@ export default angular.module('thingsboard.home', [
81 82 thingsboardDashboard,
82 83 thingsboardPlugin,
83 84 thingsboardRule,
  85 + thingsboardRuleChain,
84 86 thingsboardJsonForm,
85 87 thingsboardApiDevice,
86 88 thingsboardApiLogin,
... ...
... ... @@ -745,6 +745,10 @@ export default angular.module('thingsboard.locale', [])
745 745 "type-alarms": "Alarms",
746 746 "list-of-alarms": "{ count, select, 1 {One alarms} other {List of # alarms} }",
747 747 "alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'",
  748 + "type-rulechain": "Rule chain",
  749 + "type-rulechains": "Rule chains",
  750 + "list-of-rulechains": "{ count, select, 1 {One rule chain} other {List of # rule chains} }",
  751 + "rulechain-name-starts-with": "Rule chains whose names start with '{{prefix}}'",
748 752 "type-current-customer": "Current Customer",
749 753 "search": "Search entities",
750 754 "selected-entities": "{ count, select, 1 {1 entity} other {# entities} } selected",
... ... @@ -1133,6 +1137,38 @@ export default angular.module('thingsboard.locale', [])
1133 1137 "no-rules-matching": "No rules matching '{{entity}}' were found.",
1134 1138 "rule-required": "Rule is required"
1135 1139 },
  1140 + "rulechain": {
  1141 + "rulechain": "Rule chain",
  1142 + "rulechains": "Rule chains",
  1143 + "delete": "Delete rule chain",
  1144 + "name": "Name",
  1145 + "name-required": "Name is required.",
  1146 + "description": "Description",
  1147 + "add": "Add Rule Chain",
  1148 + "delete-rulechain-title": "Are you sure you want to delete the rule chain '{{ruleChainName}}'?",
  1149 + "delete-rulechain-text": "Be careful, after the confirmation the rule chain and all related data will become unrecoverable.",
  1150 + "delete-rulechains-title": "Are you sure you want to delete { count, select, 1 {1 rule chain} other {# rule chains} }?",
  1151 + "delete-rulechains-action-title": "Delete { count, select, 1 {1 rule chain} other {# rule chains} }",
  1152 + "delete-rulechains-text": "Be careful, after the confirmation all selected rule chains will be removed and all related data will become unrecoverable.",
  1153 + "add-rulechain-text": "Add new rule chain",
  1154 + "no-rulechains-text": "No rule chains found",
  1155 + "rulechain-details": "Rule chain details",
  1156 + "details": "Details",
  1157 + "events": "Events",
  1158 + "system": "System",
  1159 + "import": "Import rule chain",
  1160 + "export": "Export rule chain",
  1161 + "export-failed-error": "Unable to export rule chain: {{error}}",
  1162 + "create-new-rulechain": "Create new rule chain",
  1163 + "rule-file": "Rule chain file",
  1164 + "invalid-rulechain-file-error": "Unable to import rule chain: Invalid rule chain data structure.",
  1165 + "copyId": "Copy rule chain Id",
  1166 + "idCopiedMessage": "Rule chain Id has been copied to clipboard",
  1167 + "select-rulechain": "Select rule chain",
  1168 + "no-rulechains-matching": "No rule chains matching '{{entity}}' were found.",
  1169 + "rulechain-required": "Rule chain is required",
  1170 + "management": "Rules management"
  1171 + },
1136 1172 "rule-plugin": {
1137 1173 "management": "Rules and plugins management"
1138 1174 },
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2018 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<md-dialog aria-label="{{ 'rulechain.add' | translate }}" tb-help="'rulechains'" help-container-id="help-container">
  19 + <form name="theForm" ng-submit="vm.add()">
  20 + <md-toolbar>
  21 + <div class="md-toolbar-tools">
  22 + <h2 translate>rulechain.add</h2>
  23 + <span flex></span>
  24 + <div id="help-container"></div>
  25 + <md-button class="md-icon-button" ng-click="vm.cancel()">
  26 + <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
  27 + </md-button>
  28 + </div>
  29 + </md-toolbar>
  30 + <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
  31 + <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
  32 + <md-dialog-content>
  33 + <div class="md-dialog-content">
  34 + <tb-rule-chain rule-chain="vm.item" is-edit="true" the-form="theForm"></tb-rule-chain>
  35 + </div>
  36 + </md-dialog-content>
  37 + <md-dialog-actions layout="row">
  38 + <span flex></span>
  39 + <md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
  40 + class="md-raised md-primary">
  41 + {{ 'action.add' | translate }}
  42 + </md-button>
  43 + <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
  44 + translate }}
  45 + </md-button>
  46 + </md-dialog-actions>
  47 + </form>
  48 +</md-dialog>
... ...
  1 +/*
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +import RuleChainRoutes from './rulechain.routes';
  18 +import RuleChainController from './rulechain.controller';
  19 +import RuleChainDirective from './rulechain.directive';
  20 +
  21 +export default angular.module('thingsboard.ruleChain', [])
  22 + .config(RuleChainRoutes)
  23 + .controller('RuleChainController', RuleChainController)
  24 + .directive('tbRuleChain', RuleChainDirective)
  25 + .name;
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2018 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div class="tb-uppercase" ng-if="item && parentCtl.types.id.nullUid === item.tenantId.id" translate>rulechain.system</div>
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2018 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<md-button ng-click="onExportRuleChain({event: $event})"
  19 + ng-show="!isEdit"
  20 + class="md-raised md-primary">{{ 'rulechain.export' | translate }}</md-button>
  21 +<md-button ng-click="onDeleteRuleChain({event: $event})"
  22 + ng-show="!isEdit && !isReadOnly"
  23 + class="md-raised md-primary">{{ 'rulechain.delete' | translate }}</md-button>
  24 +
  25 +<div layout="row">
  26 + <md-button ngclipboard data-clipboard-action="copy"
  27 + ngclipboard-success="onRuleChainIdCopied(e)"
  28 + data-clipboard-text="{{ruleChain.id.id}}" ng-show="!isEdit"
  29 + class="md-raised">
  30 + <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
  31 + <span translate>rulechain.copyId</span>
  32 + </md-button>
  33 +</div>
  34 +
  35 +<md-content class="md-padding tb-rulechain" layout="column">
  36 + <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
  37 + <md-input-container class="md-block">
  38 + <label translate>rulechain.name</label>
  39 + <input required name="name" ng-model="ruleChain.name">
  40 + <div ng-messages="theForm.name.$error">
  41 + <div translate ng-message="required">rulechain.name-required</div>
  42 + </div>
  43 + </md-input-container>
  44 + <md-input-container class="md-block">
  45 + <label translate>rulechain.description</label>
  46 + <textarea ng-model="ruleChain.additionalInfo.description" rows="2"></textarea>
  47 + </md-input-container>
  48 + </fieldset>
  49 +</md-content>
... ...
  1 +/*
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +/* eslint-disable import/no-unresolved, import/default */
  17 +
  18 +import addRuleChainTemplate from './add-rulechain.tpl.html';
  19 +import ruleChainCard from './rulechain-card.tpl.html';
  20 +
  21 +/* eslint-enable import/no-unresolved, import/default */
  22 +
  23 +/*@ngInject*/
  24 +export default function RuleChainController(ruleChainService, userService, importExport, $state, $stateParams, $filter, $translate, types) {
  25 +
  26 + var ruleChainActionsList = [
  27 + {
  28 + onAction: function ($event, item) {
  29 + exportRuleChain($event, item);
  30 + },
  31 + name: function() { $translate.instant('action.export') },
  32 + details: function() { return $translate.instant('rulechain.export') },
  33 + icon: "file_download"
  34 + },
  35 + {
  36 + onAction: function ($event, item) {
  37 + vm.grid.deleteItem($event, item);
  38 + },
  39 + name: function() { return $translate.instant('action.delete') },
  40 + details: function() { return $translate.instant('rulechain.delete') },
  41 + icon: "delete",
  42 + isEnabled: isRuleChainEditable
  43 + }
  44 + ];
  45 +
  46 + var ruleChainAddItemActionsList = [
  47 + {
  48 + onAction: function ($event) {
  49 + vm.grid.addItem($event);
  50 + },
  51 + name: function() { return $translate.instant('action.create') },
  52 + details: function() { return $translate.instant('rulechain.create-new-rulechain') },
  53 + icon: "insert_drive_file"
  54 + },
  55 + {
  56 + onAction: function ($event) {
  57 + importExport.importRuleChain($event).then(
  58 + function() {
  59 + vm.grid.refreshList();
  60 + }
  61 + );
  62 + },
  63 + name: function() { return $translate.instant('action.import') },
  64 + details: function() { return $translate.instant('rulechain.import') },
  65 + icon: "file_upload"
  66 + }
  67 + ];
  68 +
  69 + var vm = this;
  70 +
  71 + vm.types = types;
  72 +
  73 + vm.ruleChainGridConfig = {
  74 +
  75 + refreshParamsFunc: null,
  76 +
  77 + deleteItemTitleFunc: deleteRuleChainTitle,
  78 + deleteItemContentFunc: deleteRuleChainText,
  79 + deleteItemsTitleFunc: deleteRuleChainsTitle,
  80 + deleteItemsActionTitleFunc: deleteRuleChainsActionTitle,
  81 + deleteItemsContentFunc: deleteRuleChainsText,
  82 +
  83 + fetchItemsFunc: fetchRuleChains,
  84 + saveItemFunc: saveRuleChain,
  85 + deleteItemFunc: deleteRuleChain,
  86 +
  87 + getItemTitleFunc: getRuleChainTitle,
  88 + itemCardTemplateUrl: ruleChainCard,
  89 + parentCtl: vm,
  90 +
  91 + actionsList: ruleChainActionsList,
  92 + addItemActions: ruleChainAddItemActionsList,
  93 +
  94 + onGridInited: gridInited,
  95 +
  96 + addItemTemplateUrl: addRuleChainTemplate,
  97 +
  98 + addItemText: function() { return $translate.instant('rulechain.add-rulechain-text') },
  99 + noItemsText: function() { return $translate.instant('rulechain.no-rulechains-text') },
  100 + itemDetailsText: function() { return $translate.instant('rulechain.rulechain-details') },
  101 + isSelectionEnabled: isRuleChainEditable,
  102 + isDetailsReadOnly: function(ruleChain) {
  103 + return !isRuleChainEditable(ruleChain);
  104 + }
  105 + };
  106 +
  107 + if (angular.isDefined($stateParams.items) && $stateParams.items !== null) {
  108 + vm.ruleChainGridConfig.items = $stateParams.items;
  109 + }
  110 +
  111 + if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) {
  112 + vm.ruleChainGridConfig.topIndex = $stateParams.topIndex;
  113 + }
  114 +
  115 + vm.isRuleChainEditable = isRuleChainEditable;
  116 +
  117 + vm.exportRuleChain = exportRuleChain;
  118 +
  119 + function deleteRuleChainTitle(ruleChain) {
  120 + return $translate.instant('rulechain.delete-rulechain-title', {ruleChainName: ruleChain.name});
  121 + }
  122 +
  123 + function deleteRuleChainText() {
  124 + return $translate.instant('rulechain.delete-rulechain-text');
  125 + }
  126 +
  127 + function deleteRuleChainsTitle(selectedCount) {
  128 + return $translate.instant('rulechain.delete-rulechains-title', {count: selectedCount}, 'messageformat');
  129 + }
  130 +
  131 + function deleteRuleChainsActionTitle(selectedCount) {
  132 + return $translate.instant('rulechain.delete-rulechains-action-title', {count: selectedCount}, 'messageformat');
  133 + }
  134 +
  135 + function deleteRuleChainsText() {
  136 + return $translate.instant('rulechain.delete-rulechains-text');
  137 + }
  138 +
  139 + function gridInited(grid) {
  140 + vm.grid = grid;
  141 + }
  142 +
  143 + function fetchRuleChains(pageLink) {
  144 + return ruleChainService.getRuleChains(pageLink);
  145 + }
  146 +
  147 + function saveRuleChain(ruleChain) {
  148 + return ruleChainService.saveRuleChain(ruleChain);
  149 + }
  150 +
  151 + function deleteRuleChain(ruleChainId) {
  152 + return ruleChainService.deleteRuleChain(ruleChainId);
  153 + }
  154 +
  155 + function getRuleChainTitle(ruleChain) {
  156 + return ruleChain ? ruleChain.name : '';
  157 + }
  158 +
  159 + function isRuleChainEditable(ruleChain) {
  160 + if (userService.getAuthority() === 'TENANT_ADMIN') {
  161 + return ruleChain && ruleChain.tenantId.id != types.id.nullUid;
  162 + } else {
  163 + return userService.getAuthority() === 'SYS_ADMIN';
  164 + }
  165 + }
  166 +
  167 + function exportRuleChain($event, ruleChain) {
  168 + $event.stopPropagation();
  169 + importExport.exportRuleChain(ruleChain.id.id);
  170 + }
  171 +
  172 +}
... ...
  1 +/*
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +/* eslint-disable import/no-unresolved, import/default */
  18 +
  19 +import ruleChainFieldsetTemplate from './rulechain-fieldset.tpl.html';
  20 +
  21 +/* eslint-enable import/no-unresolved, import/default */
  22 +
  23 +/*@ngInject*/
  24 +export default function RuleChainDirective($compile, $templateCache, $mdDialog, $document, $q, $translate, types, toast) {
  25 + var linker = function (scope, element) {
  26 + var template = $templateCache.get(ruleChainFieldsetTemplate);
  27 + element.html(template);
  28 +
  29 + scope.onRuleChainIdCopied = function() {
  30 + toast.showSuccess($translate.instant('rulechain.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
  31 + };
  32 +
  33 + $compile(element.contents())(scope);
  34 + }
  35 + return {
  36 + restrict: "E",
  37 + link: linker,
  38 + scope: {
  39 + ruleChain: '=',
  40 + isEdit: '=',
  41 + isReadOnly: '=',
  42 + theForm: '=',
  43 + onExportRuleChain: '&',
  44 + onDeleteRuleChain: '&'
  45 + }
  46 + };
  47 +}
... ...
  1 +/*
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +/* eslint-disable import/no-unresolved, import/default */
  17 +
  18 +import ruleChainsTemplate from './rulechains.tpl.html';
  19 +
  20 +/* eslint-enable import/no-unresolved, import/default */
  21 +
  22 +/*@ngInject*/
  23 +export default function RuleChainRoutes($stateProvider) {
  24 +
  25 + $stateProvider
  26 + .state('home.ruleChains', {
  27 + url: '/ruleChains',
  28 + params: {'topIndex': 0},
  29 + module: 'private',
  30 + auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
  31 + views: {
  32 + "content@home": {
  33 + templateUrl: ruleChainsTemplate,
  34 + controllerAs: 'vm',
  35 + controller: 'RuleChainController'
  36 + }
  37 + },
  38 + data: {
  39 + searchEnabled: true,
  40 + pageTitle: 'rulechain.rulechains'
  41 + },
  42 + ncyBreadcrumb: {
  43 + label: '{"icon": "settings_ethernet", "label": "rulechain.rulechains"}'
  44 + }
  45 + });
  46 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2018 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<tb-grid grid-configuration="vm.ruleChainGridConfig">
  19 + <details-buttons tb-help="'rulechains'" help-container-id="help-container">
  20 + <div id="help-container"></div>
  21 + </details-buttons>
  22 + <md-tabs ng-class="{'tb-headless': (vm.grid.detailsConfig.isDetailsEditMode || !vm.isRuleChainEditable(vm.grid.operatingItem()))}"
  23 + id="tabs" md-border-bottom flex class="tb-absolute-fill">
  24 + <md-tab label="{{ 'rulechain.details' | translate }}">
  25 + <tb-rule-chain rule-chain="vm.grid.operatingItem()"
  26 + is-edit="vm.grid.detailsConfig.isDetailsEditMode"
  27 + is-read-only="vm.grid.isDetailsReadOnly(vm.grid.operatingItem())"
  28 + the-form="vm.grid.detailsForm"
  29 + on-export-rule-chain="vm.exportRuleChain(event, vm.grid.detailsConfig.currentItem)"
  30 + on-delete-rule-chain="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-rule-chain>
  31 + </md-tab>
  32 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.attributes' | translate }}">
  33 + <tb-attribute-table flex
  34 + entity-id="vm.grid.operatingItem().id.id"
  35 + entity-type="{{vm.types.entityType.rulechain}}"
  36 + entity-name="vm.grid.operatingItem().name"
  37 + default-attribute-scope="{{vm.types.attributesScope.server.value}}">
  38 + </tb-attribute-table>
  39 + </md-tab>
  40 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.latest-telemetry' | translate }}">
  41 + <tb-attribute-table flex
  42 + entity-id="vm.grid.operatingItem().id.id"
  43 + entity-type="{{vm.types.entityType.rulechain}}"
  44 + entity-name="vm.grid.operatingItem().name"
  45 + default-attribute-scope="{{vm.types.latestTelemetry.value}}"
  46 + disable-attribute-scope-selection="true">
  47 + </tb-attribute-table>
  48 + </md-tab>
  49 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'alarm.alarms' | translate }}">
  50 + <tb-alarm-table flex entity-type="vm.types.entityType.rulechain"
  51 + entity-id="vm.grid.operatingItem().id.id">
  52 + </tb-alarm-table>
  53 + </md-tab>
  54 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'rulechain.events' | translate }}">
  55 + <tb-event-table flex entity-type="vm.types.entityType.rulechain"
  56 + entity-id="vm.grid.operatingItem().id.id"
  57 + tenant-id="vm.grid.operatingItem().tenantId.id"
  58 + default-event-type="{{vm.types.eventType.lcEvent.value}}">
  59 + </tb-event-table>
  60 + </md-tab>
  61 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}">
  62 + <tb-relation-table flex
  63 + entity-id="vm.grid.operatingItem().id.id"
  64 + entity-type="{{vm.types.entityType.rulechain}}">
  65 + </tb-relation-table>
  66 + </md-tab>
  67 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem()) && vm.grid.isTenantAdmin()"
  68 + md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
  69 + <tb-audit-log-table flex entity-type="vm.types.entityType.rulechain"
  70 + entity-id="vm.grid.operatingItem().id.id"
  71 + audit-log-mode="{{vm.types.auditLogMode.entity}}">
  72 + </tb-audit-log-table>
  73 + </md-tab>
  74 + </md-tabs>
  75 +</tb-grid>
... ...
... ... @@ -79,6 +79,12 @@ function Menu(userService, $state, $rootScope) {
79 79 icon: 'settings_ethernet'
80 80 },
81 81 {
  82 + name: 'rulechain.rulechains',
  83 + type: 'link',
  84 + state: 'home.ruleChains',
  85 + icon: 'settings_ethernet'
  86 + },
  87 + {
82 88 name: 'tenant.tenants',
83 89 type: 'link',
84 90 state: 'home.tenants',
... ... @@ -128,6 +134,16 @@ function Menu(userService, $state, $rootScope) {
128 134 ]
129 135 },
130 136 {
  137 + name: 'rulechain.management',
  138 + places: [
  139 + {
  140 + name: 'rulechain.rulechains',
  141 + icon: 'settings_ethernet',
  142 + state: 'home.ruleChains'
  143 + }
  144 + ]
  145 + },
  146 + {
131 147 name: 'tenant.management',
132 148 places: [
133 149 {
... ... @@ -183,6 +199,12 @@ function Menu(userService, $state, $rootScope) {
183 199 icon: 'settings_ethernet'
184 200 },
185 201 {
  202 + name: 'rulechain.rulechains',
  203 + type: 'link',
  204 + state: 'home.ruleChains',
  205 + icon: 'settings_ethernet'
  206 + },
  207 + {
186 208 name: 'customer.customers',
187 209 type: 'link',
188 210 state: 'home.customers',
... ... @@ -236,6 +258,16 @@ function Menu(userService, $state, $rootScope) {
236 258 ]
237 259 },
238 260 {
  261 + name: 'rulechain.management',
  262 + places: [
  263 + {
  264 + name: 'rulechain.rulechains',
  265 + icon: 'settings_ethernet',
  266 + state: 'home.ruleChains'
  267 + }
  268 + ]
  269 + },
  270 + {
239 271 name: 'customer.management',
240 272 places: [
241 273 {
... ...