From ae45a62f2fa1d4667dd31e65e6be4d62515a94b9 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 14 Mar 2018 20:43:56 +0200 Subject: [PATCH] Rule Chains UI --- .../server/actors/ActorSystemContext.java | 4 + .../plugin/PluginProcessingContext.java | 26 +++ .../plugin/SharedPluginProcessingContext.java | 3 + .../server/controller/BaseController.java | 2 + .../controller/RuleChainController.java | 38 ++++ .../server/dao/audit/AuditLogServiceImpl.java | 18 +- .../server/dao/rule/BaseRuleChainService.java | 7 + .../server/dao/rule/RuleChainService.java | 3 + ui/src/app/api/rule-chain.service.js | 150 +++++++++++++++ ui/src/app/app.js | 2 + ui/src/app/common/types.constant.js | 9 +- .../import-export/import-export.service.js | 59 +++++- ui/src/app/layout/index.js | 2 + ui/src/app/locale/locale.constant.js | 36 ++++ ui/src/app/rulechain/add-rulechain.tpl.html | 48 +++++ ui/src/app/rulechain/index.js | 25 +++ ui/src/app/rulechain/rulechain-card.tpl.html | 18 ++ .../app/rulechain/rulechain-fieldset.tpl.html | 49 +++++ ui/src/app/rulechain/rulechain.controller.js | 172 ++++++++++++++++++ ui/src/app/rulechain/rulechain.directive.js | 47 +++++ ui/src/app/rulechain/rulechain.routes.js | 46 +++++ ui/src/app/rulechain/rulechains.tpl.html | 75 ++++++++ ui/src/app/services/menu.service.js | 32 ++++ 23 files changed, 865 insertions(+), 6 deletions(-) create mode 100644 ui/src/app/api/rule-chain.service.js create mode 100644 ui/src/app/rulechain/add-rulechain.tpl.html create mode 100644 ui/src/app/rulechain/index.js create mode 100644 ui/src/app/rulechain/rulechain-card.tpl.html create mode 100644 ui/src/app/rulechain/rulechain-fieldset.tpl.html create mode 100644 ui/src/app/rulechain/rulechain.controller.js create mode 100644 ui/src/app/rulechain/rulechain.directive.js create mode 100644 ui/src/app/rulechain/rulechain.routes.js create mode 100644 ui/src/app/rulechain/rulechains.tpl.html 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 77953c9dc1..f59ec63ed7 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; @@ -96,6 +97,9 @@ public class ActorSystemContext { @Autowired @Getter private RuleService ruleService; + @Autowired + @Getter private RuleChainService ruleChainService; + @Autowired @Getter private PluginService pluginService; 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 ce95ee4629..10f16ce8b1 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 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 06138a3308..182897025e 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 83b304fb8b..2a4ba719af 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 012e0770b3..f24646b56f 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; @@ -53,6 +54,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 @@ -76,6 +92,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 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 23fadeba0e..2a491300b0 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 1e791635c9..04207ec2f5 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; @@ -218,6 +219,12 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC return ruleChainDao.findById(ruleChainId.getId()); } + @Override + public ListenableFuture 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."); 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 6c44090564..e5f2840b8b 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 findRuleChainByIdAsync(RuleChainId ruleChainId); + RuleChain getRootTenantRuleChain(TenantId tenantId); List 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 0000000000..16afa747b8 --- /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 0b6a141ba3..138b2308f3 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 ef6ffde28a..77c21e1fd0 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 6071fd23ed..19c249f869 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 e5ca958342..e90334bf52 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 252884dfc7..ea576ecf51 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 0000000000..44d0ec364c --- /dev/null +++ b/ui/src/app/rulechain/add-rulechain.tpl.html @@ -0,0 +1,48 @@ + + +
+ +
+

rulechain.add

+ +
+ + + +
+
+ + + +
+ +
+
+ + + + {{ 'action.add' | translate }} + + {{ 'action.cancel' | + translate }} + + +
+
diff --git a/ui/src/app/rulechain/index.js b/ui/src/app/rulechain/index.js new file mode 100644 index 0000000000..faecc03e03 --- /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-card.tpl.html b/ui/src/app/rulechain/rulechain-card.tpl.html new file mode 100644 index 0000000000..48a572c8e0 --- /dev/null +++ b/ui/src/app/rulechain/rulechain-card.tpl.html @@ -0,0 +1,18 @@ + +
rulechain.system
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 0000000000..ec68c574bf --- /dev/null +++ b/ui/src/app/rulechain/rulechain-fieldset.tpl.html @@ -0,0 +1,49 @@ + +{{ 'rulechain.export' | translate }} +{{ 'rulechain.delete' | translate }} + +
+ + + rulechain.copyId + +
+ + +
+ + + +
+
rulechain.name-required
+
+
+ + + + +
+
diff --git a/ui/src/app/rulechain/rulechain.controller.js b/ui/src/app/rulechain/rulechain.controller.js new file mode 100644 index 0000000000..475ef4e09a --- /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 0000000000..b23cd98136 --- /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 0000000000..e8d6be49d2 --- /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/rulechains.tpl.html b/ui/src/app/rulechain/rulechains.tpl.html new file mode 100644 index 0000000000..a4fbd79a91 --- /dev/null +++ b/ui/src/app/rulechain/rulechains.tpl.html @@ -0,0 +1,75 @@ + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/ui/src/app/services/menu.service.js b/ui/src/app/services/menu.service.js index 9dbddd9485..5d97ea6113 100644 --- a/ui/src/app/services/menu.service.js +++ b/ui/src/app/services/menu.service.js @@ -78,6 +78,12 @@ function Menu(userService, $state, $rootScope) { state: 'home.rules', icon: 'settings_ethernet' }, + { + name: 'rulechain.rulechains', + type: 'link', + state: 'home.ruleChains', + icon: 'settings_ethernet' + }, { name: 'tenant.tenants', type: 'link', @@ -127,6 +133,16 @@ function Menu(userService, $state, $rootScope) { } ] }, + { + name: 'rulechain.management', + places: [ + { + name: 'rulechain.rulechains', + icon: 'settings_ethernet', + state: 'home.ruleChains' + } + ] + }, { name: 'tenant.management', places: [ @@ -182,6 +198,12 @@ function Menu(userService, $state, $rootScope) { state: 'home.rules', icon: 'settings_ethernet' }, + { + name: 'rulechain.rulechains', + type: 'link', + state: 'home.ruleChains', + icon: 'settings_ethernet' + }, { name: 'customer.customers', type: 'link', @@ -235,6 +257,16 @@ function Menu(userService, $state, $rootScope) { } ] }, + { + name: 'rulechain.management', + places: [ + { + name: 'rulechain.rulechains', + icon: 'settings_ethernet', + state: 'home.ruleChains' + } + ] + }, { name: 'customer.management', places: [