From 4512185cad02848fc1f6976e209ad7035e2598e6 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Fri, 18 Feb 2022 16:41:24 +0200 Subject: [PATCH 001/276] created Queue entity, dao and ui --- .../server/controller/BaseController.java | 18 +- .../server/controller/QueueController.java | 94 ++++- .../service/security/permission/Resource.java | 3 +- .../permission/SysAdminPermissions.java | 1 + .../permission/TenantAdminPermissions.java | 1 + .../routing/HashPartitionServiceTest.java | 7 +- .../server/queue/TbQueueAdmin.java | 2 + ...{QueueService.java => TbQueueService.java} | 2 +- .../server/dao/queue/QueueService.java | 48 +++ .../server/common/data/EntityType.java | 2 +- .../server/common/data/TenantProfile.java | 6 +- .../common/data/id/EntityIdFactory.java | 2 + .../server/common/data/id/QueueId.java | 43 +++ .../common/data/queue/ProcessingStrategy.java | 27 ++ .../data/queue/ProcessingStrategyType.java | 36 ++ .../server/common/data/queue/Queue.java | 42 +++ .../common/data/queue/SubmitStrategy.java | 24 ++ .../common/data/queue/SubmitStrategyType.java | 20 ++ ...ervice.java => DefaultTbQueueService.java} | 2 +- .../queue/discovery/HashPartitionService.java | 8 +- .../dao/device/DeviceProfileServiceImpl.java | 4 +- .../server/dao/model/ModelConstants.java | 16 + .../server/dao/model/sql/QueueEntity.java | 105 ++++++ .../dao/model/sql/TenantProfileEntity.java | 12 +- .../server/dao/queue/BaseQueueService.java | 333 ++++++++++++++++++ .../server/dao/queue/QueueDao.java | 38 ++ .../server/dao/sql/queue/JpaQueueDao.java | 86 +++++ .../server/dao/sql/queue/QueueRepository.java | 42 +++ .../server/dao/tenant/TenantServiceImpl.java | 5 + .../main/resources/sql/schema-entities.sql | 15 + ui-ngx/src/app/app.component.ts | 11 + ui-ngx/src/app/core/http/queue.service.ts | 25 +- ui-ngx/src/app/core/services/menu.service.ts | 18 +- .../profile/tenant-profile.component.html | 30 ++ .../profile/tenant-profile.component.ts | 18 + .../home/pages/admin/admin-routing.module.ts | 20 +- .../modules/home/pages/admin/admin.module.ts | 4 +- .../pages/admin/queue/queue.component.html | 182 ++++++++++ .../pages/admin/queue/queue.component.scss | 59 ++++ .../home/pages/admin/queue/queue.component.ts | 154 ++++++++ .../queue/queues-table-config.resolver.ts | 115 ++++++ .../queue/queue-type-list.component.html | 4 +- .../queue/queue-type-list.component.ts | 2 +- ui-ngx/src/app/shared/models/constants.ts | 3 +- .../app/shared/models/entity-type.models.ts | 18 +- ui-ngx/src/app/shared/models/id/queue-id.ts | 26 ++ ui-ngx/src/app/shared/models/queue.models.ts | 40 +++ ui-ngx/src/app/shared/models/tenant.model.ts | 2 + .../assets/locale/locale.constant-en_US.json | 65 +++- 49 files changed, 1801 insertions(+), 39 deletions(-) rename common/cluster-api/src/main/java/org/thingsboard/server/queue/{QueueService.java => TbQueueService.java} (96%) create mode 100644 common/dao-api/src/main/java/org/thingsboard/server/dao/queue/QueueService.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/queue/ProcessingStrategy.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/queue/ProcessingStrategyType.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/queue/SubmitStrategy.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/queue/SubmitStrategyType.java rename common/queue/src/main/java/org/thingsboard/server/queue/{DefaultQueueService.java => DefaultTbQueueService.java} (97%) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/sql/QueueEntity.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/queue/QueueDao.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueDao.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/queue/QueueRepository.java create mode 100644 ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/admin/queue/queues-table-config.resolver.ts create mode 100644 ui-ngx/src/app/shared/models/id/queue-id.ts 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 f945cc30b7..1dafef871a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -70,6 +70,7 @@ import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.RpcId; +import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TbResourceId; @@ -86,6 +87,7 @@ import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.rpc.Rpc; +import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; @@ -108,6 +110,7 @@ import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.dao.ota.OtaPackageService; +import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rpc.RpcService; import org.thingsboard.server.dao.rule.RuleChainService; @@ -271,6 +274,9 @@ public abstract class BaseController { @Autowired protected EntityActionService entityActionService; + @Autowired + protected QueueService queueService; + @Value("${server.log_controller_error_stack_trace}") @Getter private boolean logControllerErrorStackTrace; @@ -516,6 +522,9 @@ public abstract class BaseController { case OTA_PACKAGE: checkOtaPackageId(new OtaPackageId(entityId.getId()), operation); return; + case QUEUE: + checkQueueId(new QueueId(entityId.getId()), operation); + return; default: throw new IllegalArgumentException("Unsupported entity type: " + entityId.getEntityType()); } @@ -807,7 +816,14 @@ public abstract class BaseController { } } - @SuppressWarnings("unchecked") + protected Queue checkQueueId(QueueId queueId, Operation operation) throws ThingsboardException { + validateId(queueId, "Incorrect queueId " + queueId); + Queue queue = queueService.findQueueById(getCurrentUser().getTenantId(), queueId); + checkNotNull(queue); + accessControlService.checkPermission(getCurrentUser(), Resource.QUEUE, operation, queueId, queue); + return queue; + } + protected I emptyId(EntityType entityType) { return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID); } diff --git a/application/src/main/java/org/thingsboard/server/controller/QueueController.java b/application/src/main/java/org/thingsboard/server/controller/QueueController.java index 33cf7b5a0f..fef6fb9027 100644 --- a/application/src/main/java/org/thingsboard/server/controller/QueueController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QueueController.java @@ -20,17 +20,26 @@ import io.swagger.annotations.ApiParam; import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.QueueId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; -import org.thingsboard.server.queue.QueueService; +import org.thingsboard.server.queue.TbQueueService; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; import java.util.Set; +import java.util.UUID; import static org.thingsboard.server.controller.ControllerConstants.QUEUE_SERVICE_TYPE_ALLOWABLE_VALUES; import static org.thingsboard.server.controller.ControllerConstants.QUEUE_SERVICE_TYPE_DESCRIPTION; @@ -42,7 +51,7 @@ import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHO @RequiredArgsConstructor public class QueueController extends BaseController { - private final QueueService queueService; + private final TbQueueService tbQueueService; @ApiOperation(value = "Get queue names (getTenantQueuesByServiceType)", notes = "Returns a set of unique queue names based on service type. " + TENANT_AUTHORITY_PARAGRAPH) @@ -53,7 +62,86 @@ public class QueueController extends BaseController { @RequestParam String serviceType) throws ThingsboardException { checkParameter("serviceType", serviceType); try { - return queueService.getQueuesByServiceType(ServiceType.valueOf(serviceType)); + //TODO: replace for using new QueueService + return tbQueueService.getQueuesByServiceType(ServiceType.valueOf(serviceType)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/tenant/queues", params = {"serviceType", "pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getTenantQueuesByServiceType(@RequestParam String serviceType, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + checkParameter("serviceType", serviceType); + try { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + ServiceType type = ServiceType.valueOf(serviceType); + switch (type) { + case TB_RULE_ENGINE: + return queueService.findQueuesByTenantId(getTenantId(), pageLink); + default: + return new PageData<>(); + } + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/tenant/queues/{queueId}", method = RequestMethod.GET) + @ResponseBody + public Queue getQueueById(@PathVariable("queueId") String queueIdStr) throws ThingsboardException { + checkParameter("queueId", queueIdStr); + try { + QueueId queueId = new QueueId(UUID.fromString(queueIdStr)); + return checkNotNull(queueService.findQueueById(getTenantId(), queueId)); + } catch ( + Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/tenant/queues", params = {"serviceType"}, method = RequestMethod.POST) + @ResponseBody + public Queue saveQueue(@RequestBody Queue queue, + @RequestParam String serviceType) throws ThingsboardException { + checkParameter("serviceType", serviceType); + try { + queue.setTenantId(getCurrentUser().getTenantId()); + + checkEntity(queue.getId(), queue, Resource.QUEUE); + + ServiceType type = ServiceType.valueOf(serviceType); + switch (type) { + case TB_RULE_ENGINE: + queue.setTenantId(getTenantId()); + Queue savedQueue = queueService.saveQueue(queue); + checkNotNull(savedQueue); + return savedQueue; + default: + return null; + } + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/tenant/queues/{queueId}", method = RequestMethod.DELETE) + @ResponseBody + public void deleteQueue(@PathVariable("queueId") String queueIdStr) throws ThingsboardException { + checkParameter("queueId", queueIdStr); + try { + QueueId queueId = new QueueId(toUUID(queueIdStr)); + checkQueueId(queueId, Operation.DELETE); + queueService.deleteQueue(getTenantId(), queueId); } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java index 1414afe232..f86680f156 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java @@ -40,7 +40,8 @@ public enum Resource { TB_RESOURCE(EntityType.TB_RESOURCE), OTA_PACKAGE(EntityType.OTA_PACKAGE), EDGE(EntityType.EDGE), - RPC(EntityType.RPC); + RPC(EntityType.RPC), + QUEUE(EntityType.QUEUE); private final EntityType entityType; diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java index ca33716692..20e2001c87 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java @@ -39,6 +39,7 @@ public class SysAdminPermissions extends AbstractPermissions { put(Resource.OAUTH2_CONFIGURATION_TEMPLATE, PermissionChecker.allowAllPermissionChecker); put(Resource.TENANT_PROFILE, PermissionChecker.allowAllPermissionChecker); put(Resource.TB_RESOURCE, systemEntityPermissionChecker); + put(Resource.QUEUE, systemEntityPermissionChecker); } private static final PermissionChecker systemEntityPermissionChecker = new PermissionChecker() { diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index 4ff884f0e1..bcc6251315 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java @@ -45,6 +45,7 @@ public class TenantAdminPermissions extends AbstractPermissions { put(Resource.OTA_PACKAGE, tenantEntityPermissionChecker); put(Resource.EDGE, tenantEntityPermissionChecker); put(Resource.RPC, tenantEntityPermissionChecker); + put(Resource.QUEUE, tenantEntityPermissionChecker); } public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { diff --git a/application/src/test/java/org/thingsboard/server/service/cluster/routing/HashPartitionServiceTest.java b/application/src/test/java/org/thingsboard/server/service/cluster/routing/HashPartitionServiceTest.java index d304a567ab..09fe580661 100644 --- a/application/src/test/java/org/thingsboard/server/service/cluster/routing/HashPartitionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cluster/routing/HashPartitionServiceTest.java @@ -21,13 +21,12 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.context.ApplicationEventPublisher; import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.queue.QueueService; +import org.thingsboard.server.queue.TbQueueService; import org.thingsboard.server.queue.discovery.HashPartitionService; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; @@ -59,7 +58,7 @@ public class HashPartitionServiceTest { private TenantRoutingInfoService routingInfoService; private ApplicationEventPublisher applicationEventPublisher; private TbQueueRuleEngineSettings ruleEngineSettings; - private QueueService queueService; + private TbQueueService queueService; private String hashFunctionName = "sha256"; @@ -70,7 +69,7 @@ public class HashPartitionServiceTest { applicationEventPublisher = mock(ApplicationEventPublisher.class); routingInfoService = mock(TenantRoutingInfoService.class); ruleEngineSettings = mock(TbQueueRuleEngineSettings.class); - queueService = mock(QueueService.class); + queueService = mock(TbQueueService.class); clusterRoutingService = new HashPartitionService(discoveryService, routingInfoService, applicationEventPublisher, diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueAdmin.java b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueAdmin.java index 6a581d522e..37c99dfccb 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueAdmin.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueAdmin.java @@ -20,4 +20,6 @@ public interface TbQueueAdmin { void createTopicIfNotExists(String topic); void destroy(); + + default void deleteTopic(String topic) { } } diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/queue/QueueService.java b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueService.java similarity index 96% rename from common/cluster-api/src/main/java/org/thingsboard/server/queue/QueueService.java rename to common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueService.java index 08155caf60..bc7964df7e 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/queue/QueueService.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueService.java @@ -19,7 +19,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import java.util.Set; -public interface QueueService { +public interface TbQueueService { Set getQueuesByServiceType(ServiceType serviceType); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/queue/QueueService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/queue/QueueService.java new file mode 100644 index 0000000000..edb51fa2a3 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/queue/QueueService.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.queue; + +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.QueueId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.queue.Queue; + +import java.util.List; + +public interface QueueService { + + Queue saveQueue(Queue queue); + + void deleteQueue(TenantId tenantId, QueueId queueId); + + List findQueuesByTenantId(TenantId tenantId); + + PageData findQueuesByTenantId(TenantId tenantId, PageLink pageLink); + + List findAllMainQueues(); + + List findAllQueues(); + + Queue findQueueById(TenantId tenantId, QueueId queueId); + + Queue findQueueByTenantIdAndName(TenantId tenantId, String name); + + Queue createDefaultMainQueue(TenantProfile tenantProfile, TenantId tenantId); + + void deleteQueuesByTenantId(TenantId tenantId); +} \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index 33a5753465..1ef5bb433a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -19,5 +19,5 @@ package org.thingsboard.server.common.data; * @author Andrew Shvayka */ public enum EntityType { - TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE, ENTITY_VIEW, WIDGETS_BUNDLE, WIDGET_TYPE, TENANT_PROFILE, DEVICE_PROFILE, API_USAGE_STATE, TB_RESOURCE, OTA_PACKAGE, EDGE, RPC; + TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE, ENTITY_VIEW, WIDGETS_BUNDLE, WIDGET_TYPE, TENANT_PROFILE, DEVICE_PROFILE, API_USAGE_STATE, TB_RESOURCE, OTA_PACKAGE, EDGE, RPC, QUEUE; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java index 27cc52ba9f..32b6ba88ba 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java @@ -57,7 +57,11 @@ public class TenantProfile extends SearchTextBased implements H @ApiModelProperty(position = 7, value = "If enabled, will push all messages related to this tenant and processed by the rule engine into separate queue. " + "Useful for complex microservices deployments, to isolate processing of the data for specific tenants", example = "true") private boolean isolatedTbRuleEngine; - @ApiModelProperty(position = 8, value = "Complex JSON object that contains profile settings: max devices, max assets, rate limits, etc.") + @ApiModelProperty(position = 8, value = "Max amount of queues configurable by isolated tenant.", example = "5") + private Integer maxNumberOfQueues; + @ApiModelProperty(position = 9, value = "Max amount of partitions per queue configurable by isolated tenant.", example = "10") + private Integer maxNumberOfPartitionsPerQueue; + @ApiModelProperty(position = 10, value = "Complex JSON object that contains profile settings: max devices, max assets, rate limits, etc.") private transient TenantProfileData profileData; @JsonIgnore private byte[] profileDataBytes; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index ad91dc8d88..8bd13b7c3f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -77,6 +77,8 @@ public class EntityIdFactory { return new EdgeId(uuid); case RPC: return new RpcId(uuid); + case QUEUE: + return new QueueId(uuid); } throw new IllegalArgumentException("EntityType " + type + " is not supported!"); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java new file mode 100644 index 0000000000..37a4136d46 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.common.data.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.thingsboard.server.common.data.EntityType; + +import java.util.UUID; + +public class QueueId extends UUIDBased implements EntityId { + + private static final long serialVersionUID = 1L; + + @JsonCreator + public QueueId(@JsonProperty("id") UUID id) { + super(id); + } + + public static QueueId fromString(String queueId) { + return new QueueId(UUID.fromString(queueId)); + } + + @JsonIgnore + @Override + public EntityType getEntityType() { + return EntityType.QUEUE; + } +} \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/ProcessingStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/ProcessingStrategy.java new file mode 100644 index 0000000000..8a315f14ad --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/ProcessingStrategy.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.common.data.queue; + +import lombok.Data; + +@Data +public class ProcessingStrategy { + private ProcessingStrategyType type; + private int retries; + private double failurePercentage; + private long pauseBetweenRetries; + private long maxPauseBetweenRetries; +} \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/ProcessingStrategyType.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/ProcessingStrategyType.java new file mode 100644 index 0000000000..7a367a2141 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/ProcessingStrategyType.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2022 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. + */ + +/** + * Copyright © 2016-2020 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. + */ +package org.thingsboard.server.common.data.queue; + +public enum ProcessingStrategyType { + SKIP_ALL_FAILURES, SKIP_ALL_FAILURES_AND_TIMED_OUT, RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT +} \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java new file mode 100644 index 0000000000..84776c88f4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.common.data.queue; + +import lombok.Data; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.id.QueueId; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +public class Queue extends BaseData implements HasName, HasTenantId { + private TenantId tenantId; + private String name; + private String topic; + private int pollInterval; + private int partitions; + private long packProcessingTimeout; + private SubmitStrategy submitStrategy; + private ProcessingStrategy processingStrategy; + + public Queue() { + } + + public Queue(QueueId id) { + super(id); + } +} \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/SubmitStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/SubmitStrategy.java new file mode 100644 index 0000000000..972433b038 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/SubmitStrategy.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.common.data.queue; + +import lombok.Data; + +@Data +public class SubmitStrategy { + private SubmitStrategyType type; + private int batchSize; +} \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/SubmitStrategyType.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/SubmitStrategyType.java new file mode 100644 index 0000000000..cb6e2989e5 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/SubmitStrategyType.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.common.data.queue; + +public enum SubmitStrategyType { + BURST, BATCH, SEQUENTIAL_BY_ORIGINATOR, SEQUENTIAL_BY_TENANT, SEQUENTIAL +} \ No newline at end of file diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/DefaultQueueService.java b/common/queue/src/main/java/org/thingsboard/server/queue/DefaultTbQueueService.java similarity index 97% rename from common/queue/src/main/java/org/thingsboard/server/queue/DefaultQueueService.java rename to common/queue/src/main/java/org/thingsboard/server/queue/DefaultTbQueueService.java index 427e2c1f1a..5a0e540ac4 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/DefaultQueueService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/DefaultTbQueueService.java @@ -31,7 +31,7 @@ import java.util.stream.Collectors; @Service @RequiredArgsConstructor -public class DefaultQueueService implements QueueService { +public class DefaultTbQueueService implements TbQueueService { private final TbQueueRuleEngineSettings ruleEngineSettings; private Set ruleEngineQueues; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index 7478d21816..4882a96f72 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -16,13 +16,11 @@ package org.thingsboard.server.queue.discovery; import com.google.common.hash.HashFunction; -import com.google.common.hash.Hasher; import com.google.common.hash.Hashing; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.queue.ServiceQueue; @@ -31,7 +29,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ServiceInfo; -import org.thingsboard.server.queue.QueueService; +import org.thingsboard.server.queue.TbQueueService; import org.thingsboard.server.queue.discovery.event.ClusterTopologyChangeEvent; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.discovery.event.ServiceListChangedEvent; @@ -66,7 +64,7 @@ public class HashPartitionService implements PartitionService { private final TbServiceInfoProvider serviceInfoProvider; private final TenantRoutingInfoService tenantRoutingInfoService; private final TbQueueRuleEngineSettings tbQueueRuleEngineSettings; - private final QueueService queueService; + private final TbQueueService queueService; private final ConcurrentMap partitionTopics = new ConcurrentHashMap<>(); private final ConcurrentMap partitionSizes = new ConcurrentHashMap<>(); private final ConcurrentMap tenantRoutingInfoMap = new ConcurrentHashMap<>(); @@ -85,7 +83,7 @@ public class HashPartitionService implements PartitionService { TenantRoutingInfoService tenantRoutingInfoService, ApplicationEventPublisher applicationEventPublisher, TbQueueRuleEngineSettings tbQueueRuleEngineSettings, - QueueService queueService) { + TbQueueService queueService) { this.serviceInfoProvider = serviceInfoProvider; this.tenantRoutingInfoService = tenantRoutingInfoService; this.applicationEventPublisher = applicationEventPublisher; diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index 9a272b4739..35d4dc2899 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -82,7 +82,7 @@ import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.tenant.TenantDao; -import org.thingsboard.server.queue.QueueService; +import org.thingsboard.server.queue.TbQueueService; import java.util.Arrays; import java.util.Collections; @@ -115,7 +115,7 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D } @Autowired(required = false) - private QueueService queueService; + private TbQueueService queueService; @Autowired private DeviceProfileDao deviceProfileDao; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 8f692d5a12..cb209c2ee7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -128,6 +128,8 @@ public class ModelConstants { public static final String TENANT_PROFILE_IS_DEFAULT_PROPERTY = "is_default"; public static final String TENANT_PROFILE_ISOLATED_TB_CORE = "isolated_tb_core"; public static final String TENANT_PROFILE_ISOLATED_TB_RULE_ENGINE = "isolated_tb_rule_engine"; + public static final String TENANT_PROFILE_MAX_NUMBER_OF_QUEUES = "max_number_of_queues"; + public static final String TENANT_PROFILE_MAX_NUMBER_OF_PARTITIONS_PER_QUEUE = "max_number_of_partitions_per_queue"; /** * Cassandra customer constants. @@ -581,6 +583,20 @@ public class ModelConstants { public static final String DOUBLE_VALUE_COLUMN = "dbl_v"; public static final String JSON_VALUE_COLUMN = "json_v"; + /** + * Queue constants. + */ + + public static final String QUEUE_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; + public static final String QUEUE_NAME_PROPERTY = "name"; + public static final String QUEUE_TOPIC_PROPERTY = "topic"; + public static final String QUEUE_POLL_INTERVAL_PROPERTY = "poll_interval"; + public static final String QUEUE_PARTITIONS_PROPERTY = "partitions"; + public static final String QUEUE_PACK_PROCESSING_TIMEOUT_PROPERTY = "pack_processing_timeout"; + public static final String QUEUE_SUBMIT_STRATEGY_PROPERTY = "submit_strategy"; + public static final String QUEUE_PROCESSING_STRATEGY_PROPERTY = "processing_strategy"; + public static final String QUEUE_COLUMN_FAMILY_NAME = "queue"; + protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN}; protected static final String[] COUNT_AGGREGATION_COLUMNS = new String[]{count(LONG_VALUE_COLUMN), count(DOUBLE_VALUE_COLUMN), count(BOOLEAN_VALUE_COLUMN), count(STRING_VALUE_COLUMN), count(JSON_VALUE_COLUMN)}; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/QueueEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/QueueEntity.java new file mode 100644 index 0000000000..3791ab4ac3 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/QueueEntity.java @@ -0,0 +1,105 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.id.QueueId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.queue.ProcessingStrategy; +import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.common.data.queue.SubmitStrategy; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@TypeDef(name = "json", typeClass = JsonStringType.class) +@Table(name = ModelConstants.QUEUE_COLUMN_FAMILY_NAME) +public class QueueEntity extends BaseSqlEntity { + + private static final ObjectMapper mapper = new ObjectMapper(); + + @Column(name = ModelConstants.QUEUE_TENANT_ID_PROPERTY) + private UUID tenantId; + + @Column(name = ModelConstants.QUEUE_NAME_PROPERTY) + private String name; + + @Column(name = ModelConstants.QUEUE_TOPIC_PROPERTY) + private String topic; + @Column(name = ModelConstants.QUEUE_POLL_INTERVAL_PROPERTY) + private int pollInterval; + + @Column(name = ModelConstants.QUEUE_PARTITIONS_PROPERTY) + private int partitions; + + @Column(name = ModelConstants.QUEUE_PACK_PROCESSING_TIMEOUT_PROPERTY) + private long packProcessingTimeout; + + @Type(type = "json") + @Column(name = ModelConstants.QUEUE_SUBMIT_STRATEGY_PROPERTY) + private JsonNode submitStrategy; + + @Type(type = "json") + @Column(name = ModelConstants.QUEUE_PROCESSING_STRATEGY_PROPERTY) + private JsonNode processingStrategy; + + public QueueEntity() { + } + + public QueueEntity(Queue queue) { + if (queue.getId() != null) { + this.setId(queue.getId().getId()); + } + this.createdTime = queue.getCreatedTime(); + this.tenantId = DaoUtil.getId(queue.getTenantId()); + this.name = queue.getName(); + this.topic = queue.getTopic(); + this.pollInterval = queue.getPollInterval(); + this.partitions = queue.getPartitions(); + this.packProcessingTimeout = queue.getPackProcessingTimeout(); + this.submitStrategy = mapper.valueToTree(queue.getSubmitStrategy()); + this.processingStrategy = mapper.valueToTree(queue.getProcessingStrategy()); + } + + @Override + public Queue toData() { + Queue queue = new Queue(new QueueId(getUuid())); + queue.setCreatedTime(createdTime); + queue.setTenantId(new TenantId(tenantId)); + queue.setName(name); + queue.setTopic(topic); + queue.setPollInterval(pollInterval); + queue.setPartitions(partitions); + queue.setPackProcessingTimeout(packProcessingTimeout); + queue.setSubmitStrategy(mapper.convertValue(this.submitStrategy, SubmitStrategy.class)); + queue.setProcessingStrategy(mapper.convertValue(this.processingStrategy, ProcessingStrategy.class)); + return queue; + } +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java index 8613c7ced5..2b62588c76 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java @@ -21,13 +21,13 @@ import lombok.Data; import lombok.EqualsAndHashCode; import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.SearchTextEntity; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.dao.util.mapping.JsonBinaryType; import javax.persistence.Column; @@ -59,6 +59,12 @@ public final class TenantProfileEntity extends BaseSqlEntity impl @Column(name = ModelConstants.TENANT_PROFILE_ISOLATED_TB_RULE_ENGINE) private boolean isolatedTbRuleEngine; + @Column(name = ModelConstants.TENANT_PROFILE_MAX_NUMBER_OF_QUEUES) + private Integer maxNumberOfQueues; + + @Column(name = ModelConstants.TENANT_PROFILE_MAX_NUMBER_OF_PARTITIONS_PER_QUEUE) + private Integer maxNumberOfPartitionsPerQueue; + @Type(type = "jsonb") @Column(name = ModelConstants.TENANT_PROFILE_PROFILE_DATA_PROPERTY, columnDefinition = "jsonb") private JsonNode profileData; @@ -77,6 +83,8 @@ public final class TenantProfileEntity extends BaseSqlEntity impl this.isDefault = tenantProfile.isDefault(); this.isolatedTbCore = tenantProfile.isIsolatedTbCore(); this.isolatedTbRuleEngine = tenantProfile.isIsolatedTbRuleEngine(); + this.maxNumberOfPartitionsPerQueue = tenantProfile.getMaxNumberOfPartitionsPerQueue(); + this.maxNumberOfQueues = tenantProfile.getMaxNumberOfQueues(); this.profileData = JacksonUtil.convertValue(tenantProfile.getProfileData(), ObjectNode.class); } @@ -103,6 +111,8 @@ public final class TenantProfileEntity extends BaseSqlEntity impl tenantProfile.setDefault(isDefault); tenantProfile.setIsolatedTbCore(isolatedTbCore); tenantProfile.setIsolatedTbRuleEngine(isolatedTbRuleEngine); + tenantProfile.setMaxNumberOfPartitionsPerQueue(maxNumberOfPartitionsPerQueue); + tenantProfile.setMaxNumberOfQueues(maxNumberOfQueues); tenantProfile.setProfileData(JacksonUtil.convertValue(profileData, TenantProfileData.class)); return tenantProfile; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java new file mode 100644 index 0000000000..cad7f9cb48 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java @@ -0,0 +1,333 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.queue; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.QueueId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.queue.ProcessingStrategy; +import org.thingsboard.server.common.data.queue.ProcessingStrategyType; +import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.common.data.queue.SubmitStrategy; +import org.thingsboard.server.common.data.queue.SubmitStrategyType; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.service.PaginatedRemover; +import org.thingsboard.server.dao.service.Validator; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.dao.tenant.TenantDao; +import org.thingsboard.server.queue.TbQueueAdmin; + +import java.util.List; + +@Service +@Slf4j +@RequiredArgsConstructor +public class BaseQueueService extends AbstractEntityService implements QueueService { + + @Autowired + private QueueDao queueDao; + + @Autowired + private TenantDao tenantDao; + + @Autowired + private TbTenantProfileCache tenantProfileCache; + + @Autowired(required = false) + private TbQueueAdmin tbQueueAdmin; + +// @Autowired(required = false) +// private TbQueueClusterService queueClusterService; + +// @Autowired +// private QueueStatsService queueStatsService; + + @Override + @Transactional + public Queue saveQueue(Queue queue) { + log.trace("Executing createOrUpdateQueue [{}]", queue); + queueValidator.validate(queue, Queue::getTenantId); + Queue savedQueue; + if (queue.getId() == null) { + savedQueue = createQueue(queue); + } else { + savedQueue = updateQueue(queue); + } + +// if (queueClusterService != null) { +// queueClusterService.onQueueChange(savedQueue, null); +// } + return savedQueue; + } + + private Queue createQueue(Queue queue) { + Queue createdQueue = queueDao.save(queue.getTenantId(), queue); + if (tbQueueAdmin != null) { + for (int i = 0; i < queue.getPartitions(); i++) { + tbQueueAdmin.createTopicIfNotExists(new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName()); + } + } + return createdQueue; + } + + private Queue updateQueue(Queue queue) { + Queue oldQueue = queueDao.findById(queue.getTenantId(), queue.getUuidId()); + Queue updatedQueue = queueDao.save(queue.getTenantId(), queue); + + int oldPartitions = oldQueue.getPartitions(); + int currentPartitions = queue.getPartitions(); + + //TODO: 3.2 remove if partitions can't be deleted. +// if (currentPartitions != oldPartitions && tbQueueAdmin != null) { +// queueClusterService.onQueueDelete(queue, null); +// if (currentPartitions > oldPartitions) { +// log.info("Added [{}] new partitions to [{}] queue", currentPartitions - oldPartitions, queue.getName()); +// for (int i = oldPartitions; i < currentPartitions; i++) { +// tbQueueAdmin.createTopicIfNotExists(new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName()); +// } +// } else { +// log.info("Removed [{}] partitions from [{}] queue", oldPartitions - currentPartitions, queue.getName()); +// for (int i = currentPartitions; i < oldPartitions; i++) { +// tbQueueAdmin.deleteTopic(new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName()); +// } +// } +// } + + return updatedQueue; + } + + @Override + @Transactional + public void deleteQueue(TenantId tenantId, QueueId queueId) { + log.trace("Executing deleteQueue, queueId: [{}]", queueId); + Queue queue = findQueueById(tenantId, queueId); +// if (queueClusterService != null) { +// queueClusterService.onQueueDelete(queue, null); +// } +// queueStatsService.deleteQueueStatsByQueueId(tenantId, queueId); + boolean result = queueDao.removeById(tenantId, queueId.getId()); + if (result && tbQueueAdmin != null) { + for (int i = 0; i < queue.getPartitions(); i++) { + String fullTopicName = new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName(); + log.debug("Deleting queue [{}]", fullTopicName); + try { + tbQueueAdmin.deleteTopic(fullTopicName); + } catch (Exception e) { + log.error("Failed to delete queue [{}]", fullTopicName); + } + } + } + } + + @Override + public List findQueuesByTenantId(TenantId tenantId) { + log.trace("Executing findQueues, tenantId: [{}]", tenantId); + return queueDao.findAllByTenantId(getSystemOrIsolatedTenantId(tenantId)); + } + + @Override + public PageData findQueuesByTenantId(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findQueues pageLink [{}]", pageLink); + Validator.validatePageLink(pageLink); + return queueDao.findQueuesByTenantId(getSystemOrIsolatedTenantId(tenantId), pageLink); + } + + @Override + public List findAllMainQueues() { + log.trace("Executing findAllMainQueues"); + return queueDao.findAllMainQueues(); + } + + @Override + public List findAllQueues() { + log.trace("Executing findAllQueues"); + return queueDao.findAllQueues(); + } + + @Override + public Queue findQueueById(TenantId tenantId, QueueId queueId) { + log.trace("Executing findQueueById, queueId: [{}]", queueId); + return queueDao.findById(tenantId, queueId.getId()); + } + + @Override + public Queue findQueueByTenantIdAndName(TenantId tenantId, String queueName) { + log.trace("Executing findQueueByTenantIdAndName, tenantId: [{}] queueName: [{}]", tenantId, queueName); + return queueDao.findQueueByTenantIdAndName(getSystemOrIsolatedTenantId(tenantId), queueName); + } + + @Override + public void deleteQueuesByTenantId(TenantId tenantId) { + Validator.validateId(tenantId, "Incorrect tenant id for delete queues request."); + tenantQueuesRemover.removeEntities(tenantId, tenantId); + } + + @Override + @Transactional + public Queue createDefaultMainQueue(TenantProfile tenantProfile, TenantId tenantId) { + Queue mainQueue = new Queue(); + mainQueue.setTenantId(tenantId); + mainQueue.setName("Main"); + mainQueue.setTopic("tb_rule_engine.main"); + mainQueue.setPollInterval(25); + mainQueue.setPartitions(Math.max(tenantProfile.getMaxNumberOfPartitionsPerQueue(), 1)); + mainQueue.setPackProcessingTimeout(60000); + SubmitStrategy mainQueueSubmitStrategy = new SubmitStrategy(); + mainQueueSubmitStrategy.setType(SubmitStrategyType.BURST); + mainQueueSubmitStrategy.setBatchSize(1000); + mainQueue.setSubmitStrategy(mainQueueSubmitStrategy); + ProcessingStrategy mainQueueProcessingStrategy = new ProcessingStrategy(); + mainQueueProcessingStrategy.setType(ProcessingStrategyType.SKIP_ALL_FAILURES); + mainQueueProcessingStrategy.setRetries(3); + mainQueueProcessingStrategy.setFailurePercentage(0); + mainQueueProcessingStrategy.setPauseBetweenRetries(3); + mainQueueProcessingStrategy.setMaxPauseBetweenRetries(3); + mainQueue.setProcessingStrategy(mainQueueProcessingStrategy); + return saveQueue(mainQueue); + } + + private DataValidator queueValidator = + new DataValidator<>() { + + @Override + protected void validateCreate(TenantId tenantId, Queue queue) { + if (queueDao.findQueueByTenantIdAndTopic(tenantId, queue.getTopic()) != null) { + throw new DataValidationException(String.format("Queue with topic: %s already exists!", queue.getTopic())); + } + if (queueDao.findQueueByTenantIdAndName(tenantId, queue.getName()) != null) { + throw new DataValidationException(String.format("Queue with name: %s already exists!", queue.getName())); + } + } + + @Override + protected void validateUpdate(TenantId tenantId, Queue queue) { + Queue foundQueue = queueDao.findById(tenantId, queue.getUuidId()); + if (queueDao.findById(tenantId, queue.getUuidId()) == null) { + throw new DataValidationException(String.format("Queue with id: %s does not exists!", queue.getId())); + } + if (!foundQueue.getName().equals(queue.getName())) { + throw new DataValidationException("Queue name can't be changed!"); + } + if (!foundQueue.getTopic().equals(queue.getTopic())) { + throw new DataValidationException("Queue topic can't be changed!"); + } + } + + @Override + protected void validateDataImpl(TenantId tenantId, Queue queue) { + if (!tenantId.equals(TenantId.SYS_TENANT_ID)) { + TenantProfile tenantProfile = tenantProfileCache.get(tenantId); + + if (!tenantProfile.isIsolatedTbRuleEngine()) { + throw new DataValidationException("Tenant should be isolated!"); + } + + if (queue.getId() == null) { + List existingQueues = findQueuesByTenantId(tenantId); + if (existingQueues.size() >= tenantProfile.getMaxNumberOfQueues()) { + throw new DataValidationException("The limit for creating new queue has been exceeded!"); + } + } + + if (queue.getPartitions() > tenantProfile.getMaxNumberOfPartitionsPerQueue()) { + throw new DataValidationException(String.format("Queue partitions can't be more then %d", tenantProfile.getMaxNumberOfPartitionsPerQueue())); + } + } + + if (StringUtils.isEmpty(queue.getName())) { + throw new DataValidationException("Queue name should be specified!"); + } + if (StringUtils.isBlank(queue.getTopic())) { + throw new DataValidationException("Queue topic should be non empty and without spaces!"); + } + if (queue.getPollInterval() < 1) { + throw new DataValidationException("Queue poll interval should be more then 0!"); + } + if (queue.getPartitions() < 1) { + throw new DataValidationException("Queue partitions should be more then 0!"); + } + if (queue.getPackProcessingTimeout() < 1) { + throw new DataValidationException("Queue pack processing timeout should be more then 0!"); + } + + SubmitStrategy submitStrategy = queue.getSubmitStrategy(); + if (submitStrategy == null) { + throw new DataValidationException("Queue submit strategy can't be null!"); + } + if (submitStrategy.getType() == null) { + throw new DataValidationException("Queue submit strategy type can't be null!"); + } + if (submitStrategy.getType() == SubmitStrategyType.BATCH && submitStrategy.getBatchSize() < 1) { + throw new DataValidationException("Queue submit strategy batch size should be more then 0!"); + } + ProcessingStrategy processingStrategy = queue.getProcessingStrategy(); + if (processingStrategy == null) { + throw new DataValidationException("Queue processing strategy can't be null!"); + } + if (processingStrategy.getType() == null) { + throw new DataValidationException("Queue processing strategy type can't be null!"); + } + if (processingStrategy.getRetries() < 0) { + throw new DataValidationException("Queue processing strategy retries can't be less then 0!"); + } + if (processingStrategy.getFailurePercentage() < 0 || processingStrategy.getFailurePercentage() > 100) { + throw new DataValidationException("Queue processing strategy failure percentage should be in a range from 0 to 100!"); + } + if (processingStrategy.getPauseBetweenRetries() < 0) { + throw new DataValidationException("Queue processing strategy pause between retries can't be less then 0!"); + } + if (processingStrategy.getMaxPauseBetweenRetries() < processingStrategy.getPauseBetweenRetries()) { + throw new DataValidationException("Queue processing strategy MAX pause between retries can't be less then pause between retries!"); + } + } + }; + + private PaginatedRemover tenantQueuesRemover = + new PaginatedRemover<>() { + + @Override + protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { + return queueDao.findQueuesByTenantId(id, pageLink); + } + + @Override + protected void removeEntity(TenantId tenantId, Queue entity) { + deleteQueue(tenantId, entity.getId()); + } + }; + + private TenantId getSystemOrIsolatedTenantId(TenantId tenantId) { + if (!tenantId.equals(TenantId.SYS_TENANT_ID)) { + TenantProfile tenantProfile = tenantProfileCache.get(tenantId); + if (tenantProfile.isIsolatedTbRuleEngine()) { + return tenantId; + } + } + + return TenantId.SYS_TENANT_ID; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/QueueDao.java b/dao/src/main/java/org/thingsboard/server/dao/queue/QueueDao.java new file mode 100644 index 0000000000..5cc7a5d7ba --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/queue/QueueDao.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.queue; + +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.dao.Dao; + +import java.util.List; + +public interface QueueDao extends Dao { + Queue findQueueByTenantIdAndTopic(TenantId tenantId, String topic); + + Queue findQueueByTenantIdAndName(TenantId tenantId, String name); + + List findAllMainQueues(); + + List findAllQueues(); + + List findAllByTenantId(TenantId tenantId); + + PageData findQueuesByTenantId(TenantId tenantId, PageLink pageLink); +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueDao.java new file mode 100644 index 0000000000..4e544f35cd --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueDao.java @@ -0,0 +1,86 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.sql.queue; + +import com.google.common.collect.Lists; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.QueueEntity; +import org.thingsboard.server.dao.queue.QueueDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; + +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +@Slf4j +@Component +public class JpaQueueDao extends JpaAbstractDao implements QueueDao { + + @Autowired + private QueueRepository queueRepository; + + @Override + protected Class getEntityClass() { + return QueueEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return queueRepository; + } + + @Override + public Queue findQueueByTenantIdAndTopic(TenantId tenantId, String topic) { + return DaoUtil.getData(queueRepository.findByTenantIdAndTopic(tenantId.getId(), topic)); + } + + @Override + public Queue findQueueByTenantIdAndName(TenantId tenantId, String name) { + return DaoUtil.getData(queueRepository.findByTenantIdAndName(tenantId.getId(), name)); + } + + @Override + public List findAllByTenantId(TenantId tenantId) { + List entities = queueRepository.findByTenantId(tenantId.getId()); + return DaoUtil.convertDataList(entities); + } + + @Override + public List findAllMainQueues() { + List entities = Lists.newArrayList(queueRepository.findAllByName("Main")); + return DaoUtil.convertDataList(entities); + } + + @Override + public List findAllQueues() { + List entities = Lists.newArrayList(queueRepository.findAll()); + return DaoUtil.convertDataList(entities); + } + + @Override + public PageData findQueuesByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(queueRepository + .findByTenantId(tenantId.getId(), Objects.toString(pageLink.getTextSearch(), ""), DaoUtil.toPageable(pageLink))); + } +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/queue/QueueRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/QueueRepository.java new file mode 100644 index 0000000000..d531072a39 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/QueueRepository.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.sql.queue; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.thingsboard.server.dao.model.sql.QueueEntity; + +import java.util.List; +import java.util.UUID; + +public interface QueueRepository extends CrudRepository { + QueueEntity findByTenantIdAndTopic(UUID tenantId, String topic); + + QueueEntity findByTenantIdAndName(UUID tenantId, String name); + + List findByTenantId(UUID tenantId); + + @Query("SELECT q FROM QueueEntity q WHERE q.tenantId = :tenantId " + + "AND LOWER(q.name) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findByTenantId(@Param("tenantId") UUID tenantId, + @Param("textSearch") String textSearch, + Pageable pageable); + + List findAllByName(String name); +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 056f5987d8..2ae46df19a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -37,6 +37,7 @@ import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.ota.OtaPackageService; +import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rpc.RpcService; import org.thingsboard.server.dao.rule.RuleChainService; @@ -104,6 +105,9 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe @Autowired private RpcService rpcService; + @Autowired + private QueueService queueService; + @Override public Tenant findTenantById(TenantId tenantId) { log.trace("Executing findTenantById [{}]", tenantId); @@ -160,6 +164,7 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe resourceService.deleteResourcesByTenantId(tenantId); otaPackageService.deleteOtaPackagesByTenantId(tenantId); rpcService.deleteAllRpcByTenantId(tenantId); + queueService.deleteQueuesByTenantId(tenantId); tenantDao.removeById(tenantId, tenantId.getId()); deleteEntityRelations(tenantId, tenantId); } diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index cdae84fe15..5335ce6f5f 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -337,6 +337,8 @@ CREATE TABLE IF NOT EXISTS tenant_profile ( is_default boolean, isolated_tb_core boolean, isolated_tb_rule_engine boolean, + max_number_of_queues int , + max_number_of_partitions_per_queue int, CONSTRAINT tenant_profile_name_unq_key UNIQUE (name) ); @@ -632,6 +634,19 @@ CREATE TABLE IF NOT EXISTS rpc ( status varchar(255) NOT NULL ); +CREATE TABLE IF NOT EXISTS queue( + id uuid NOT NULL CONSTRAINT queue_pkey PRIMARY KEY, + created_time bigint NOT NULL, + tenant_id uuid, + name varchar(255), + topic varchar(255), + poll_interval int, + partitions int, + pack_processing_timeout bigint, + submit_strategy varchar(255), + processing_strategy varchar(255) +); + CREATE OR REPLACE PROCEDURE cleanup_events_by_ttl(IN ttl bigint, IN debug_ttl bigint, INOUT deleted bigint) LANGUAGE plpgsql AS $$ diff --git a/ui-ngx/src/app/app.component.ts b/ui-ngx/src/app/app.component.ts index 9f914dda59..0864e2e8ed 100644 --- a/ui-ngx/src/app/app.component.ts +++ b/ui-ngx/src/app/app.component.ts @@ -78,6 +78,17 @@ export class AppComponent implements OnInit { ) ); + this.matIconRegistry.addSvgIconLiteral( + 'queues-list', + this.domSanitizer.bypassSecurityTrustHtml( + '' + + '' + + '' + + '' + + '' + ) + ); + this.storageService.testLocalStorage(); this.setupTranslate(); diff --git a/ui-ngx/src/app/core/http/queue.service.ts b/ui-ngx/src/app/core/http/queue.service.ts index 5aae5fe561..dc98c15d39 100644 --- a/ui-ngx/src/app/core/http/queue.service.ts +++ b/ui-ngx/src/app/core/http/queue.service.ts @@ -18,7 +18,9 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; import { Observable } from 'rxjs'; -import { ServiceType } from '@shared/models/queue.models'; +import { QueueInfo, ServiceType } from '@shared/models/queue.models'; +import { PageLink } from '@shared/models/page/page-link'; +import { PageData } from '@shared/models/page/page-data'; @Injectable({ providedIn: 'root' @@ -29,8 +31,27 @@ export class QueueService { private http: HttpClient ) { } - public getTenantQueuesByServiceType(serviceType: ServiceType, config?: RequestConfig): Observable> { + public getTenantQueuesNamesByServiceType(serviceType: ServiceType, config?: RequestConfig): Observable> { return this.http.get>(`/api/tenant/queues?serviceType=${serviceType}`, defaultHttpOptionsFromConfig(config)); } + + public getQueueById(queueId: string): Observable { + return this.http.get(`/api/tenant/queues/${queueId}`); + } + + public getTenantQueuesByServiceType(pageLink: PageLink, + serviceType: ServiceType, + config?: RequestConfig): Observable> { + return this.http.get>(`/api/tenant/queues${pageLink.toQuery()}&serviceType=${serviceType}`, + defaultHttpOptionsFromConfig(config)); + } + + public saveQueue(queue: QueueInfo, serviceType: ServiceType, config?: RequestConfig): Observable { + return this.http.post(`/api/tenant/queues?serviceType=${serviceType}`, queue, defaultHttpOptionsFromConfig(config)); + } + + public deleteQueue(queueId: string) { + return this.http.delete(`/api/tenant/queues/${queueId}`); + } } diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index 4a52fc66f6..215693f318 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -108,7 +108,7 @@ export class MenuService { name: 'admin.system-settings', type: 'toggle', path: '/settings', - height: '240px', + height: '280px', icon: 'settings', pages: [ { @@ -152,7 +152,14 @@ export class MenuService { type: 'link', path: '/settings/resources-library', icon: 'folder' - } + }, + { + id: guid(), + name: 'admin.queues', + type: 'link', + path: '/settings/queues', + icon: 'swap_calls' + }, ] } ); @@ -220,7 +227,12 @@ export class MenuService { name: 'resource.resources-library', icon: 'folder', path: '/settings/resources-library' - } + }, + { + name: 'admin.queues', + icon: 'swap_calls', + path: '/settings/queues' + }, ] } ); diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html index e2eb776e5b..03ff1ff921 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html @@ -67,6 +67,36 @@
{{ 'tenant.isolated-tb-rule-engine' | translate }}
{{'tenant.isolated-tb-rule-engine-details' | translate}}
+ + tenant.max-number-of-queues + + + {{ 'tenant.max-number-of-queues-required' | translate }} + + + {{ 'tenant.max-number-of-queues-min-length' | translate }} + + + + tenant.max-number-of-partitions-per-queue + + + {{ 'tenant.max-number-of-partitions-per-queue-required' | translate }} + + + {{ 'tenant.max-number-of-partitions-per-queue-min-length' | translate }} + + { name: [entity ? entity.name : '', [Validators.required, Validators.maxLength(255)]], isolatedTbCore: [entity ? entity.isolatedTbCore : false, []], isolatedTbRuleEngine: [entity ? entity.isolatedTbRuleEngine : false, []], + maxNumberOfQueues: [entity ? entity.maxNumberOfQueues : 1, [Validators.required, Validators.min(1)]], + maxNumberOfPartitionsPerQueue: [entity ? entity.maxNumberOfPartitionsPerQueue : 1, [Validators.required, Validators.min(1)]], profileData: [entity && !this.isAdd ? entity.profileData : { configuration: createTenantProfileConfiguration(TenantProfileType.DEFAULT) } as TenantProfileData, []], @@ -74,12 +76,26 @@ export class TenantProfileComponent extends EntityComponent { this.entityForm.patchValue({name: entity.name}); this.entityForm.patchValue({isolatedTbCore: entity.isolatedTbCore}); this.entityForm.patchValue({isolatedTbRuleEngine: entity.isolatedTbRuleEngine}); + this.entityForm.patchValue({maxNumberOfQueues: entity.maxNumberOfQueues}); + this.entityForm.patchValue({maxNumberOfPartitionsPerQueue: entity.maxNumberOfPartitionsPerQueue}); this.entityForm.patchValue({profileData: !this.isAdd ? entity.profileData : { configuration: createTenantProfileConfiguration(TenantProfileType.DEFAULT) } as TenantProfileData}); this.entityForm.patchValue({description: entity.description}); } + showQueueParams(): boolean { + let isolatedTbRuleEngine: boolean = this.entityForm.get('isolatedTbRuleEngine').value; + if (isolatedTbRuleEngine) { + this.entityForm.controls['maxNumberOfQueues'].enable(); + this.entityForm.controls['maxNumberOfPartitionsPerQueue'].enable(); + } else { + this.entityForm.controls['maxNumberOfQueues'].disable(); + this.entityForm.controls['maxNumberOfPartitionsPerQueue'].disable(); + } + return isolatedTbRuleEngine; + } + updateFormState() { if (this.entityForm) { if (this.isEditValue) { @@ -87,6 +103,8 @@ export class TenantProfileComponent extends EntityComponent { if (!this.isAdd) { this.entityForm.get('isolatedTbCore').disable({emitEvent: false}); this.entityForm.get('isolatedTbRuleEngine').disable({emitEvent: false}); + this.entityForm.get('maxNumberOfQueues').disable({emitEvent: false}); + this.entityForm.get('maxNumberOfPartitionsPerQueue').disable({emitEvent: false}); } } else { this.entityForm.disable({emitEvent: false}); diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts index ddccbb4da4..3f8b26f105 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts @@ -32,6 +32,7 @@ import { ResourcesLibraryTableConfigResolver } from '@home/pages/admin/resource/ import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { BreadCrumbConfig } from '@shared/components/breadcrumb'; +import { QueuesTableConfigResolver } from '@home/pages/admin/queue/queues-table-config.resolver'; @Injectable() export class OAuth2LoginProcessingUrlResolver implements Resolve { @@ -183,6 +184,22 @@ const routes: Routes = [ } } ] + }, + { + path: 'queues', + component: EntitiesTableComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.SYS_ADMIN], + title: 'admin.queues', + breadcrumb: { + label: 'admin.queues', + icon: 'swap_calls' + } + }, + resolve: { + entitiesTableConfig: QueuesTableConfigResolver + } } ] } @@ -193,7 +210,8 @@ const routes: Routes = [ exports: [RouterModule], providers: [ OAuth2LoginProcessingUrlResolver, - ResourcesLibraryTableConfigResolver + ResourcesLibraryTableConfigResolver, + QueuesTableConfigResolver ] }) export class AdminRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts index 326b16f950..6f5e91bd61 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts @@ -28,6 +28,7 @@ import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component'; import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dialog.component'; import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component'; import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources-library.component'; +import { QueueComponent} from '@home/pages/admin/queue/queue.component'; @NgModule({ declarations: @@ -39,7 +40,8 @@ import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources- SecuritySettingsComponent, OAuth2SettingsComponent, HomeSettingsComponent, - ResourcesLibraryComponent + ResourcesLibraryComponent, + QueueComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.html b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.html new file mode 100644 index 0000000000..ae163d9ba7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.html @@ -0,0 +1,182 @@ + +
+ +
+ +
+
+
+ + admin.queue-name + + + {{ 'queue.name-required' | translate }} + + + + queue.poll-interval + + + {{ 'queue.poll-interval-required' | translate }} + + + {{ 'queue.poll-interval-min-value' | translate }} + + + + queue.partitions + + + {{ 'queue.partitions-required' | translate }} + + + {{ 'queue.partitions-min-value' | translate }} + + + + queue.processing-timeout + + + {{ 'queue.pack-processing-timeout-required' | translate }} + + + {{ 'queue.pack-processing-timeout-min-value' | translate }} + + + +
+ + + + + + queue.submit-strategy + {{panel1.expanded ? 'keyboard_arrow_up' : 'keyboard_arrow_down' }} + + +
+ + queue.submit-strategy + + + {{ strategy }} + + + + {{ 'queue.submit-strategy-type-required' | translate }} + + + + queue.batch-size + + + {{ 'queue.batch-size-required' | translate }} + + + {{ 'queue.batch-size-min-value' | translate }} + + +
+
+ + + + queue.processing-strategy + {{panel2.expanded ? 'keyboard_arrow_up' : 'keyboard_arrow_down' }} + + + +
+ + queue.processing-strategy + + + {{ strategy }} + + + + {{ 'queue.processing-strategy-type-required' | translate }} + + + + queue.retries + + + {{ 'queue.retries-required' | translate }} + + + {{ 'queue.retries-min-value' | translate }} + + + + queue.failure-percentage + + + {{ 'queue.failure-percentage-required' | translate }} + + + {{ 'queue.failure-percentage-min-value' | translate }} + + + {{ 'queue.failure-percentage-max-value' | translate }} + + + + queue.pause-between-retries + + + {{ 'queue.pause-between-retries-required' | translate }} + + + {{ 'queue.pause-between-retries-min-value' | translate }} + + + + queue.max-pause-between-retries + + + {{ 'queue.max-pause-between-retries-required' | translate }} + + + {{ 'queue.max-pause-between-retries-min-value' | translate }} + + +
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.scss b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.scss new file mode 100644 index 0000000000..852e391b28 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.scss @@ -0,0 +1,59 @@ +/** + * Copyright © 2016-2022 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. + */ +:host ::ng-deep { + + .mat-expansion-panel:not([class*='mat-elevation-z']) { + box-shadow: none; + } + + .mat-accordion-container { + margin-bottom: 16px; + } + + .mat-expansion-panel { + + &:hover { + box-shadow: 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 5px 5px -3px rgba(0, 0, 0, 0.2); + } + + &-header { + height: 56px; + border: 1px solid #f2f2f2; + + &-title { + align-items: center; + justify-content: space-between; + margin-right: 0; + } + } + + &.mat-expanded { + border: none; + box-shadow: 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 5px 5px -3px rgba(0, 0, 0, 0.2); + + .mat-expansion-panel { + + &-content { + margin-top: 20px; + } + + &-body { + padding-bottom: 10px; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.ts b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.ts new file mode 100644 index 0000000000..b694e21ddf --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.ts @@ -0,0 +1,154 @@ +/// +/// Copyright © 2016-2022 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 { ChangeDetectorRef, Component, Inject } from '@angular/core'; +import { EntityType } from '@shared/models/entity-type.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { EntityComponent } from '@home/components/entity/entity.component'; +import { QueueInfo, QueueProcessingStrategyTypes, QueueSubmitStrategyTypes } from '@shared/models/queue.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import set = Reflect.set; +import { distinctUntilChanged } from 'rxjs/operators'; + +@Component({ + selector: 'tb-queue', + templateUrl: './queue.component.html', + styleUrls: ['./queue.component.scss'] +}) +export class QueueComponent extends EntityComponent { + entityForm: FormGroup; + + entityType = EntityType; + submitStrategies: string[] = []; + processingStrategies: string[] = []; + + QueueSubmitStrategyTypes = QueueSubmitStrategyTypes; + hideBatchSize: boolean = false; + + constructor(protected store: Store, + protected translate: TranslateService, + @Inject('entity') protected entityValue: QueueInfo, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + protected cd: ChangeDetectorRef, + public fb: FormBuilder) { + super(store, fb, entityValue, entitiesTableConfigValue, cd); + this.submitStrategies = Object.values(QueueSubmitStrategyTypes); + this.processingStrategies = Object.values(QueueProcessingStrategyTypes); + } + + ngOnInit() { + super.ngOnInit(); + this.entityForm.get('submitStrategy').get('type').valueChanges.subscribe(() => { + this.submitStrategyTypeChanged(); + }); + } + + buildForm(entity: QueueInfo): FormGroup { + return this.fb.group( + { + name: [entity ? entity.name : '', [Validators.required]], + pollInterval: [ + entity && entity.pollInterval ? entity.pollInterval : 25, + [Validators.min(1), Validators.required] + ], + partitions: [ + entity && entity.partitions ? entity.partitions : 10, + [Validators.min(1), Validators.required] + ], + packProcessingTimeout: [ + entity && entity.packProcessingTimeout ? entity.packProcessingTimeout : 2000, + [Validators.min(1), Validators.required] + ], + submitStrategy: this.fb.group({ + type: [entity ? entity.submitStrategy?.type : null, [Validators.required]], + batchSize: [ + entity && entity.submitStrategy?.batchSize ? entity.submitStrategy?.batchSize : 1000, + [Validators.min(1), Validators.required] + ], + }), + processingStrategy: this.fb.group({ + type: [entity ? entity.processingStrategy?.type : null, [Validators.required]], + retries: [ + entity && entity.processingStrategy?.retries ? entity.processingStrategy?.retries : 3, + [Validators.min(0), Validators.required] + ], + failurePercentage: [ + entity && entity.processingStrategy?.failurePercentage ? entity.processingStrategy?.failurePercentage : 0, + [Validators.min(0), Validators.required, Validators.max(100)] + ], + pauseBetweenRetries: [ + entity && entity.processingStrategy?.pauseBetweenRetries ? entity.processingStrategy?.pauseBetweenRetries : 3, + [Validators.min(1), Validators.required] + ], + maxPauseBetweenRetries: [ + entity && entity.processingStrategy?.maxPauseBetweenRetries ? entity.processingStrategy?.maxPauseBetweenRetries : 3, + [Validators.min(1), Validators.required] + ], + }) + } + ); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + updateForm(entity: QueueInfo) { + this.entityForm.patchValue({ + name: entity.name, + pollInterval: entity.pollInterval, + partitions: entity.partitions, + packProcessingTimeout: entity.packProcessingTimeout, + submitStrategy: { + type: entity.submitStrategy?.type, + batchSize: entity.submitStrategy?.batchSize, + }, + processingStrategy: { + type: entity.processingStrategy?.type, + retries: entity.processingStrategy?.retries, + failurePercentage: entity.processingStrategy?.failurePercentage, + pauseBetweenRetries: entity.processingStrategy?.pauseBetweenRetries, + maxPauseBetweenRetries: entity.processingStrategy?.maxPauseBetweenRetries, + } + }, {emitEvent: true}); + + if (!this.isAdd) { + this.entityForm.get('name').disable({emitEvent: false}); + } + } + + submitStrategyTypeChanged() { + const form = this.entityForm.get("submitStrategy") as FormGroup; + const type: QueueSubmitStrategyTypes = form.get('type').value; + const batchSizeField = form.get('batchSize'); + if (type === QueueSubmitStrategyTypes.BATCH) { + batchSizeField.enable(); + batchSizeField.patchValue(1000); + this.hideBatchSize = true; + } else { + batchSizeField.patchValue(null); + batchSizeField.disable(); + this.hideBatchSize = false; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/queue/queues-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/admin/queue/queues-table-config.resolver.ts new file mode 100644 index 0000000000..aaaac2168f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/queue/queues-table-config.resolver.ts @@ -0,0 +1,115 @@ +/// +/// Copyright © 2016-2022 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 { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; +import { + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { QueueInfo, ServiceType } from '@shared/models/queue.models'; +import { select, Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { BroadcastService } from '@core/services/broadcast.service'; +import { CustomerService } from '@core/http/customer.service'; +import { DialogService } from '@core/services/dialog.service'; +import { HomeDialogsService } from '@home/dialogs/home-dialogs.service'; +import { map, mergeMap, take } from 'rxjs/operators'; +import { Observable } from 'rxjs'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@app/shared/models/entity-type.models'; +import { TranslateService } from '@ngx-translate/core'; +import { QueueComponent } from './queue.component'; +import { QueueService } from '@core/http/queue.service'; +import { selectAuthUser } from '@core/auth/auth.selectors'; + +@Injectable() +export class QueuesTableConfigResolver implements Resolve> { + + readonly queueType = ServiceType.TB_RULE_ENGINE; + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor(private store: Store, + private broadcast: BroadcastService, + private queueService: QueueService, + private customerService: CustomerService, + private dialogService: DialogService, + private homeDialogs: HomeDialogsService, + private translate: TranslateService) { + + this.config.entityType = EntityType.QUEUE; + this.config.entityComponent = QueueComponent; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.QUEUE); + this.config.entityResources = entityTypeResources.get(EntityType.QUEUE); + + this.config.deleteEntityTitle = queue => this.translate.instant('queue.delete-queue-title', {queueName: queue.name}); + this.config.deleteEntityContent = () => this.translate.instant('queue.delete-queue-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('queue.delete-queues-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('queue.delete-queues-text'); + } + + resolve(route: ActivatedRouteSnapshot): Observable> { + this.config.componentsData = { + queueType: this.queueType + }; + + return this.store.pipe(select(selectAuthUser), take(1)).pipe( + map(() => { + this.config.tableTitle = this.translate.instant('admin.queues'); + this.config.columns = this.configureColumns(); + this.configureEntityFunctions(); + return this.config; + }) + ); + } + + configureColumns(): Array> { + return [ + new EntityTableColumn('name', 'admin.queue-name', '25%'), + new EntityTableColumn('partitions', 'admin.queue-partitions', '25%'), + new EntityTableColumn('submitStrategy', 'admin.queue-submit-strategy', '25%', + (entity: QueueInfo) => { + return entity.submitStrategy.type; + }, + () => ({}), + false + ), + new EntityTableColumn('processingStrategy', 'admin.queue-processing-strategy', '25%', + (entity: QueueInfo) => { + return entity.processingStrategy.type; + }, + () => ({}), + false + ) + ]; + } + + configureEntityFunctions(): void { + this.config.entitiesFetchFunction = pageLink => this.queueService.getTenantQueuesByServiceType(pageLink, this.queueType); + this.config.loadEntity = id => this.queueService.getQueueById(id.id); + this.config.saveEntity = queue => this.queueService.saveQueue(this.addTopicForQueue(queue), this.queueType).pipe( + mergeMap((savedQueue) => this.queueService.getQueueById(savedQueue.id.id) + )); + this.config.deleteEntity = id => this.queueService.deleteQueue(id.id); + this.config.deleteEnabled = (queue) => queue && queue.name !== 'Main'; + } + + private addTopicForQueue(queue: QueueInfo): QueueInfo { + const modifiedQueue = Object.assign({}, queue); + modifiedQueue.topic = `tb_rule_engine.${queue.name}`; + return modifiedQueue; + } +} diff --git a/ui-ngx/src/app/shared/components/queue/queue-type-list.component.html b/ui-ngx/src/app/shared/components/queue/queue-type-list.component.html index e773c16743..6abd58dda9 100644 --- a/ui-ngx/src/app/shared/components/queue/queue-type-list.component.html +++ b/ui-ngx/src/app/shared/components/queue/queue-type-list.component.html @@ -17,7 +17,7 @@ --> {{ 'queue.name' | translate }} - - {{ 'queue.name_required' | translate }} + {{ 'queue.name-required' | translate }} device-profile.select-queue-hint diff --git a/ui-ngx/src/app/shared/components/queue/queue-type-list.component.ts b/ui-ngx/src/app/shared/components/queue/queue-type-list.component.ts index 14538c9fc7..28a3d94e9e 100644 --- a/ui-ngx/src/app/shared/components/queue/queue-type-list.component.ts +++ b/ui-ngx/src/app/shared/components/queue/queue-type-list.component.ts @@ -175,7 +175,7 @@ export class QueueTypeListComponent implements ControlValueAccessor, OnInit, Aft getQueues(): Observable> { if (!this.queues) { this.queues = this.queueService. - getTenantQueuesByServiceType(this.queueType, {ignoreLoading: true}).pipe( + getTenantQueuesNamesByServiceType(this.queueType, {ignoreLoading: true}).pipe( map((queues) => { return queues.map((queueName) => { return { queueName }; diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 4bfce6414a..29f6ccc020 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -133,7 +133,8 @@ export const HelpLinks = { widgetsConfigAlarm: helpBaseUrl + '/docs/user-guide/ui/dashboards#alarm', widgetsConfigStatic: helpBaseUrl + '/docs/user-guide/ui/dashboards#static', ruleNodePushToCloud: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#push-to-cloud', - ruleNodePushToEdge: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#push-to-edge' + ruleNodePushToEdge: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#push-to-edge', + queue: helpBaseUrl + '/docs/user-guide/queue' } }; diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 2604787b0e..9c97cdaa82 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -36,7 +36,8 @@ export enum EntityType { API_USAGE_STATE = 'API_USAGE_STATE', TB_RESOURCE = 'TB_RESOURCE', OTA_PACKAGE = 'OTA_PACKAGE', - RPC = 'RPC' + RPC = 'RPC', + QUEUE = 'QUEUE' } export enum AliasEntityType { @@ -306,6 +307,15 @@ export const entityTypeTranslations = new Map { + packProcessingTimeout: number; + partitions: number; + pollInterval: number; + processingStrategy: { + type: QueueProcessingStrategyTypes, + retries: number, + failurePercentage: number, + pauseBetweenRetries: number, + maxPauseBetweenRetries: number + }; + submitStrategy: { + type: QueueSubmitStrategyTypes, + batchSize: number, + }; + tenantId: TenantId; + topic: string; +} diff --git a/ui-ngx/src/app/shared/models/tenant.model.ts b/ui-ngx/src/app/shared/models/tenant.model.ts index 027c8cdce0..dc3c5200d3 100644 --- a/ui-ngx/src/app/shared/models/tenant.model.ts +++ b/ui-ngx/src/app/shared/models/tenant.model.ts @@ -106,6 +106,8 @@ export interface TenantProfile extends BaseData { default?: boolean; isolatedTbCore?: boolean; isolatedTbRuleEngine?: boolean; + maxNumberOfQueues?: number; + maxNumberOfPartitionsPerQueue?: number; profileData?: TenantProfileData; } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index d593fd0c08..75dbdf4f5f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -252,7 +252,14 @@ "platform-ios": "iOS", "all-platforms": "All platforms", "allowed-platforms": "Allowed platforms" - } + }, + "queue-select-name": "Select queue name", + "queue-name": "Name", + "queue-name-required": "Queue name is required!", + "queues": "Queues", + "queue-partitions": "Partitions", + "queue-submit-strategy": "Submit strategy", + "queue-processing-strategy": "Processing strategy" }, "alarm": { "alarm": "Alarm", @@ -2670,10 +2677,50 @@ "browser-time": "Browser Time" }, "queue": { - "select_name": "Select queue name", - "name": "Queue Name", - "name_required": "Queue name is required!", - "no-queues-matching": "No queues matching '{{queue}}' were found." + "select-name": "Select queue name", + "name": "Name", + "name-required": "Queue name is required!", + "topic-required": "Queue topic is required!", + "poll-interval-required": "Poll interval is required!", + "poll-interval-min-value": "Poll interval value can't be less then 1", + "partitions-required": "Partitions is required!", + "partitions-min-value": "Partitions value can't be less then 1", + "pack-processing-timeout-required": "Processing timeout is required", + "pack-processing-timeout-min-value": "Processing timeout value can't be less then 1", + "batch-size-required": "Batch size is required!", + "batch-size-min-value": "Batch size value can't be less then 1", + "retries-required": "Retries is required!", + "retries-min-value": "Retries value can't be negative", + "failure-percentage-required": "Failure percentage is required!", + "failure-percentage-min-value": "Failure percentage value can't be less then 0", + "failure-percentage-max-value": "Failure percentage value can't be more then 100", + "pause-between-retries-required": "Pause between retries is required!", + "pause-between-retries-min-value": "Pause between retries value can't be less then 1", + "max-pause-between-retries-required": "Max pause between retries is required!", + "max-pause-between-retries-min-value": "Max pause between retries value can't be less then 1", + "submit-strategy-type-required": "Submit strategy type is required!", + "processing-strategy-type-required": "Processing strategy type is required!", + "queues": "Queues", + "selected-queues": "{ count, plural, 1 {1 queue} other {# queues} } selected", + "delete-queue-title": "Are you sure you want to delete the queue '{{queueName}}'?", + "delete-queues-title": "Are you sure you want to delete { count, plural, 1 {1 queue} other {# queues} }?", + "delete-queue-text": "Be careful, after the confirmation the queue and all related data will become unrecoverable.", + "delete-queues-text": "After the confirmation all selected queues will be deleted and won't be accessible.", + "search": "Search queue", + "add" : "Add queue", + "details": "Queue details", + "topic": "Topic", + "submit-strategy": "Submit Strategy", + "processing-strategy": "Processing Strategy", + "poll-interval": "Poll interval", + "partitions": "Partitions", + "processing-timeout": "Processing timeout", + "batch-size": "Batch size", + "retries": "Retries (0 - unlimited)", + "failure-percentage": "Failure Percentage", + "pause-between-retries": "Pause between retries", + "max-pause-between-retries": "Maximal pause between retries", + "delete": "Delete queue" }, "tenant": { "tenant": "Tenant", @@ -2707,7 +2754,13 @@ "isolated-tb-core": "Processing in isolated ThingsBoard Core container", "isolated-tb-rule-engine": "Processing in isolated ThingsBoard Rule Engine container", "isolated-tb-core-details": "Requires separate microservice(s) per isolated Tenant", - "isolated-tb-rule-engine-details": "Requires separate microservice(s) per isolated Tenant" + "isolated-tb-rule-engine-details": "Requires separate microservice(s) per isolated Tenant", + "max-number-of-queues": "Max number of ThingsBoard Rule Engine queues", + "max-number-of-queues-required": "Max number of queues is required", + "max-number-of-queues-min-length": "Max number of queues can't be less then 1", + "max-number-of-partitions-per-queue": "Max number of partitions per ThingsBoard Rule Engine queue", + "max-number-of-partitions-per-queue-required": "Max number of partitions per queue is required", + "max-number-of-partitions-per-queue-min-length": "Max number of partitions per queue can't be less then 1" }, "tenant-profile": { "tenant-profile": "Tenant profile", From e2c9a5ffdf2c86f3267d752398a0e024113ebca7 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Thu, 10 Mar 2022 17:59:59 +0200 Subject: [PATCH 002/276] TOTP and SMS 2FA providers; 2FA set-up API --- application/pom.xml | 16 ++ .../controller/TwoFactorAuthController.java | 128 +++++++++++++ .../auth/mfa/TwoFactorAuthService.java | 179 ++++++++++++++++++ .../mfa/config/TwoFactorAuthSettings.java | 40 ++++ .../SmsTwoFactorAuthAccountConfig.java | 34 ++++ .../TotpTwoFactorAuthAccountConfig.java | 34 ++++ .../account/TwoFactorAuthAccountConfig.java | 37 ++++ .../SmsTwoFactorAuthProviderConfig.java | 36 ++++ .../TotpTwoFactorAuthProviderConfig.java | 34 ++++ .../provider/TwoFactorAuthProviderConfig.java | 37 ++++ .../mfa/provider/TwoFactorAuthProvider.java | 34 ++++ .../provider/TwoFactorAuthProviderType.java | 21 ++ .../impl/SmsTwoFactorAuthProvider.java | 110 +++++++++++ .../impl/TotpTwoFactorAuthProvider.java | 73 +++++++ .../common/util/TripleFunction.java | 21 ++ .../dao/service/ConstraintValidator.java | 2 +- pom.xml | 6 + .../rule/engine/api/util/TbNodeUtils.java | 8 + 18 files changed, 849 insertions(+), 1 deletion(-) create mode 100644 application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TwoFactorAuthAccountConfig.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TotpTwoFactorAuthProviderConfig.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TwoFactorAuthProviderConfig.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProviderType.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFactorAuthProvider.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFactorAuthProvider.java create mode 100644 common/util/src/main/java/org/thingsboard/common/util/TripleFunction.java diff --git a/application/pom.xml b/application/pom.xml index efbc35dc6c..303cd6013b 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -337,6 +337,22 @@ Java-WebSocket test + + org.jboss.aerogear + aerogear-otp-java + + + + com.google.zxing + core + 3.3.0 + + + com.google.zxing + javase + 3.3.0 + + diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java new file mode 100644 index 0000000000..ec07ae2058 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -0,0 +1,128 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.controller; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.client.j2se.MatrixToImageWriter; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.QRCodeWriter; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; +import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; +import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig; +import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; +import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.model.token.JwtTokenFactory; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class TwoFactorAuthController extends BaseController { + + private final TwoFactorAuthService twoFactorAuthService; + private final JwtTokenFactory tokenFactory; + + + @GetMapping("/2fa/account/config") + @PreAuthorize("isAuthenticated()") + public TwoFactorAuthAccountConfig getTwoFactorAuthAccountConfig() throws ThingsboardException { + SecurityUser user = getCurrentUser(); + + return twoFactorAuthService.getTwoFaAccountConfig(user.getTenantId(), user.getId()).orElse(null); + } + + @PostMapping("/2fa/account/config/generate") + @PreAuthorize("isAuthenticated()") + public TwoFactorAuthAccountConfig generateTwoFactorAuthAccountConfig(@RequestParam TwoFactorAuthProviderType providerType) throws ThingsboardException { + SecurityUser user = getCurrentUser(); + + return twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), providerType, + (provider, providerConfig) -> { + return provider.generateNewAccountConfig(user, providerConfig); + }); + } + + // temporary endpoint for testing purposes + @PostMapping("/2fa/account/config/generate/qr") + @PreAuthorize("isAuthenticated()") + public void generateTwoFactorAuthAccountConfigWithQr(@RequestParam TwoFactorAuthProviderType providerType, HttpServletResponse response) throws Exception { + TwoFactorAuthAccountConfig config = generateTwoFactorAuthAccountConfig(providerType); + if (providerType == TwoFactorAuthProviderType.TOTP) { + BitMatrix qr = new QRCodeWriter().encode(((TotpTwoFactorAuthAccountConfig) config).getAuthUrl(), BarcodeFormat.QR_CODE, 200, 200); + try (ServletOutputStream outputStream = response.getOutputStream()) { + MatrixToImageWriter.writeToStream(qr, "PNG", outputStream); + } + } + response.setHeader("body", JacksonUtil.toString(config)); + } + + @PostMapping("/2fa/account/config/submit") + @PreAuthorize("isAuthenticated()") + public void submitTwoFactorAuthAccountConfig(@RequestBody TwoFactorAuthAccountConfig accountConfig) throws ThingsboardException { + SecurityUser user = getCurrentUser(); + + twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), accountConfig.getProviderType(), + (provider, providerConfig) -> { + provider.prepareVerificationCode(user, providerConfig, accountConfig); + }); + } + + @PostMapping("/2fa/account/config") + @PreAuthorize("isAuthenticated()") + public void verifyAndSaveTwoFactorAuthAccountConfig(@RequestBody TwoFactorAuthAccountConfig accountConfig, + @RequestParam String verificationCode) throws ThingsboardException { + SecurityUser user = getCurrentUser(); + + boolean verificationSuccess = twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), accountConfig.getProviderType(), + (provider, providerConfig) -> { + return provider.checkVerificationCode(user, verificationCode, accountConfig); + }); + + if (verificationSuccess) { + twoFactorAuthService.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig); + } else { + throw new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.INVALID_ARGUMENTS); + } + } + + + @GetMapping("/2fa/settings") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public TwoFactorAuthSettings getTwoFactorAuthSettings() throws ThingsboardException { + return twoFactorAuthService.getTwoFaSettings(getTenantId()).orElse(null); + } + + @PostMapping("/2fa/settings") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public void saveTwoFactorAuthSettings(@RequestBody TwoFactorAuthSettings twoFactorAuthSettings) throws ThingsboardException { + twoFactorAuthService.saveTwoFaSettings(getTenantId(), twoFactorAuthSettings); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java new file mode 100644 index 0000000000..51d4af28fd --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java @@ -0,0 +1,179 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.security.auth.mfa; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.TripleFunction; +import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.service.ConstraintValidator; +import org.thingsboard.server.dao.settings.AdminSettingsService; +import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; +import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; +import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig; +import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProvider; +import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; + +import java.util.Collection; +import java.util.Collections; +import java.util.EnumMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; + +@Service +@RequiredArgsConstructor +public class TwoFactorAuthService { + + private final UserService userService; + private final AdminSettingsService adminSettingsService; + private final AttributesService attributesService; + private final Map> providers = new EnumMap<>(TwoFactorAuthProviderType.class); + + protected static final String TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY = "twoFaConfig"; + protected static final String TWO_FACTOR_AUTH_SETTINGS_KEY = "twoFaSettings"; + + + @Autowired + private void setProviders(Collection> providers) { + providers.forEach(provider -> { + this.providers.put(provider.getType(), provider); + }); + } + + private Optional> getTwoFaProvider(TwoFactorAuthProviderType providerType) { + return Optional.of((TwoFactorAuthProvider) providers.get(providerType)); + } + + private Optional getTwoFaProviderConfig(TenantId tenantId, TwoFactorAuthProviderType providerType) { + return getTwoFaSettings(tenantId) + .flatMap(twoFaSettings -> twoFaSettings.getProviderConfig(providerType)) + .map(providerConfig -> (C) providerConfig); + } + + + public R processByTwoFaProvider(TenantId tenantId, TwoFactorAuthProviderType providerType, BiFunction, TwoFactorAuthProviderConfig, R> function) throws ThingsboardException { + TwoFactorAuthProviderConfig providerConfig = getTwoFaProviderConfig(tenantId, providerType) + .orElseThrow(() -> new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS)); + TwoFactorAuthProvider provider = getTwoFaProvider(providerType) + .orElseThrow(() -> new ThingsboardException("2FA provider is not available", ThingsboardErrorCode.ITEM_NOT_FOUND)); + + return function.apply(provider, providerConfig); + } + + public void processByTwoFaProvider(TenantId tenantId, TwoFactorAuthProviderType providerType, BiConsumer, TwoFactorAuthProviderConfig> function) throws ThingsboardException { + processByTwoFaProvider(tenantId, providerType, (provider, providerConfig) -> { + function.accept(provider, providerConfig); + return null; + }); + } + + public R processByTwoFaProvider(TenantId tenantId, UserId userId, TripleFunction, TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig, R> function) throws ThingsboardException { + TwoFactorAuthAccountConfig accountConfig = getTwoFaAccountConfig(tenantId, userId) + .orElseThrow(() -> new ThingsboardException("2FA is not configured for user", ThingsboardErrorCode.BAD_REQUEST_PARAMS)); + + TwoFactorAuthProviderConfig providerConfig = getTwoFaProviderConfig(tenantId, accountConfig.getProviderType()) + .orElseThrow(() -> new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS)); + TwoFactorAuthProvider provider = getTwoFaProvider(accountConfig.getProviderType()) + .orElseThrow(() -> new ThingsboardException("2FA provider is not available", ThingsboardErrorCode.ITEM_NOT_FOUND)); + + return function.apply(provider, providerConfig, accountConfig); + } + + + public Optional getTwoFaAccountConfig(TenantId tenantId, UserId userId) { + User user = userService.findUserById(tenantId, userId); + return Optional.ofNullable(user.getAdditionalInfo()) + .flatMap(additionalInfo -> Optional.ofNullable(additionalInfo.get(TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY)).filter(jsonNode -> !jsonNode.isNull())) + .map(jsonNode -> JacksonUtil.treeToValue(jsonNode, TwoFactorAuthAccountConfig.class)) + .filter(twoFactorAuthAccountConfig -> { + return getTwoFaProviderConfig(tenantId, twoFactorAuthAccountConfig.getProviderType()).isPresent(); + }); + } + + public void saveTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFactorAuthAccountConfig accountConfig) throws ThingsboardException { + ConstraintValidator.validateFields(accountConfig); + getTwoFaProviderConfig(tenantId, accountConfig.getProviderType()) + .orElseThrow(() -> new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS)); + + User user = userService.findUserById(tenantId, userId); + ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(user.getAdditionalInfo()) + .orElseGet(JacksonUtil::newObjectNode); + additionalInfo.set(TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY, JacksonUtil.valueToTree(accountConfig)); + user.setAdditionalInfo(additionalInfo); + + userService.saveUser(user); + } + + public void deleteTwoFaAccountConfig(TenantId tenantId, UserId userId) { + User user = userService.findUserById(tenantId, userId); + ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(user.getAdditionalInfo()) + .orElseGet(JacksonUtil::newObjectNode); + additionalInfo.remove(TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY); + user.setAdditionalInfo(additionalInfo); + + userService.saveUser(user); + } + + + @SneakyThrows + public Optional getTwoFaSettings(TenantId tenantId) { + if (tenantId.equals(TenantId.SYS_TENANT_ID)) { + return Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY)) + .map(adminSettings -> JacksonUtil.treeToValue(adminSettings.getJsonValue(), TwoFactorAuthSettings.class)); + } else { + return attributesService.find(TenantId.SYS_TENANT_ID, tenantId, DataConstants.SERVER_SCOPE, TWO_FACTOR_AUTH_SETTINGS_KEY).get() + .map(adminSettingsAttribute -> JacksonUtil.fromString(adminSettingsAttribute.getJsonValue().get(), TwoFactorAuthSettings.class)) + .filter(tenantTwoFactorAuthSettings -> !tenantTwoFactorAuthSettings.isUseSystemTwoFactorAuthSettings()) + .or(() -> getTwoFaSettings(TenantId.SYS_TENANT_ID)); + } + } + + @SneakyThrows + public void saveTwoFaSettings(TenantId tenantId, TwoFactorAuthSettings twoFactorAuthSettings) { + ConstraintValidator.validateFields(twoFactorAuthSettings); + if (tenantId.equals(TenantId.SYS_TENANT_ID)) { + AdminSettings settings = Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY)) + .orElseGet(() -> { + AdminSettings newSettings = new AdminSettings(); + newSettings.setKey(TWO_FACTOR_AUTH_SETTINGS_KEY); + return newSettings; + }); + settings.setJsonValue(JacksonUtil.valueToTree(twoFactorAuthSettings)); + adminSettingsService.saveAdminSettings(tenantId, settings); + } else { + attributesService.save(TenantId.SYS_TENANT_ID, tenantId, DataConstants.SERVER_SCOPE, Collections.singletonList( + new BaseAttributeKvEntry(new JsonDataEntry(TWO_FACTOR_AUTH_SETTINGS_KEY, JacksonUtil.toString(twoFactorAuthSettings)), System.currentTimeMillis()) + )).get(); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java new file mode 100644 index 0000000000..a0620ec96e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.security.auth.mfa.config; + +import lombok.Data; +import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig; +import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; + +import javax.validation.Valid; +import java.util.List; +import java.util.Optional; + +@Data +public class TwoFactorAuthSettings { + + private boolean useSystemTwoFactorAuthSettings; + @Valid + private List providers; + + public Optional getProviderConfig(TwoFactorAuthProviderType providerType) { + return Optional.ofNullable(providers) + .flatMap(providersConfigs -> providersConfigs.stream() + .filter(providerConfig -> providerConfig.getProviderType() == providerType) + .findFirst()); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java new file mode 100644 index 0000000000..6f3ef06775 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.security.auth.mfa.config.account; + +import lombok.Data; +import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; + +import javax.validation.constraints.NotBlank; + +@Data +public class SmsTwoFactorAuthAccountConfig implements TwoFactorAuthAccountConfig { + + @NotBlank + private String phoneNumber; + + @Override + public TwoFactorAuthProviderType getProviderType() { + return TwoFactorAuthProviderType.SMS; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java new file mode 100644 index 0000000000..a48cf162a0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.security.auth.mfa.config.account; + +import lombok.Data; +import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; + +import javax.validation.constraints.NotBlank; + +@Data +public class TotpTwoFactorAuthAccountConfig implements TwoFactorAuthAccountConfig { + + @NotBlank + private String authUrl; + + @Override + public TwoFactorAuthProviderType getProviderType() { + return TwoFactorAuthProviderType.TOTP; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TwoFactorAuthAccountConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TwoFactorAuthAccountConfig.java new file mode 100644 index 0000000000..141535eea8 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TwoFactorAuthAccountConfig.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.security.auth.mfa.config.account; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + property = "providerType") +@JsonSubTypes({ + @JsonSubTypes.Type(value = TotpTwoFactorAuthAccountConfig.class, name = "TOTP"), + @JsonSubTypes.Type(value = SmsTwoFactorAuthAccountConfig.class, name = "SMS"), +}) +public interface TwoFactorAuthAccountConfig { + + @JsonIgnore + TwoFactorAuthProviderType getProviderType(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java new file mode 100644 index 0000000000..a036e47574 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.security.auth.mfa.config.provider; + +import lombok.Data; +import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +@Data +public class SmsTwoFactorAuthProviderConfig implements TwoFactorAuthProviderConfig { + + @NotBlank + @Pattern(regexp = ".*\\$\\{verificationCode}.*", message = "Template must contain verification code") + private String smsVerificationMessageTemplate; + + @Override + public TwoFactorAuthProviderType getProviderType() { + return TwoFactorAuthProviderType.SMS; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TotpTwoFactorAuthProviderConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TotpTwoFactorAuthProviderConfig.java new file mode 100644 index 0000000000..2d6cc5ddf5 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TotpTwoFactorAuthProviderConfig.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.security.auth.mfa.config.provider; + +import lombok.Data; +import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; + +import javax.validation.constraints.NotBlank; + +@Data +public class TotpTwoFactorAuthProviderConfig implements TwoFactorAuthProviderConfig { + + @NotBlank(message = "Issuer name must not be blank") + private String issuerName; + + @Override + public TwoFactorAuthProviderType getProviderType() { + return TwoFactorAuthProviderType.TOTP; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TwoFactorAuthProviderConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TwoFactorAuthProviderConfig.java new file mode 100644 index 0000000000..a86bcee222 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TwoFactorAuthProviderConfig.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.security.auth.mfa.config.provider; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + property = "providerType") +@JsonSubTypes({ + @JsonSubTypes.Type(value = TotpTwoFactorAuthProviderConfig.class, name = "TOTP"), + @JsonSubTypes.Type(value = SmsTwoFactorAuthProviderConfig.class, name = "SMS"), +}) +public interface TwoFactorAuthProviderConfig { + + @JsonIgnore + TwoFactorAuthProviderType getProviderType(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java new file mode 100644 index 0000000000..82122a9163 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.security.auth.mfa.provider; + +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; +import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig; +import org.thingsboard.server.service.security.model.SecurityUser; + +public interface TwoFactorAuthProvider { + + A generateNewAccountConfig(User user, C providerConfig); + + default void prepareVerificationCode(SecurityUser user, C providerConfig, A accountConfig) {} + + boolean checkVerificationCode(SecurityUser user, String verificationCode, A accountConfig); + + + TwoFactorAuthProviderType getType(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProviderType.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProviderType.java new file mode 100644 index 0000000000..9a4a3672a7 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProviderType.java @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.security.auth.mfa.provider; + +public enum TwoFactorAuthProviderType { + TOTP, + SMS +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFactorAuthProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFactorAuthProvider.java new file mode 100644 index 0000000000..7d4a0b9ab7 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFactorAuthProvider.java @@ -0,0 +1,110 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.security.auth.mfa.provider.impl; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.stereotype.Service; +import org.thingsboard.rule.engine.api.SmsService; +import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.dao.service.ConstraintValidator; +import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.auth.mfa.config.account.SmsTwoFactorAuthAccountConfig; +import org.thingsboard.server.service.security.auth.mfa.config.provider.SmsTwoFactorAuthProviderConfig; +import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProvider; +import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; +import org.thingsboard.server.service.security.model.SecurityUser; + +import java.util.Collections; +import java.util.Map; + +@Service +@RequiredArgsConstructor +@TbCoreComponent +public class SmsTwoFactorAuthProvider implements TwoFactorAuthProvider { + + private final SmsService smsService; + private final TimeseriesService timeseriesService; + + @Override + public SmsTwoFactorAuthAccountConfig generateNewAccountConfig(User user, SmsTwoFactorAuthProviderConfig providerConfig) { + return new SmsTwoFactorAuthAccountConfig(); + } + + @Override + @SneakyThrows // fixme + public void prepareVerificationCode(SecurityUser user, SmsTwoFactorAuthProviderConfig providerConfig, SmsTwoFactorAuthAccountConfig accountConfig) { + ConstraintValidator.validateFields(accountConfig); + + String verificationCode = RandomStringUtils.randomNumeric(6); + saveVerificationCode(user, verificationCode); + + String phoneNumber = accountConfig.getPhoneNumber(); + + Map data = Map.of( + "verificationCode", verificationCode, + "userEmail", user.getEmail() + ); + String message = TbNodeUtils.processTemplate(providerConfig.getSmsVerificationMessageTemplate(), data); + + smsService.sendSms(user.getTenantId(), user.getCustomerId(), new String[]{phoneNumber}, message); + } + + @Override + public boolean checkVerificationCode(SecurityUser user, String verificationCode, SmsTwoFactorAuthAccountConfig accountConfig) { + if (verificationCode.equals(getVerificationCode(user))) { + removeVerificationCode(user); + return true; + } else { + return false; + } + } + + + @SneakyThrows + private void saveVerificationCode(SecurityUser user, String verificationCode) { + timeseriesService.save(user.getTenantId(), user.getId(), + new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry("twoFaVerificationCode:" + user.getSessionId(), verificationCode)) + ).get(); + } + + @SneakyThrows + private String getVerificationCode(SecurityUser user) { + return timeseriesService.findLatest(user.getTenantId(), user.getId(), + Collections.singletonList("twoFaVerificationCode:" + user.getSessionId())).get().stream().findFirst() + .map(codeTs -> codeTs.getStrValue().get()) + .orElse(null); + } + + private void removeVerificationCode(SecurityUser user) { + timeseriesService.remove(user.getTenantId(), user.getId(), Collections.singletonList( + new BaseDeleteTsKvQuery("twoFaVerificationCode:" + user.getSessionId(), 0, System.currentTimeMillis()) + )); + } + + + @Override + public TwoFactorAuthProviderType getType() { + return TwoFactorAuthProviderType.SMS; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFactorAuthProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFactorAuthProvider.java new file mode 100644 index 0000000000..d7747521c1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFactorAuthProvider.java @@ -0,0 +1,73 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.security.auth.mfa.provider.impl; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.apache.commons.lang3.RandomUtils; +import org.apache.http.client.utils.URIBuilder; +import org.jboss.aerogear.security.otp.Totp; +import org.jboss.aerogear.security.otp.api.Base32; +import org.springframework.stereotype.Service; +import org.springframework.web.util.UriComponentsBuilder; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig; +import org.thingsboard.server.service.security.auth.mfa.config.provider.TotpTwoFactorAuthProviderConfig; +import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProvider; +import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; +import org.thingsboard.server.service.security.model.SecurityUser; + +@Service +@RequiredArgsConstructor +@TbCoreComponent +public class TotpTwoFactorAuthProvider implements TwoFactorAuthProvider { + + @Override + public final TotpTwoFactorAuthAccountConfig generateNewAccountConfig(User user, TotpTwoFactorAuthProviderConfig providerConfig) { + TotpTwoFactorAuthAccountConfig config = new TotpTwoFactorAuthAccountConfig(); + String secretKey = generateSecretKey(); + config.setAuthUrl(getTotpAuthUrl(user, secretKey, providerConfig)); + return config; + } + + @Override + public final boolean checkVerificationCode(SecurityUser user, String verificationCode, TotpTwoFactorAuthAccountConfig accountConfig) { + String secretKey = UriComponentsBuilder.fromUriString(accountConfig.getAuthUrl()).build().getQueryParams().getFirst("secret"); + return new Totp(secretKey).verify(verificationCode); + } + + @SneakyThrows + private String getTotpAuthUrl(User user, String secretKey, TotpTwoFactorAuthProviderConfig providerConfig) { + URIBuilder uri = new URIBuilder() + .setScheme("otpauth") + .setHost("totp") + .setParameter("issuer", providerConfig.getIssuerName()) + .setPath("/" + providerConfig.getIssuerName() + ":" + user.getEmail()) + .setParameter("secret", secretKey); + return uri.build().toASCIIString(); + } + + private String generateSecretKey() { + return Base32.encode(RandomUtils.nextBytes(20)); + } + + @Override + public TwoFactorAuthProviderType getType() { + return TwoFactorAuthProviderType.TOTP; + } + +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/TripleFunction.java b/common/util/src/main/java/org/thingsboard/common/util/TripleFunction.java new file mode 100644 index 0000000000..5134703cd3 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/TripleFunction.java @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.common.util; + +@FunctionalInterface +public interface TripleFunction { + R apply(A a, B b, C c); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java index 0e8b71abee..da7537ae75 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java @@ -46,7 +46,7 @@ public class ConstraintValidator { .distinct() .collect(Collectors.toList()); if (!validationErrors.isEmpty()) { - throw new ValidationException("Validation error: " + String.join(", ", validationErrors)); + throw new ValidationException(String.join(", ", validationErrors)); } } diff --git a/pom.xml b/pom.xml index 295e01f2d3..d339c644c9 100755 --- a/pom.xml +++ b/pom.xml @@ -132,6 +132,7 @@ 1.16.0 1.12 + 1.0.0 @@ -1872,6 +1873,11 @@ ${zeroturnaround.version} test + + org.jboss.aerogear + aerogear-otp-java + ${aerogear-otp.version} + diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java index 9b0f0e9f0a..32ee585820 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java @@ -101,6 +101,14 @@ public class TbNodeUtils { return result; } + public static String processTemplate(String template, Map data) { + String result = template; + for (Map.Entry kv : data.entrySet()) { + result = processVar(result, kv.getKey(), kv.getValue()); + } + return result; + } + private static String processVar(String pattern, String key, String val) { return pattern.replace(formatMetadataVarTemplate(key), val); } From 1ad769048cee05f287f1efd7efcea39a74a8bfbe Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Thu, 10 Mar 2022 18:15:42 +0200 Subject: [PATCH 003/276] 2FA support for platform authentication --- .../server/config/JwtSettings.java | 42 ++---- .../controller/TwoFactorAuthController.java | 19 +++ .../RefreshTokenAuthenticationProvider.java | 1 + ...RestAwareAuthenticationSuccessHandler.java | 46 ++++--- .../service/security/model/JwtTokenPair.java | 2 + .../service/security/model/SecurityUser.java | 11 ++ .../security/model/token/AccessJwtToken.java | 12 +- .../security/model/token/JwtTokenFactory.java | 128 +++++++++++------- .../src/main/resources/thingsboard.yml | 5 + .../common/data/security/Authority.java | 5 +- 10 files changed, 157 insertions(+), 114 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/config/JwtSettings.java b/application/src/main/java/org/thingsboard/server/config/JwtSettings.java index 31dbfc68e8..1c15c4c150 100644 --- a/application/src/main/java/org/thingsboard/server/config/JwtSettings.java +++ b/application/src/main/java/org/thingsboard/server/config/JwtSettings.java @@ -15,12 +15,14 @@ */ package org.thingsboard.server.config; +import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.security.model.JwtToken; -@Configuration +@Component @ConfigurationProperties(prefix = "security.jwt") +@Data public class JwtSettings { /** * {@link JwtToken} will expire after this time. @@ -42,34 +44,10 @@ public class JwtSettings { */ private Integer refreshTokenExpTime; - public Integer getRefreshTokenExpTime() { - return refreshTokenExpTime; - } - - public void setRefreshTokenExpTime(Integer refreshTokenExpTime) { - this.refreshTokenExpTime = refreshTokenExpTime; - } - - public Integer getTokenExpirationTime() { - return tokenExpirationTime; - } - - public void setTokenExpirationTime(Integer tokenExpirationTime) { - this.tokenExpirationTime = tokenExpirationTime; - } - - public String getTokenIssuer() { - return tokenIssuer; - } - public void setTokenIssuer(String tokenIssuer) { - this.tokenIssuer = tokenIssuer; - } - - public String getTokenSigningKey() { - return tokenSigningKey; - } + /** + * Issued when 2FA is being used. + * Valid only for 2FA verification code checking. + * */ + private Integer preVerificationTokenExpirationTime; - public void setTokenSigningKey(String tokenSigningKey) { - this.tokenSigningKey = tokenSigningKey; - } -} \ No newline at end of file +} diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java index ec07ae2058..1355337127 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -35,6 +35,7 @@ import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSett import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig; import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; +import org.thingsboard.server.service.security.model.JwtTokenPair; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; @@ -125,4 +126,22 @@ public class TwoFactorAuthController extends BaseController { twoFactorAuthService.saveTwoFaSettings(getTenantId(), twoFactorAuthSettings); } + + @PostMapping("/auth/2fa/verification/check") + @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") + public JwtTokenPair checkTwoFaVerificationCode(@RequestParam String verificationCode) throws ThingsboardException { + SecurityUser user = getCurrentUser(); + + boolean verificationSuccess = twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), user.getId(), + (provider, providerConfig, accountConfig) -> { + return provider.checkVerificationCode(user, verificationCode, accountConfig); + }); + + if (verificationSuccess) { + return tokenFactory.createTokenPair(user); + } else { + throw new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.AUTHENTICATION); + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java index 8003cfd012..b744adcdaf 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java @@ -66,6 +66,7 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide } else { securityUser = authenticateByPublicId(principal.getValue()); } + securityUser.setSessionId(unsafeUser.getSessionId()); if (tokenOutdatingService.isOutdated(rawAccessToken, securityUser.getId())) { throw new CredentialsExpiredException("Token is outdated"); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java index 057577fdca..ca2f15953a 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java @@ -16,15 +16,18 @@ package org.thingsboard.server.service.security.auth.rest; import com.fasterxml.jackson.databind.ObjectMapper; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; import org.springframework.security.web.WebAttributes; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.security.model.JwtToken; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; +import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; +import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; +import org.thingsboard.server.service.security.model.JwtTokenPair; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; @@ -33,37 +36,44 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; -import java.util.HashMap; -import java.util.Map; +import java.util.Optional; @Component(value = "defaultAuthenticationSuccessHandler") +@RequiredArgsConstructor +@Slf4j public class RestAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private final ObjectMapper mapper; private final JwtTokenFactory tokenFactory; - private final RefreshTokenRepository refreshTokenRepository; - - @Autowired - public RestAwareAuthenticationSuccessHandler(final ObjectMapper mapper, final JwtTokenFactory tokenFactory, final RefreshTokenRepository refreshTokenRepository) { - this.mapper = mapper; - this.tokenFactory = tokenFactory; - this.refreshTokenRepository = refreshTokenRepository; - } + private final TwoFactorAuthService twoFactorAuthService; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { SecurityUser securityUser = (SecurityUser) authentication.getPrincipal(); - JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); - JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); + JwtTokenPair tokenPair; - Map tokenMap = new HashMap(); - tokenMap.put("token", accessToken.getToken()); - tokenMap.put("refreshToken", refreshToken.getToken()); + Optional twoFaAccountConfig = twoFactorAuthService.getTwoFaAccountConfig(securityUser.getTenantId(), securityUser.getId()); + if (twoFaAccountConfig.isPresent()) { + try { + twoFactorAuthService.processByTwoFaProvider(securityUser.getTenantId(), twoFaAccountConfig.get().getProviderType(), + (provider, providerConfig) -> { + provider.prepareVerificationCode(securityUser, providerConfig, twoFaAccountConfig.get()); + }); + tokenPair = new JwtTokenPair(); + tokenPair.setToken(tokenFactory.createPreVerificationToken(securityUser).getToken()); + } catch (Exception e) { + log.error("Failed to process 2FA for user {}. Falling back to plain auth", securityUser.getId(), e); + tokenPair = tokenFactory.createTokenPair(securityUser); + } + } else { + tokenPair = tokenFactory.createTokenPair(securityUser); + } response.setStatus(HttpStatus.OK.value()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); - mapper.writeValue(response.getWriter(), tokenMap); + + mapper.writeValue(response.getWriter(), tokenPair); clearAuthenticationAttributes(request); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/JwtTokenPair.java b/application/src/main/java/org/thingsboard/server/service/security/model/JwtTokenPair.java index 7a9c339fc2..57964570c1 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/JwtTokenPair.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/JwtTokenPair.java @@ -19,10 +19,12 @@ import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; @ApiModel(value = "JWT Token Pair") @Data @AllArgsConstructor +@NoArgsConstructor public class JwtTokenPair { @ApiModelProperty(position = 1, value = "The JWT Access Token. Used to perform API calls.", example = "AAB254FF67D..") diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java b/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java index 380d6537f2..3698282489 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.UserId; import java.util.Collection; +import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -31,6 +32,7 @@ public class SecurityUser extends User { private Collection authorities; private boolean enabled; private UserPrincipal userPrincipal; + private String sessionId; public SecurityUser() { super(); @@ -44,6 +46,7 @@ public class SecurityUser extends User { super(user); this.enabled = enabled; this.userPrincipal = userPrincipal; + this.sessionId = UUID.randomUUID().toString(); } public Collection getAuthorities() { @@ -71,4 +74,12 @@ public class SecurityUser extends User { this.userPrincipal = userPrincipal; } + public String getSessionId() { + return sessionId; + } + + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/AccessJwtToken.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/AccessJwtToken.java index 639bd2a347..f96e0bfc33 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/AccessJwtToken.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/AccessJwtToken.java @@ -15,25 +15,17 @@ */ package org.thingsboard.server.service.security.model.token; -import com.fasterxml.jackson.annotation.JsonIgnore; -import io.jsonwebtoken.Claims; import org.thingsboard.server.common.data.security.model.JwtToken; public final class AccessJwtToken implements JwtToken { private final String rawToken; - @JsonIgnore - private transient Claims claims; - protected AccessJwtToken(final String token, Claims claims) { - this.rawToken = token; - this.claims = claims; + public AccessJwtToken(String rawToken) { + this.rawToken = rawToken; } public String getToken() { return this.rawToken; } - public Claims getClaims() { - return claims; - } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index 24e49f9db0..b2bc3add26 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.security.model.token; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.SignatureAlgorithm; @@ -36,6 +37,7 @@ import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.model.JwtToken; import org.thingsboard.server.config.JwtSettings; import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; +import org.thingsboard.server.service.security.model.JwtTokenPair; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; @@ -58,6 +60,7 @@ public class JwtTokenFactory { private static final String IS_PUBLIC = "isPublic"; private static final String TENANT_ID = "tenantId"; private static final String CUSTOMER_ID = "customerId"; + private static final String SESSION_ID = "sessionId"; private final JwtSettings settings; @@ -70,39 +73,28 @@ public class JwtTokenFactory { * Factory method for issuing new JWT Tokens. */ public AccessJwtToken createAccessJwtToken(SecurityUser securityUser) { - if (StringUtils.isBlank(securityUser.getEmail())) - throw new IllegalArgumentException("Cannot create JWT Token without username/email"); - - if (securityUser.getAuthority() == null) + if (securityUser.getAuthority() == null) { throw new IllegalArgumentException("User doesn't have any privileges"); + } UserPrincipal principal = securityUser.getUserPrincipal(); - String subject = principal.getValue(); - Claims claims = Jwts.claims().setSubject(subject); - claims.put(SCOPES, securityUser.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList())); - claims.put(USER_ID, securityUser.getId().getId().toString()); - claims.put(FIRST_NAME, securityUser.getFirstName()); - claims.put(LAST_NAME, securityUser.getLastName()); - claims.put(ENABLED, securityUser.isEnabled()); - claims.put(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID); + + JwtBuilder jwtBuilder = setUpToken(securityUser, securityUser.getAuthorities().stream() + .map(GrantedAuthority::getAuthority).collect(Collectors.toList()), settings.getTokenExpirationTime()); + jwtBuilder.claim(FIRST_NAME, securityUser.getFirstName()) + .claim(LAST_NAME, securityUser.getLastName()) + .claim(ENABLED, securityUser.isEnabled()) + .claim(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID); if (securityUser.getTenantId() != null) { - claims.put(TENANT_ID, securityUser.getTenantId().getId().toString()); + jwtBuilder.claim(TENANT_ID, securityUser.getTenantId().getId().toString()); } if (securityUser.getCustomerId() != null) { - claims.put(CUSTOMER_ID, securityUser.getCustomerId().getId().toString()); + jwtBuilder.claim(CUSTOMER_ID, securityUser.getCustomerId().getId().toString()); } - ZonedDateTime currentTime = ZonedDateTime.now(); + String token = jwtBuilder.compact(); - String token = Jwts.builder() - .setClaims(claims) - .setIssuer(settings.getTokenIssuer()) - .setIssuedAt(Date.from(currentTime.toInstant())) - .setExpiration(Date.from(currentTime.plusSeconds(settings.getTokenExpirationTime()).toInstant())) - .signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey()) - .compact(); - - return new AccessJwtToken(token, claims); + return new AccessJwtToken(token); } public SecurityUser parseAccessJwtToken(RawAccessJwtToken rawAccessToken) { @@ -118,47 +110,40 @@ public class JwtTokenFactory { SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class)))); securityUser.setEmail(subject); securityUser.setAuthority(Authority.parse(scopes.get(0))); - securityUser.setFirstName(claims.get(FIRST_NAME, String.class)); - securityUser.setLastName(claims.get(LAST_NAME, String.class)); - securityUser.setEnabled(claims.get(ENABLED, Boolean.class)); - boolean isPublic = claims.get(IS_PUBLIC, Boolean.class); - UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject); - securityUser.setUserPrincipal(principal); String tenantId = claims.get(TENANT_ID, String.class); if (tenantId != null) { securityUser.setTenantId(TenantId.fromUUID(UUID.fromString(tenantId))); + } else if (securityUser.getAuthority() == Authority.SYS_ADMIN) { + securityUser.setTenantId(TenantId.SYS_TENANT_ID); } - String customerId = claims.get(CUSTOMER_ID, String.class); - if (customerId != null) { - securityUser.setCustomerId(new CustomerId(UUID.fromString(customerId))); + securityUser.setSessionId(claims.get(SESSION_ID, String.class)); + + if (securityUser.getAuthority() != Authority.PRE_VERIFICATION_TOKEN) { + securityUser.setFirstName(claims.get(FIRST_NAME, String.class)); + securityUser.setLastName(claims.get(LAST_NAME, String.class)); + securityUser.setEnabled(claims.get(ENABLED, Boolean.class)); + boolean isPublic = claims.get(IS_PUBLIC, Boolean.class); + UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject); + securityUser.setUserPrincipal(principal); + String customerId = claims.get(CUSTOMER_ID, String.class); + if (customerId != null) { + securityUser.setCustomerId(new CustomerId(UUID.fromString(customerId))); + } + } else { + securityUser.setUserPrincipal(new UserPrincipal(UserPrincipal.Type.USER_NAME, subject)); } return securityUser; } public JwtToken createRefreshToken(SecurityUser securityUser) { - if (StringUtils.isBlank(securityUser.getEmail())) { - throw new IllegalArgumentException("Cannot create JWT Token without username/email"); - } - - ZonedDateTime currentTime = ZonedDateTime.now(); - UserPrincipal principal = securityUser.getUserPrincipal(); - Claims claims = Jwts.claims().setSubject(principal.getValue()); - claims.put(SCOPES, Collections.singletonList(Authority.REFRESH_TOKEN.name())); - claims.put(USER_ID, securityUser.getId().getId().toString()); - claims.put(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID); - String token = Jwts.builder() - .setClaims(claims) - .setIssuer(settings.getTokenIssuer()) - .setId(UUID.randomUUID().toString()) - .setIssuedAt(Date.from(currentTime.toInstant())) - .setExpiration(Date.from(currentTime.plusSeconds(settings.getRefreshTokenExpTime()).toInstant())) - .signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey()) - .compact(); + String token = setUpToken(securityUser, Collections.singletonList(Authority.REFRESH_TOKEN.name()), settings.getRefreshTokenExpTime()) + .claim(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID) + .setId(UUID.randomUUID().toString()).compact(); - return new AccessJwtToken(token, claims); + return new AccessJwtToken(token); } public SecurityUser parseRefreshToken(RawAccessJwtToken rawAccessToken) { @@ -177,9 +162,41 @@ public class JwtTokenFactory { UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject); SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class)))); securityUser.setUserPrincipal(principal); + securityUser.setSessionId(claims.get(SESSION_ID, String.class)); return securityUser; } + public JwtToken createPreVerificationToken(SecurityUser user) { + String token = setUpToken(user, Collections.singletonList(Authority.PRE_VERIFICATION_TOKEN.name()), settings.getPreVerificationTokenExpirationTime()) + .claim(TENANT_ID, user.getTenantId().toString()) + .compact(); + return new AccessJwtToken(token); + } + + private JwtBuilder setUpToken(SecurityUser securityUser, List scopes, long expirationTime) { + if (StringUtils.isBlank(securityUser.getEmail())) { + throw new IllegalArgumentException("Cannot create JWT Token without username/email"); + } + + UserPrincipal principal = securityUser.getUserPrincipal(); + + Claims claims = Jwts.claims().setSubject(principal.getValue()); + claims.put(USER_ID, securityUser.getId().getId().toString()); + claims.put(SCOPES, scopes); + if (securityUser.getSessionId() != null) { + claims.put(SESSION_ID, securityUser.getSessionId()); + } + + ZonedDateTime currentTime = ZonedDateTime.now(); + + return Jwts.builder() + .setClaims(claims) + .setIssuer(settings.getTokenIssuer()) + .setIssuedAt(Date.from(currentTime.toInstant())) + .setExpiration(Date.from(currentTime.plusSeconds(expirationTime).toInstant())) + .signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey()); + } + public Jws parseTokenClaims(JwtToken token) { try { return Jwts.parser() @@ -193,4 +210,11 @@ public class JwtTokenFactory { throw new JwtExpiredTokenException(token, "JWT Token expired", expiredEx); } } + + public JwtTokenPair createTokenPair(SecurityUser securityUser) { + JwtToken accessToken = createAccessJwtToken(securityUser); + JwtToken refreshToken = createRefreshToken(securityUser); + return new JwtTokenPair(accessToken.getToken(), refreshToken.getToken()); + } + } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 51494484db..e79d8979ce 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -127,6 +127,8 @@ security: jwt: tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:9000}" # Number of seconds (2.5 hours) refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:604800}" # Number of seconds (1 week) + # Number of seconds. Issued when 2FA is being used; valid only for checking 2FA verification code after which usual token pair is issued + preVerificationTokenExpirationTime: "${JWT_PRE_VERIFICATION_TOKEN_EXPIRATION_TIME:30}" tokenIssuer: "${JWT_TOKEN_ISSUER:thingsboard.io}" tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}" # Enable/disable access to Tenant Administrators JWT token by System Administrator or Customer Users JWT token by Tenant Administrator @@ -425,6 +427,9 @@ caffeine: edges: timeToLiveInMinutes: "${CACHE_SPECS_EDGES_TTL:1440}" maxSize: "${CACHE_SPECS_EDGES_MAX_SIZE:10000}" + twoFaVerificationCodes: + timeToLiveInMinutes: "1" + maxSize: "100000" redis: # standalone or cluster diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/Authority.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/Authority.java index e30d481118..06efb4240b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/Authority.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/Authority.java @@ -16,11 +16,12 @@ package org.thingsboard.server.common.data.security; public enum Authority { - + SYS_ADMIN(0), TENANT_ADMIN(1), CUSTOMER_USER(2), - REFRESH_TOKEN(10); + REFRESH_TOKEN(10), + PRE_VERIFICATION_TOKEN(11); private int code; From 9eb03950fa78215b3411d6e903568cdfc1b2a8fb Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Thu, 17 Mar 2022 18:49:08 +0200 Subject: [PATCH 004/276] 2FA: refactoring, tests --- .../server/controller/BaseController.java | 20 ++ .../controller/TwoFactorAuthController.java | 7 +- .../auth/mfa/TwoFactorAuthService.java | 5 +- .../impl/SmsTwoFactorAuthProvider.java | 59 ++-- ...RestAwareAuthenticationSuccessHandler.java | 3 + .../service/security/model/JwtTokenPair.java | 10 +- .../src/main/resources/thingsboard.yml | 4 +- .../server/controller/AbstractWebTest.java | 4 + .../server/controller/TwoFactorAuthTest.java | 256 ++++++++++++++++++ .../controller/sql/TwoFactorAuthSqlTest.java | 23 ++ .../server/common/data/CacheConstants.java | 1 + .../dao/service/ConstraintValidator.java | 3 +- .../service/BaseOtaPackageServiceTest.java | 2 +- 13 files changed, 352 insertions(+), 45 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java create mode 100644 application/src/test/java/org/thingsboard/server/controller/sql/TwoFactorAuthSqlTest.java 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 3ed2d4a47d..6dee3b6114 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -283,11 +283,31 @@ public abstract class BaseController { @Getter protected boolean edgesEnabled; + @ExceptionHandler(Exception.class) + public void handleControllerException(Exception e, HttpServletResponse response) { + ThingsboardException thingsboardException = handleException(e); + handleThingsboardException(thingsboardException, response); + } + @ExceptionHandler(ThingsboardException.class) public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) { errorResponseHandler.handle(ex, response); } + /** + * @deprecated Exceptions that are not of {@link ThingsboardException} type + * are now caught and mapped to {@link ThingsboardException} by + * {@link ExceptionHandler} {@link BaseController#handleControllerException(Exception, HttpServletResponse)} + * which basically acts like the following boilerplate: + * {@code + * try { + * someExceptionThrowingMethod(); + * } catch (Exception e) { + * throw handleException(e); + * } + * } + * */ + @Deprecated ThingsboardException handleException(Exception exception) { return handleException(exception, true); } diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java index 1355337127..f0d50c2600 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -30,6 +30,7 @@ import org.springframework.web.bind.annotation.RestController; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig; @@ -42,6 +43,9 @@ import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; +// FIXME: Swagger documentation +// FIXME: tests for 2FA + @RestController @RequestMapping("/api") @RequiredArgsConstructor @@ -81,7 +85,7 @@ public class TwoFactorAuthController extends BaseController { MatrixToImageWriter.writeToStream(qr, "PNG", outputStream); } } - response.setHeader("body", JacksonUtil.toString(config)); + response.setHeader("config", JacksonUtil.toString(config)); } @PostMapping("/2fa/account/config/submit") @@ -91,6 +95,7 @@ public class TwoFactorAuthController extends BaseController { twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), accountConfig.getProviderType(), (provider, providerConfig) -> { + ConstraintValidator.validateFields(accountConfig); provider.prepareVerificationCode(user, providerConfig, accountConfig); }); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java index 51d4af28fd..2b44fddeb5 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java @@ -46,6 +46,7 @@ import java.util.Collections; import java.util.EnumMap; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ExecutionException; import java.util.function.BiConsumer; import java.util.function.BiFunction; @@ -144,7 +145,7 @@ public class TwoFactorAuthService { } - @SneakyThrows + @SneakyThrows({InterruptedException.class, ExecutionException.class}) public Optional getTwoFaSettings(TenantId tenantId) { if (tenantId.equals(TenantId.SYS_TENANT_ID)) { return Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY)) @@ -157,7 +158,7 @@ public class TwoFactorAuthService { } } - @SneakyThrows + @SneakyThrows({InterruptedException.class, ExecutionException.class}) public void saveTwoFaSettings(TenantId tenantId, TwoFactorAuthSettings twoFactorAuthSettings) { ConstraintValidator.validateFields(twoFactorAuthSettings); if (tenantId.equals(TenantId.SYS_TENANT_ID)) { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFactorAuthProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFactorAuthProvider.java index 7d4a0b9ab7..2dda68899b 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFactorAuthProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFactorAuthProvider.java @@ -15,18 +15,16 @@ */ package org.thingsboard.server.service.security.auth.mfa.provider.impl; -import lombok.RequiredArgsConstructor; +import lombok.Data; import lombok.SneakyThrows; import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery; -import org.thingsboard.server.common.data.kv.BasicTsKvEntry; -import org.thingsboard.server.common.data.kv.StringDataEntry; -import org.thingsboard.server.dao.service.ConstraintValidator; -import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.mfa.config.account.SmsTwoFactorAuthAccountConfig; import org.thingsboard.server.service.security.auth.mfa.config.provider.SmsTwoFactorAuthProviderConfig; @@ -34,16 +32,20 @@ import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthPr import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; import org.thingsboard.server.service.security.model.SecurityUser; -import java.util.Collections; import java.util.Map; @Service -@RequiredArgsConstructor @TbCoreComponent public class SmsTwoFactorAuthProvider implements TwoFactorAuthProvider { private final SmsService smsService; - private final TimeseriesService timeseriesService; + private final Cache verificationCodesCache; + + public SmsTwoFactorAuthProvider(SmsService smsService, CacheManager cacheManager) { + this.smsService = smsService; + this.verificationCodesCache = cacheManager.getCache(CacheConstants.TWO_FA_VERIFICATION_CODES_CACHE); + } + @Override public SmsTwoFactorAuthAccountConfig generateNewAccountConfig(User user, SmsTwoFactorAuthProviderConfig providerConfig) { @@ -53,10 +55,8 @@ public class SmsTwoFactorAuthProvider implements TwoFactorAuthProvider codeTs.getStrValue().get()) - .orElse(null); - } - - private void removeVerificationCode(SecurityUser user) { - timeseriesService.remove(user.getTenantId(), user.getId(), Collections.singletonList( - new BaseDeleteTsKvQuery("twoFaVerificationCode:" + user.getSessionId(), 0, System.currentTimeMillis()) - )); - } - - @Override public TwoFactorAuthProviderType getType() { return TwoFactorAuthProviderType.SMS; } + + @Data + private static class VerificationCode { + private final long timestamp; + private final String value; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java index ca2f15953a..bb5feb9af6 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java @@ -24,6 +24,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.web.WebAttributes; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; @@ -53,6 +54,7 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc JwtTokenPair tokenPair; + // fixme: check if this handler is not called when token is refreshed Optional twoFaAccountConfig = twoFactorAuthService.getTwoFaAccountConfig(securityUser.getTenantId(), securityUser.getId()); if (twoFaAccountConfig.isPresent()) { try { @@ -62,6 +64,7 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc }); tokenPair = new JwtTokenPair(); tokenPair.setToken(tokenFactory.createPreVerificationToken(securityUser).getToken()); + tokenPair.setScope(Authority.PRE_VERIFICATION_TOKEN); } catch (Exception e) { log.error("Failed to process 2FA for user {}. Falling back to plain auth", securityUser.getId(), e); tokenPair = tokenFactory.createTokenPair(securityUser); diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/JwtTokenPair.java b/application/src/main/java/org/thingsboard/server/service/security/model/JwtTokenPair.java index 57964570c1..02e28cd885 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/JwtTokenPair.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/JwtTokenPair.java @@ -20,10 +20,10 @@ import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.security.Authority; @ApiModel(value = "JWT Token Pair") @Data -@AllArgsConstructor @NoArgsConstructor public class JwtTokenPair { @@ -31,4 +31,12 @@ public class JwtTokenPair { private String token; @ApiModelProperty(position = 1, value = "The JWT Refresh Token. Used to get new JWT Access Token if old one has expired.", example = "AAB254FF67D..") private String refreshToken; + + private Authority scope; + + public JwtTokenPair(String token, String refreshToken) { + this.token = token; + this.refreshToken = refreshToken; + } + } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index e79d8979ce..ba6dc222ab 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -428,8 +428,8 @@ caffeine: timeToLiveInMinutes: "${CACHE_SPECS_EDGES_TTL:1440}" maxSize: "${CACHE_SPECS_EDGES_MAX_SIZE:10000}" twoFaVerificationCodes: - timeToLiveInMinutes: "1" - maxSize: "100000" + timeToLiveInMinutes: "${CACHE_SPECS_TWO_FA_VERIFICATION_CODES_TTL:1}" + maxSize: "${CACHE_SPECS_TWO_FA_VERIFICATION_CODES_MAX_SIZE:100000}" redis: # standalone or cluster diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 6cbf5d82d0..32d55a71dd 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -584,6 +584,10 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return mapper.readerFor(type).readValue(content); } + protected String getErrorMessage(ResultActions result) throws Exception { + return readResponse(result, JsonNode.class).get("message").asText(); + } + public class IdComparator implements Comparator { @Override public int compare(D o1, D o2) { diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java new file mode 100644 index 0000000000..fcc2c8bcdf --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java @@ -0,0 +1,256 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.controller; + +import org.jboss.aerogear.security.otp.Totp; +import org.jboss.aerogear.security.otp.api.Base32; +import org.junit.Before; +import org.junit.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; +import org.thingsboard.rule.engine.api.SmsService; +import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; +import org.thingsboard.server.service.security.auth.mfa.config.account.SmsTwoFactorAuthAccountConfig; +import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig; +import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; +import org.thingsboard.server.service.security.auth.mfa.config.provider.SmsTwoFactorAuthProviderConfig; +import org.thingsboard.server.service.security.auth.mfa.config.provider.TotpTwoFactorAuthProviderConfig; +import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig; +import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; +import org.thingsboard.server.service.security.auth.mfa.provider.impl.TotpTwoFactorAuthProvider; + +import java.util.Arrays; +import java.util.Collections; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public abstract class TwoFactorAuthTest extends AbstractControllerTest { + + @SpyBean + private TotpTwoFactorAuthProvider totpTwoFactorAuthProvider; + @MockBean + private SmsService smsService; + + @Before + public void beforeEach() throws Exception { + loginSysAdmin(); + } + + + @Test + public void testSaveTwoFaSettings() throws Exception { + loginSysAdmin(); + testSaveTestTwoFaSettings(); + + loginTenantAdmin(); + testSaveTestTwoFaSettings(); + } + + private void testSaveTestTwoFaSettings() throws Exception { + TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig(); + totpTwoFaProviderConfig.setIssuerName("tb"); + SmsTwoFactorAuthProviderConfig smsTwoFaProviderConfig = new SmsTwoFactorAuthProviderConfig(); + smsTwoFaProviderConfig.setSmsVerificationMessageTemplate("${verificationCode}"); + + saveProvidersConfigs(totpTwoFaProviderConfig, smsTwoFaProviderConfig); + + TwoFactorAuthSettings savedTwoFaSettings = readResponse(doGet("/api/2fa/settings").andExpect(status().isOk()), TwoFactorAuthSettings.class); + + assertThat(savedTwoFaSettings.getProviders()).hasSize(2); + assertThat(savedTwoFaSettings.getProviders()).contains(totpTwoFaProviderConfig, smsTwoFaProviderConfig); + } + + @Test + public void testSaveTotpTwoFaProviderConfig_validationError() throws Exception { + TotpTwoFactorAuthProviderConfig invalidTotpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig(); + invalidTotpTwoFaProviderConfig.setIssuerName(" "); + + String errorResponse = saveTwoFaSettingsAndGetError(invalidTotpTwoFaProviderConfig); + assertThat(errorResponse).containsIgnoringCase("issuer name must not be blank"); + } + + @Test + public void testSaveSmsTwoFaProviderConfig_validationError() throws Exception { + SmsTwoFactorAuthProviderConfig invalidSmsTwoFaProviderConfig = new SmsTwoFactorAuthProviderConfig(); + invalidSmsTwoFaProviderConfig.setSmsVerificationMessageTemplate("does not contain verification code"); + + String errorResponse = saveTwoFaSettingsAndGetError(invalidSmsTwoFaProviderConfig); + assertThat(errorResponse).containsIgnoringCase("must contain verification code"); + } + + private String saveTwoFaSettingsAndGetError(TwoFactorAuthProviderConfig invalidTwoFaProviderConfig) throws Exception { + TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings(); + twoFaSettings.setProviders(Collections.singletonList(invalidTwoFaProviderConfig)); + + return getErrorMessage(doPost("/api/2fa/settings", twoFaSettings) + .andExpect(status().isBadRequest())); + } + + @Test + public void testSaveTwoFaAccountConfig_providerNotConfigured() throws Exception { + configureSmsTwoFaProvider("${verificationCode}"); + + loginTenantAdmin(); + + TwoFactorAuthProviderType notConfiguredProviderType = TwoFactorAuthProviderType.TOTP; + String errorMessage = getErrorMessage(doPost("/api/2fa/account/config/generate?providerType=" + notConfiguredProviderType) + .andExpect(status().isBadRequest())); + assertThat(errorMessage).containsIgnoringCase("provider is not configured"); + + TotpTwoFactorAuthAccountConfig notConfiguredProviderAccountConfig = new TotpTwoFactorAuthAccountConfig(); + notConfiguredProviderAccountConfig.setAuthUrl("aba"); + errorMessage = getErrorMessage(doPost("/api/2fa/account/config/submit", notConfiguredProviderAccountConfig)); + assertThat(errorMessage).containsIgnoringCase("provider is not configured"); + } + + @Test + public void testGenerateTotpTwoFaAccountConfig() throws Exception { + TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider(); + + loginTenantAdmin(); + + assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), String.class)).isNullOrEmpty(); + generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig); + } + + @Test + public void testSubmitTotpTwoFaAccountConfig() throws Exception { + TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider(); + + loginTenantAdmin(); + + TotpTwoFactorAuthAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig); + doPost("/api/2fa/account/config/submit", generatedTotpTwoFaAccountConfig).andExpect(status().isOk()); + verify(totpTwoFactorAuthProvider).prepareVerificationCode(argThat(user -> user.getEmail().equals(TENANT_ADMIN_EMAIL)), + eq(totpTwoFaProviderConfig), eq(generatedTotpTwoFaAccountConfig)); + } + + @Test + public void testVerifyAndSaveTotpTwoFaAccountConfig() throws Exception { + TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider(); + + loginTenantAdmin(); + + TotpTwoFactorAuthAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig); + + String secret = UriComponentsBuilder.fromUriString(generatedTotpTwoFaAccountConfig.getAuthUrl()).build() + .getQueryParams().getFirst("secret"); + String correctVerificationCode = new Totp(secret).now(); + + doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, generatedTotpTwoFaAccountConfig) + .andExpect(status().isOk()); + + TwoFactorAuthAccountConfig twoFaAccountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFactorAuthAccountConfig.class); + assertThat(twoFaAccountConfig).isEqualTo(generatedTotpTwoFaAccountConfig); + } + + @Test + public void testVerifyAndSaveTotpTwoFaAccountConfig_incorrectVerificationCode() throws Exception { + TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider(); + + loginTenantAdmin(); + + TotpTwoFactorAuthAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig); + + String incorrectVerificationCode = "100000"; + String errorMessage = getErrorMessage(doPost("/api/2fa/account/config?verificationCode=" + incorrectVerificationCode, generatedTotpTwoFaAccountConfig) + .andExpect(status().isBadRequest())); + + assertThat(errorMessage).containsIgnoringCase("verification code is incorrect"); + } + + private TotpTwoFactorAuthAccountConfig generateTotpTwoFaAccountConfig(TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig) throws Exception { + TwoFactorAuthAccountConfig generatedTwoFaAccountConfig = readResponse(doPost("/api/2fa/account/config/generate?providerType=TOTP") + .andExpect(status().isOk()), TwoFactorAuthAccountConfig.class); + assertThat(generatedTwoFaAccountConfig).isInstanceOf(TotpTwoFactorAuthAccountConfig.class); + + assertThat(((TotpTwoFactorAuthAccountConfig) generatedTwoFaAccountConfig)).satisfies(accountConfig -> { + UriComponents otpAuthUrl = UriComponentsBuilder.fromUriString(accountConfig.getAuthUrl()).build(); + assertThat(otpAuthUrl.getScheme()).isEqualTo("otpauth"); + assertThat(otpAuthUrl.getHost()).isEqualTo("totp"); + assertThat(otpAuthUrl.getQueryParams().getFirst("issuer")).isEqualTo(totpTwoFaProviderConfig.getIssuerName()); + assertThat(otpAuthUrl.getPath()).isEqualTo("/%s:%s", totpTwoFaProviderConfig.getIssuerName(), TENANT_ADMIN_EMAIL); + assertThat(otpAuthUrl.getQueryParams().getFirst("secret")).satisfies(secretKey -> { + assertDoesNotThrow(() -> Base32.decode(secretKey)); + }); + }); + return (TotpTwoFactorAuthAccountConfig) generatedTwoFaAccountConfig; + } + + + @Test + public void testGetTwoFaAccountConfig_whenProviderNotConfigured() throws Exception { + testVerifyAndSaveTotpTwoFaAccountConfig(); + assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), + TotpTwoFactorAuthAccountConfig.class)).isNotNull(); + + loginSysAdmin(); + + saveProvidersConfigs(); + + assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), String.class)) + .isNullOrEmpty(); + } + +// @Test +// public void testSubmitSmsTwoFaAccountConfig() throws Exception { +// String verificationMessageTemplate = "Here is your verification code: ${verificationCode}"; +// SmsTwoFactorAuthProviderConfig smsTwoFaProviderConfig = configureSmsTwoFaProvider(verificationMessageTemplate); +// +// SmsTwoFactorAuthAccountConfig smsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig(); +// smsTwoFaAccountConfig.setPhoneNumber("+38054159785"); +// +// String verificationCode = ""; ? +// +// verify(smsService).sendSms(eq(tenantId), any(), argThat(phoneNumbers -> { +// return phoneNumbers[0].equals(smsTwoFaAccountConfig.getPhoneNumber()) +// }), eq("Here is your verification code: " + verificationCode)); +// } + + + + private TotpTwoFactorAuthProviderConfig configureTotpTwoFaProvider() throws Exception { + TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig(); + totpTwoFaProviderConfig.setIssuerName("tb"); + + saveProvidersConfigs(totpTwoFaProviderConfig); + return totpTwoFaProviderConfig; + } + + private SmsTwoFactorAuthProviderConfig configureSmsTwoFaProvider(String verificationMessageTemplate) throws Exception { + SmsTwoFactorAuthProviderConfig smsTwoFaProviderConfig = new SmsTwoFactorAuthProviderConfig(); + smsTwoFaProviderConfig.setSmsVerificationMessageTemplate(verificationMessageTemplate); + + saveProvidersConfigs(smsTwoFaProviderConfig); + return smsTwoFaProviderConfig; + } + + private void saveProvidersConfigs(TwoFactorAuthProviderConfig... providerConfigs) throws Exception { + TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings(); + twoFaSettings.setProviders(Arrays.stream(providerConfigs).collect(Collectors.toList())); + doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/TwoFactorAuthSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/TwoFactorAuthSqlTest.java new file mode 100644 index 0000000000..62024fc64e --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/sql/TwoFactorAuthSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.controller.sql; + +import org.thingsboard.server.controller.TwoFactorAuthTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class TwoFactorAuthSqlTest extends TwoFactorAuthTest { +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index 571af34086..49db7befd3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -31,4 +31,5 @@ public class CacheConstants { public static final String TOKEN_OUTDATAGE_TIME_CACHE = "tokensOutdatageTime"; public static final String OTA_PACKAGE_CACHE = "otaPackages"; public static final String OTA_PACKAGE_DATA_CACHE = "otaPackagesData"; + public static final String TWO_FA_VERIFICATION_CODES_CACHE = "twoFaVerificationCodes"; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java index da7537ae75..404eeb44cc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java @@ -21,6 +21,7 @@ import org.hibernate.validator.HibernateValidatorConfiguration; import org.hibernate.validator.cfg.ConstraintMapping; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; +import org.thingsboard.server.dao.exception.DataValidationException; import javax.validation.ConstraintViolation; import javax.validation.Validation; @@ -46,7 +47,7 @@ public class ConstraintValidator { .distinct() .collect(Collectors.toList()); if (!validationErrors.isEmpty()) { - throw new ValidationException(String.join(", ", validationErrors)); + throw new DataValidationException("Validation error: " + String.join(", ", validationErrors)); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java index d49594a955..b750d5f1c3 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java @@ -675,7 +675,7 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmwareInfo.setUrl(URL); firmwareInfo.setTenantId(tenantId); - thrown.expect(ValidationException.class); + thrown.expect(DataValidationException.class); thrown.expectMessage("length of title must be equal or less than 255"); otaPackageService.saveOtaPackageInfo(firmwareInfo, true); From b5afb32f569c88ec56a77a9359578d2f0bd36a0d Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Fri, 18 Mar 2022 11:05:17 +0200 Subject: [PATCH 005/276] 2FA: validation and error handling refactoring --- .../server/controller/BaseController.java | 15 ++++++ .../controller/TwoFactorAuthController.java | 47 ++++++++++++++----- .../auth/mfa/TwoFactorAuthService.java | 23 +++++---- .../TotpTwoFactorAuthAccountConfig.java | 2 + .../SmsTwoFactorAuthProviderConfig.java | 2 +- .../TotpTwoFactorAuthProviderConfig.java | 2 +- .../mfa/provider/TwoFactorAuthProvider.java | 3 +- .../impl/SmsTwoFactorAuthProvider.java | 4 +- .../auth/rest/RestAuthenticationProvider.java | 1 + ...RestAwareAuthenticationSuccessHandler.java | 33 ++++++++----- .../common/util/ThrowingBiConsumer.java | 21 +++++++++ ...eFunction.java => ThrowingBiFunction.java} | 4 +- .../common/util/ThrowingTripleConsumer.java | 21 +++++++++ .../common/util/ThrowingTripleFunction.java | 21 +++++++++ 14 files changed, 159 insertions(+), 40 deletions(-) create mode 100644 common/util/src/main/java/org/thingsboard/common/util/ThrowingBiConsumer.java rename common/util/src/main/java/org/thingsboard/common/util/{TripleFunction.java => ThrowingBiFunction.java} (88%) create mode 100644 common/util/src/main/java/org/thingsboard/common/util/ThrowingTripleConsumer.java create mode 100644 common/util/src/main/java/org/thingsboard/common/util/ThrowingTripleFunction.java 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 6dee3b6114..fdef4cbc0c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -23,9 +23,11 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.Customer; @@ -145,6 +147,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; import static org.thingsboard.server.controller.ControllerConstants.DEFAULT_PAGE_SIZE; import static org.thingsboard.server.controller.ControllerConstants.INCORRECT_TENANT_ID; @@ -334,6 +337,18 @@ public abstract class BaseController { } } + /** + * Handles validation error for controller method arguments annotated with @{@link javax.validation.Valid} + * */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public void handleValidationError(MethodArgumentNotValidException e, HttpServletResponse response) { + String errorMessage = "Validation error: " + e.getBindingResult().getAllErrors().stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .collect(Collectors.joining(", ")); + ThingsboardException thingsboardException = new ThingsboardException(errorMessage, ThingsboardErrorCode.BAD_REQUEST_PARAMS); + handleThingsboardException(thingsboardException, response); + } + T checkNotNull(T reference) throws ThingsboardException { return checkNotNull(reference, "Requested item wasn't found!"); } diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java index f0d50c2600..b6ad9626b1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -30,7 +30,7 @@ import org.springframework.web.bind.annotation.RestController; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.dao.service.ConstraintValidator; +import org.thingsboard.server.service.security.auth.TokenOutdatingService; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig; @@ -42,10 +42,25 @@ import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; -// FIXME: Swagger documentation -// FIXME: tests for 2FA - +/* + * + * TODO [viacheslav]: + * - 2FA should be mandatory when logging in and must be rolled out to all existing users when 2FA is activated. + * - Rate limits should be implemented to protect against brute force leaked accounts to prevent SMS cost explosion. + * - Configurable softlock after XX (3) attempts: XX (15) mins + * - Configurable hardlock (user blocking) after a total of XX (10) unsuccessful attempts. + * - The OTP token should only be valid for XX (5) minutes. + * - Disable 2FA only possible after successful 2FA auth - it is possible with simple password resest + * - 2FA entries should be secured against code injection by code validation. + * - Email 2FA provider + * + * FIXME [viacheslav]: + * - Tests for 2FA + * - Swagger documentation + * + * */ @RestController @RequestMapping("/api") @RequiredArgsConstructor @@ -65,7 +80,7 @@ public class TwoFactorAuthController extends BaseController { @PostMapping("/2fa/account/config/generate") @PreAuthorize("isAuthenticated()") - public TwoFactorAuthAccountConfig generateTwoFactorAuthAccountConfig(@RequestParam TwoFactorAuthProviderType providerType) throws ThingsboardException { + public TwoFactorAuthAccountConfig generateTwoFactorAuthAccountConfig(@RequestParam TwoFactorAuthProviderType providerType) throws Exception { SecurityUser user = getCurrentUser(); return twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), providerType, @@ -90,20 +105,19 @@ public class TwoFactorAuthController extends BaseController { @PostMapping("/2fa/account/config/submit") @PreAuthorize("isAuthenticated()") - public void submitTwoFactorAuthAccountConfig(@RequestBody TwoFactorAuthAccountConfig accountConfig) throws ThingsboardException { + public void submitTwoFactorAuthAccountConfig(@Valid @RequestBody TwoFactorAuthAccountConfig accountConfig) throws Exception { SecurityUser user = getCurrentUser(); twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), accountConfig.getProviderType(), (provider, providerConfig) -> { - ConstraintValidator.validateFields(accountConfig); provider.prepareVerificationCode(user, providerConfig, accountConfig); }); } @PostMapping("/2fa/account/config") @PreAuthorize("isAuthenticated()") - public void verifyAndSaveTwoFactorAuthAccountConfig(@RequestBody TwoFactorAuthAccountConfig accountConfig, - @RequestParam String verificationCode) throws ThingsboardException { + public void verifyAndSaveTwoFactorAuthAccountConfig(@Valid @RequestBody TwoFactorAuthAccountConfig accountConfig, + @RequestParam String verificationCode) throws Exception { SecurityUser user = getCurrentUser(); boolean verificationSuccess = twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), accountConfig.getProviderType(), @@ -127,14 +141,14 @@ public class TwoFactorAuthController extends BaseController { @PostMapping("/2fa/settings") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - public void saveTwoFactorAuthSettings(@RequestBody TwoFactorAuthSettings twoFactorAuthSettings) throws ThingsboardException { + public void saveTwoFactorAuthSettings(@Valid @RequestBody TwoFactorAuthSettings twoFactorAuthSettings) throws ThingsboardException { twoFactorAuthService.saveTwoFaSettings(getTenantId(), twoFactorAuthSettings); } @PostMapping("/auth/2fa/verification/check") @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") - public JwtTokenPair checkTwoFaVerificationCode(@RequestParam String verificationCode) throws ThingsboardException { + public JwtTokenPair checkTwoFaVerificationCode(@RequestParam String verificationCode) throws Exception { SecurityUser user = getCurrentUser(); boolean verificationSuccess = twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), user.getId(), @@ -149,4 +163,15 @@ public class TwoFactorAuthController extends BaseController { } } + @PostMapping("/auth/2fa/verification/resend") + @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") + public void resendTwoFaVerificationCode() throws Exception { + SecurityUser user = getCurrentUser(); + + twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), user.getId(), + (provider, providerConfig, accountConfig) -> { + provider.prepareVerificationCode(user, providerConfig, accountConfig); + }); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java index 2b44fddeb5..580a12ce44 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java @@ -21,7 +21,10 @@ import lombok.SneakyThrows; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.common.util.TripleFunction; +import org.thingsboard.common.util.ThrowingBiConsumer; +import org.thingsboard.common.util.ThrowingBiFunction; +import org.thingsboard.common.util.ThrowingTripleConsumer; +import org.thingsboard.common.util.ThrowingTripleFunction; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.User; @@ -32,7 +35,6 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.dao.attributes.AttributesService; -import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; @@ -47,8 +49,6 @@ import java.util.EnumMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.ExecutionException; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; @Service @RequiredArgsConstructor @@ -81,7 +81,7 @@ public class TwoFactorAuthService { } - public R processByTwoFaProvider(TenantId tenantId, TwoFactorAuthProviderType providerType, BiFunction, TwoFactorAuthProviderConfig, R> function) throws ThingsboardException { + public R processByTwoFaProvider(TenantId tenantId, TwoFactorAuthProviderType providerType, ThrowingBiFunction, TwoFactorAuthProviderConfig, R> function) throws Exception { TwoFactorAuthProviderConfig providerConfig = getTwoFaProviderConfig(tenantId, providerType) .orElseThrow(() -> new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS)); TwoFactorAuthProvider provider = getTwoFaProvider(providerType) @@ -90,14 +90,14 @@ public class TwoFactorAuthService { return function.apply(provider, providerConfig); } - public void processByTwoFaProvider(TenantId tenantId, TwoFactorAuthProviderType providerType, BiConsumer, TwoFactorAuthProviderConfig> function) throws ThingsboardException { + public void processByTwoFaProvider(TenantId tenantId, TwoFactorAuthProviderType providerType, ThrowingBiConsumer, TwoFactorAuthProviderConfig> function) throws Exception { processByTwoFaProvider(tenantId, providerType, (provider, providerConfig) -> { function.accept(provider, providerConfig); return null; }); } - public R processByTwoFaProvider(TenantId tenantId, UserId userId, TripleFunction, TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig, R> function) throws ThingsboardException { + public R processByTwoFaProvider(TenantId tenantId, UserId userId, ThrowingTripleFunction, TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig, R> function) throws Exception { TwoFactorAuthAccountConfig accountConfig = getTwoFaAccountConfig(tenantId, userId) .orElseThrow(() -> new ThingsboardException("2FA is not configured for user", ThingsboardErrorCode.BAD_REQUEST_PARAMS)); @@ -109,6 +109,13 @@ public class TwoFactorAuthService { return function.apply(provider, providerConfig, accountConfig); } + public void processByTwoFaProvider(TenantId tenantId, UserId userId, ThrowingTripleConsumer, TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig> function) throws Exception { + processByTwoFaProvider(tenantId, userId, (provider, providerConfig, accountConfig) -> { + function.accept(provider, providerConfig, accountConfig); + return null; + }); + } + public Optional getTwoFaAccountConfig(TenantId tenantId, UserId userId) { User user = userService.findUserById(tenantId, userId); @@ -121,7 +128,6 @@ public class TwoFactorAuthService { } public void saveTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFactorAuthAccountConfig accountConfig) throws ThingsboardException { - ConstraintValidator.validateFields(accountConfig); getTwoFaProviderConfig(tenantId, accountConfig.getProviderType()) .orElseThrow(() -> new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS)); @@ -160,7 +166,6 @@ public class TwoFactorAuthService { @SneakyThrows({InterruptedException.class, ExecutionException.class}) public void saveTwoFaSettings(TenantId tenantId, TwoFactorAuthSettings twoFactorAuthSettings) { - ConstraintValidator.validateFields(twoFactorAuthSettings); if (tenantId.equals(TenantId.SYS_TENANT_ID)) { AdminSettings settings = Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY)) .orElseGet(() -> { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java index a48cf162a0..78ab4cf80b 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java @@ -19,11 +19,13 @@ import lombok.Data; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; @Data public class TotpTwoFactorAuthAccountConfig implements TwoFactorAuthAccountConfig { @NotBlank +// @Pattern(regexp = ) // TODO [viacheslav]: validate otp auth url by pattern private String authUrl; @Override diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java index a036e47574..2d15a07ad1 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java @@ -25,7 +25,7 @@ import javax.validation.constraints.Pattern; public class SmsTwoFactorAuthProviderConfig implements TwoFactorAuthProviderConfig { @NotBlank - @Pattern(regexp = ".*\\$\\{verificationCode}.*", message = "Template must contain verification code") + @Pattern(regexp = ".*\\$\\{verificationCode}.*", message = "template must contain verification code") private String smsVerificationMessageTemplate; @Override diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TotpTwoFactorAuthProviderConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TotpTwoFactorAuthProviderConfig.java index 2d6cc5ddf5..e1604bb618 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TotpTwoFactorAuthProviderConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TotpTwoFactorAuthProviderConfig.java @@ -23,7 +23,7 @@ import javax.validation.constraints.NotBlank; @Data public class TotpTwoFactorAuthProviderConfig implements TwoFactorAuthProviderConfig { - @NotBlank(message = "Issuer name must not be blank") + @NotBlank(message = "issuer name must not be blank") private String issuerName; @Override diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java index 82122a9163..011404268c 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.security.auth.mfa.provider; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig; import org.thingsboard.server.service.security.model.SecurityUser; @@ -24,7 +25,7 @@ public interface TwoFactorAuthProvider twoFaAccountConfig = twoFactorAuthService.getTwoFaAccountConfig(securityUser.getTenantId(), securityUser.getId()); if (twoFaAccountConfig.isPresent()) { try { @@ -62,23 +76,16 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc (provider, providerConfig) -> { provider.prepareVerificationCode(securityUser, providerConfig, twoFaAccountConfig.get()); }); - tokenPair = new JwtTokenPair(); + JwtTokenPair tokenPair = new JwtTokenPair(); tokenPair.setToken(tokenFactory.createPreVerificationToken(securityUser).getToken()); tokenPair.setScope(Authority.PRE_VERIFICATION_TOKEN); + return tokenPair; } catch (Exception e) { + // TODO [viacheslav]: write audit log log.error("Failed to process 2FA for user {}. Falling back to plain auth", securityUser.getId(), e); - tokenPair = tokenFactory.createTokenPair(securityUser); } - } else { - tokenPair = tokenFactory.createTokenPair(securityUser); } - - response.setStatus(HttpStatus.OK.value()); - response.setContentType(MediaType.APPLICATION_JSON_VALUE); - - mapper.writeValue(response.getWriter(), tokenPair); - - clearAuthenticationAttributes(request); + return tokenFactory.createTokenPair(securityUser); } /** diff --git a/common/util/src/main/java/org/thingsboard/common/util/ThrowingBiConsumer.java b/common/util/src/main/java/org/thingsboard/common/util/ThrowingBiConsumer.java new file mode 100644 index 0000000000..269f9f75cb --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/ThrowingBiConsumer.java @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.common.util; + +@FunctionalInterface +public interface ThrowingBiConsumer { + void accept(A a, B b) throws Exception; +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/TripleFunction.java b/common/util/src/main/java/org/thingsboard/common/util/ThrowingBiFunction.java similarity index 88% rename from common/util/src/main/java/org/thingsboard/common/util/TripleFunction.java rename to common/util/src/main/java/org/thingsboard/common/util/ThrowingBiFunction.java index 5134703cd3..32058df26e 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/TripleFunction.java +++ b/common/util/src/main/java/org/thingsboard/common/util/ThrowingBiFunction.java @@ -16,6 +16,6 @@ package org.thingsboard.common.util; @FunctionalInterface -public interface TripleFunction { - R apply(A a, B b, C c); +public interface ThrowingBiFunction { + R apply(A a, B b) throws Exception; } diff --git a/common/util/src/main/java/org/thingsboard/common/util/ThrowingTripleConsumer.java b/common/util/src/main/java/org/thingsboard/common/util/ThrowingTripleConsumer.java new file mode 100644 index 0000000000..5230da4863 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/ThrowingTripleConsumer.java @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.common.util; + +@FunctionalInterface +public interface ThrowingTripleConsumer { + void accept(A a, B b, C c) throws Exception; +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/ThrowingTripleFunction.java b/common/util/src/main/java/org/thingsboard/common/util/ThrowingTripleFunction.java new file mode 100644 index 0000000000..cade9d1717 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/ThrowingTripleFunction.java @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.common.util; + +@FunctionalInterface +public interface ThrowingTripleFunction { + R apply(A a, B b, C c) throws Exception; +} From 1a006285095750704e9a5e66b40f99f1442f4b51 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Fri, 18 Mar 2022 18:08:45 +0200 Subject: [PATCH 006/276] Email 2FA provider, refactoring --- .../controller/TwoFactorAuthController.java | 77 +++++++++++++------ .../security/auth/MfaAuthenticationToken.java | 24 ++++++ .../auth/mfa/TwoFactorAuthService.java | 36 ++++----- .../mfa/config/TwoFactorAuthSettings.java | 15 +++- .../EmailTwoFactorAuthAccountConfig.java | 34 ++++++++ .../OtpBasedTwoFactorAuthAccountConfig.java | 19 +++++ .../SmsTwoFactorAuthAccountConfig.java | 4 +- .../EmailTwoFactorAuthProviderConfig.java | 33 ++++++++ .../OtpBasedTwoFactorAuthProviderConfig.java | 23 ++++++ .../SmsTwoFactorAuthProviderConfig.java | 4 +- .../mfa/provider/TwoFactorAuthProvider.java | 2 +- .../provider/TwoFactorAuthProviderType.java | 3 +- .../impl/EmailTwoFactorAuthProvider.java | 67 ++++++++++++++++ .../impl/OtpBasedTwoFactorAuthProvider.java | 70 +++++++++++++++++ .../impl/SmsTwoFactorAuthProvider.java | 40 ++-------- .../impl/TotpTwoFactorAuthProvider.java | 3 +- .../auth/rest/RestAuthenticationProvider.java | 31 +++++--- ...RestAwareAuthenticationSuccessHandler.java | 42 +++------- .../security/model/token/JwtTokenFactory.java | 4 +- .../server/controller/TwoFactorAuthTest.java | 3 +- 20 files changed, 408 insertions(+), 126 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/MfaAuthenticationToken.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/EmailTwoFactorAuthAccountConfig.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/OtpBasedTwoFactorAuthAccountConfig.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/EmailTwoFactorAuthProviderConfig.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/OtpBasedTwoFactorAuthProviderConfig.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/EmailTwoFactorAuthProvider.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java index b6ad9626b1..bf8f2a7054 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -28,9 +28,10 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.service.security.auth.TokenOutdatingService; +import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig; @@ -43,24 +44,30 @@ import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; +import java.util.HashMap; +import java.util.Map; /* * * TODO [viacheslav]: - * - 2FA should be mandatory when logging in and must be rolled out to all existing users when 2FA is activated. - * - Rate limits should be implemented to protect against brute force leaked accounts to prevent SMS cost explosion. - * - Configurable softlock after XX (3) attempts: XX (15) mins - * - Configurable hardlock (user blocking) after a total of XX (10) unsuccessful attempts. - * - The OTP token should only be valid for XX (5) minutes. - * - Disable 2FA only possible after successful 2FA auth - it is possible with simple password resest - * - 2FA entries should be secured against code injection by code validation. - * - Email 2FA provider + * - Configurable softlock after XX (3) attempts: XX (15) mins - on session level + * - Configurable hardlock (user blocking) after a total of XX (10) unsuccessful attempts - on user level * * FIXME [viacheslav]: * - Tests for 2FA * - Swagger documentation * * */ +// TODO [viacheslav]: maybe get rid of sessionId concept.. + +/* + * + * + * TODO (later): + * - 2FA entries should be secured against code injection by code validation + * - ability to force users to use 2FA (maybe on log in, do not give them token pair but to give temporary + * token to configure 2FA account config); also will need to make users configure 2FA during activation and password setup... + * */ @RestController @RequestMapping("/api") @RequiredArgsConstructor @@ -122,7 +129,7 @@ public class TwoFactorAuthController extends BaseController { boolean verificationSuccess = twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), accountConfig.getProviderType(), (provider, providerConfig) -> { - return provider.checkVerificationCode(user, verificationCode, accountConfig); + return provider.checkVerificationCode(user, verificationCode, providerConfig, accountConfig); }); if (verificationSuccess) { @@ -146,32 +153,58 @@ public class TwoFactorAuthController extends BaseController { } + private final Map verificationCodeSendRateLimits = new HashMap<>(); + private final Map verificationCodeCheckRateLimits = new HashMap<>(); + + @PostMapping("/auth/2fa/verification/send") + @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") + public void sendTwoFaVerificationCode() throws Exception { + SecurityUser user = getCurrentUser(); + + TwoFactorAuthSettings twoFaSettings = twoFactorAuthService.getTwoFaSettings(user.getTenantId()).get(); + if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeSendRateLimit())) { + TbRateLimits rateLimits = verificationCodeSendRateLimits.computeIfAbsent(user.getSessionId(), sessionId -> { + return new TbRateLimits(twoFaSettings.getVerificationCodeSendRateLimit()); + }); + if (!rateLimits.tryConsume()) { + throw new ThingsboardException(ThingsboardErrorCode.TOO_MANY_REQUESTS); + } + } + + twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), user.getId(), + (provider, providerConfig, accountConfig) -> { + provider.prepareVerificationCode(user, providerConfig, accountConfig); + }); + } + @PostMapping("/auth/2fa/verification/check") @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") public JwtTokenPair checkTwoFaVerificationCode(@RequestParam String verificationCode) throws Exception { SecurityUser user = getCurrentUser(); + + + // FIXME [viacheslav]: rate limits for verification code check boolean verificationSuccess = twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), user.getId(), (provider, providerConfig, accountConfig) -> { - return provider.checkVerificationCode(user, verificationCode, accountConfig); + return provider.checkVerificationCode(user, verificationCode, providerConfig, accountConfig); }); + if (verificationSuccess) { return tokenFactory.createTokenPair(user); } else { + TwoFactorAuthSettings twoFaSettings = twoFactorAuthService.getTwoFaSettings(user.getTenantId()).get(); + if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeSendRateLimit())) { + TbRateLimits rateLimits = verificationCodeSendRateLimits.computeIfAbsent(user.getSessionId(), sessionId -> { + return new TbRateLimits(twoFaSettings.getVerificationCodeSendRateLimit()); + }); + if (!rateLimits.tryConsume()) { + throw new ThingsboardException(ThingsboardErrorCode.TOO_MANY_REQUESTS); + } + } throw new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.AUTHENTICATION); } } - @PostMapping("/auth/2fa/verification/resend") - @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") - public void resendTwoFaVerificationCode() throws Exception { - SecurityUser user = getCurrentUser(); - - twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), user.getId(), - (provider, providerConfig, accountConfig) -> { - provider.prepareVerificationCode(user, providerConfig, accountConfig); - }); - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/MfaAuthenticationToken.java b/application/src/main/java/org/thingsboard/server/service/security/auth/MfaAuthenticationToken.java new file mode 100644 index 0000000000..8c70e69179 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/MfaAuthenticationToken.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.security.auth; + +import org.thingsboard.server.service.security.model.SecurityUser; + +public class MfaAuthenticationToken extends AbstractJwtAuthenticationToken { + public MfaAuthenticationToken(SecurityUser securityUser) { + super(securityUser); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java index 580a12ce44..501517ca88 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java @@ -63,24 +63,6 @@ public class TwoFactorAuthService { protected static final String TWO_FACTOR_AUTH_SETTINGS_KEY = "twoFaSettings"; - @Autowired - private void setProviders(Collection> providers) { - providers.forEach(provider -> { - this.providers.put(provider.getType(), provider); - }); - } - - private Optional> getTwoFaProvider(TwoFactorAuthProviderType providerType) { - return Optional.of((TwoFactorAuthProvider) providers.get(providerType)); - } - - private Optional getTwoFaProviderConfig(TenantId tenantId, TwoFactorAuthProviderType providerType) { - return getTwoFaSettings(tenantId) - .flatMap(twoFaSettings -> twoFaSettings.getProviderConfig(providerType)) - .map(providerConfig -> (C) providerConfig); - } - - public R processByTwoFaProvider(TenantId tenantId, TwoFactorAuthProviderType providerType, ThrowingBiFunction, TwoFactorAuthProviderConfig, R> function) throws Exception { TwoFactorAuthProviderConfig providerConfig = getTwoFaProviderConfig(tenantId, providerType) .orElseThrow(() -> new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS)); @@ -182,4 +164,22 @@ public class TwoFactorAuthService { } } + + private Optional> getTwoFaProvider(TwoFactorAuthProviderType providerType) { + return Optional.of((TwoFactorAuthProvider) providers.get(providerType)); + } + + private Optional getTwoFaProviderConfig(TenantId tenantId, TwoFactorAuthProviderType providerType) { + return getTwoFaSettings(tenantId) + .flatMap(twoFaSettings -> twoFaSettings.getProviderConfig(providerType)) + .map(providerConfig -> (C) providerConfig); + } + + @Autowired + private void setProviders(Collection> providers) { + providers.forEach(provider -> { + this.providers.put(provider.getType(), provider); + }); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java index a0620ec96e..8b8927f721 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java @@ -16,20 +16,33 @@ package org.thingsboard.server.service.security.auth.mfa.config; import lombok.Data; +import org.checkerframework.checker.index.qual.NonNegative; import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; import javax.validation.Valid; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; import java.util.List; import java.util.Optional; @Data public class TwoFactorAuthSettings { - private boolean useSystemTwoFactorAuthSettings; + @NotNull + private Boolean useSystemTwoFactorAuthSettings; @Valid private List providers; + @Pattern(regexp = "\\d+:\\d+") + private String verificationCodeSendRateLimit; // 1:60 - one time in a minute + @Pattern(regexp = "\\d+:\\d+") + private String verificationCodeCheckRateLimit; // soft lockout, on session level + @Min(0) + private Integer maxVerificationCodeSubmitAttemptsBeforeUserBlocking; + + public Optional getProviderConfig(TwoFactorAuthProviderType providerType) { return Optional.ofNullable(providers) .flatMap(providersConfigs -> providersConfigs.stream() diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/EmailTwoFactorAuthAccountConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/EmailTwoFactorAuthAccountConfig.java new file mode 100644 index 0000000000..c18e4c41c1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/EmailTwoFactorAuthAccountConfig.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.security.auth.mfa.config.account; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; + +@EqualsAndHashCode(callSuper = true) +@Data +public class EmailTwoFactorAuthAccountConfig extends OtpBasedTwoFactorAuthAccountConfig { + + private boolean useAccountEmail; // TODO [viacheslav]: validate + private String email; + + @Override + public TwoFactorAuthProviderType getProviderType() { + return TwoFactorAuthProviderType.EMAIL; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/OtpBasedTwoFactorAuthAccountConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/OtpBasedTwoFactorAuthAccountConfig.java new file mode 100644 index 0000000000..80b832a831 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/OtpBasedTwoFactorAuthAccountConfig.java @@ -0,0 +1,19 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.security.auth.mfa.config.account; + +public abstract class OtpBasedTwoFactorAuthAccountConfig implements TwoFactorAuthAccountConfig { +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java index 6f3ef06775..82e3760e35 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java @@ -16,12 +16,14 @@ package org.thingsboard.server.service.security.auth.mfa.config.account; import lombok.Data; +import lombok.EqualsAndHashCode; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; import javax.validation.constraints.NotBlank; +@EqualsAndHashCode(callSuper = true) @Data -public class SmsTwoFactorAuthAccountConfig implements TwoFactorAuthAccountConfig { +public class SmsTwoFactorAuthAccountConfig extends OtpBasedTwoFactorAuthAccountConfig { @NotBlank private String phoneNumber; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/EmailTwoFactorAuthProviderConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/EmailTwoFactorAuthProviderConfig.java new file mode 100644 index 0000000000..a782f73a68 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/EmailTwoFactorAuthProviderConfig.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.security.auth.mfa.config.provider; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; + +@EqualsAndHashCode(callSuper = true) +@Data +public class EmailTwoFactorAuthProviderConfig extends OtpBasedTwoFactorAuthProviderConfig{ + + private String emailVerificationMessageTemplate; // FIXME [viacheslav]: + + @Override + public TwoFactorAuthProviderType getProviderType() { + return TwoFactorAuthProviderType.EMAIL; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/OtpBasedTwoFactorAuthProviderConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/OtpBasedTwoFactorAuthProviderConfig.java new file mode 100644 index 0000000000..c7169def48 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/OtpBasedTwoFactorAuthProviderConfig.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.security.auth.mfa.config.provider; + +import lombok.Data; + +@Data +public abstract class OtpBasedTwoFactorAuthProviderConfig implements TwoFactorAuthProviderConfig { + private Integer verificationCodeLifetime; // seconds +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java index 2d15a07ad1..3fe9e77ce7 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java @@ -16,13 +16,15 @@ package org.thingsboard.server.service.security.auth.mfa.config.provider; import lombok.Data; +import lombok.EqualsAndHashCode; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Pattern; +@EqualsAndHashCode(callSuper = true) @Data -public class SmsTwoFactorAuthProviderConfig implements TwoFactorAuthProviderConfig { +public class SmsTwoFactorAuthProviderConfig extends OtpBasedTwoFactorAuthProviderConfig { @NotBlank @Pattern(regexp = ".*\\$\\{verificationCode}.*", message = "template must contain verification code") diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java index 011404268c..858b5995f7 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java @@ -27,7 +27,7 @@ public interface TwoFactorAuthProvider { + + private final MailService mailService; + + protected EmailTwoFactorAuthProvider(CacheManager cacheManager, MailService mailService) { + super(cacheManager); + this.mailService = mailService; + } + + + @Override + public EmailTwoFactorAuthAccountConfig generateNewAccountConfig(User user, EmailTwoFactorAuthProviderConfig providerConfig) { + EmailTwoFactorAuthAccountConfig accountConfig = new EmailTwoFactorAuthAccountConfig(); + accountConfig.setUseAccountEmail(true); + return accountConfig; + } + + @Override + protected void sendVerificationCode(SecurityUser user, String verificationCode, EmailTwoFactorAuthProviderConfig providerConfig, EmailTwoFactorAuthAccountConfig accountConfig) throws ThingsboardException { + String email; + if (accountConfig.isUseAccountEmail()) { + email = user.getEmail(); + } else { + email = accountConfig.getEmail(); + } + + // FIXME [viacheslav]: mail template for 2FA verification + mailService.sendEmail(user.getTenantId(), email, "subject", ""); + } + + + @Override + public TwoFactorAuthProviderType getType() { + return TwoFactorAuthProviderType.EMAIL; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java new file mode 100644 index 0000000000..f8d07aedb2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java @@ -0,0 +1,70 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.security.auth.mfa.provider.impl; + +import lombok.Data; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.service.security.auth.mfa.config.account.OtpBasedTwoFactorAuthAccountConfig; +import org.thingsboard.server.service.security.auth.mfa.config.provider.OtpBasedTwoFactorAuthProviderConfig; +import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProvider; +import org.thingsboard.server.service.security.model.SecurityUser; + +import java.util.concurrent.TimeUnit; + +public abstract class OtpBasedTwoFactorAuthProvider implements TwoFactorAuthProvider { + + private final Cache verificationCodesCache; + + protected OtpBasedTwoFactorAuthProvider(CacheManager cacheManager) { + this.verificationCodesCache = cacheManager.getCache(CacheConstants.TWO_FA_VERIFICATION_CODES_CACHE); + } + + + @Override + public final void prepareVerificationCode(SecurityUser user, C providerConfig, A accountConfig) throws ThingsboardException { + String verificationCode = RandomStringUtils.randomNumeric(6); + verificationCodesCache.put(user.getSessionId(), new Otp(System.currentTimeMillis(), verificationCode)); + + sendVerificationCode(user, verificationCode, providerConfig, accountConfig); + } + + protected abstract void sendVerificationCode(SecurityUser user, String verificationCode, C providerConfig, A accountConfig) throws ThingsboardException; + + + @Override + public final boolean checkVerificationCode(SecurityUser user, String verificationCode, C providerConfig, A accountConfig) { + Otp correctVerificationCode = verificationCodesCache.get(user.getSessionId(), Otp.class); + if (correctVerificationCode != null && verificationCode.equals(correctVerificationCode.getValue())) { + if (System.currentTimeMillis() - correctVerificationCode.getTimestamp() <= TimeUnit.SECONDS.toMillis(providerConfig.getVerificationCodeLifetime())) { + verificationCodesCache.evict(user.getSessionId()); + return true; + } + } + return false; + } + + + @Data + private static class Otp { + private final long timestamp; + private final String value; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFactorAuthProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFactorAuthProvider.java index c4b51ab16a..e633a0afd1 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFactorAuthProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFactorAuthProvider.java @@ -15,21 +15,15 @@ */ package org.thingsboard.server.service.security.auth.mfa.provider.impl; -import lombok.Data; -import lombok.SneakyThrows; -import org.apache.commons.lang3.RandomStringUtils; -import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.util.TbNodeUtils; -import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.mfa.config.account.SmsTwoFactorAuthAccountConfig; import org.thingsboard.server.service.security.auth.mfa.config.provider.SmsTwoFactorAuthProviderConfig; -import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProvider; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; import org.thingsboard.server.service.security.model.SecurityUser; @@ -37,14 +31,13 @@ import java.util.Map; @Service @TbCoreComponent -public class SmsTwoFactorAuthProvider implements TwoFactorAuthProvider { +public class SmsTwoFactorAuthProvider extends OtpBasedTwoFactorAuthProvider { private final SmsService smsService; - private final Cache verificationCodesCache; public SmsTwoFactorAuthProvider(SmsService smsService, CacheManager cacheManager) { + super(cacheManager); this.smsService = smsService; - this.verificationCodesCache = cacheManager.getCache(CacheConstants.TWO_FA_VERIFICATION_CODES_CACHE); } @@ -54,42 +47,21 @@ public class SmsTwoFactorAuthProvider implements TwoFactorAuthProvider data = Map.of( + protected void sendVerificationCode(SecurityUser user, String verificationCode, SmsTwoFactorAuthProviderConfig providerConfig, SmsTwoFactorAuthAccountConfig accountConfig) throws ThingsboardException { + Map messageData = Map.of( "verificationCode", verificationCode, "userEmail", user.getEmail() ); - String message = TbNodeUtils.processTemplate(providerConfig.getSmsVerificationMessageTemplate(), data); + String message = TbNodeUtils.processTemplate(providerConfig.getSmsVerificationMessageTemplate(), messageData); + String phoneNumber = accountConfig.getPhoneNumber(); smsService.sendSms(user.getTenantId(), user.getCustomerId(), new String[]{phoneNumber}, message); } - @Override - public boolean checkVerificationCode(SecurityUser user, String verificationCode, SmsTwoFactorAuthAccountConfig accountConfig) { - VerificationCode correctVerificationCode = verificationCodesCache.get(user.getSessionId(), VerificationCode.class); - if (correctVerificationCode != null && verificationCode.equals(correctVerificationCode.getValue())) { - verificationCodesCache.evict(user.getSessionId()); - return true; - } else { - return false; - } - } @Override public TwoFactorAuthProviderType getType() { return TwoFactorAuthProviderType.SMS; } - - @Data - private static class VerificationCode { - private final long timestamp; - private final String value; - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFactorAuthProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFactorAuthProvider.java index d7747521c1..65574fc760 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFactorAuthProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFactorAuthProvider.java @@ -45,7 +45,7 @@ public class TotpTwoFactorAuthProvider implements TwoFactorAuthProvider twoFaAccountConfig = twoFactorAuthService.getTwoFaAccountConfig(securityUser.getTenantId(), securityUser.getId()); - if (twoFaAccountConfig.isPresent()) { - try { - twoFactorAuthService.processByTwoFaProvider(securityUser.getTenantId(), twoFaAccountConfig.get().getProviderType(), - (provider, providerConfig) -> { - provider.prepareVerificationCode(securityUser, providerConfig, twoFaAccountConfig.get()); - }); - JwtTokenPair tokenPair = new JwtTokenPair(); - tokenPair.setToken(tokenFactory.createPreVerificationToken(securityUser).getToken()); - tokenPair.setScope(Authority.PRE_VERIFICATION_TOKEN); - return tokenPair; - } catch (Exception e) { - // TODO [viacheslav]: write audit log - log.error("Failed to process 2FA for user {}. Falling back to plain auth", securityUser.getId(), e); - } - } - return tokenFactory.createTokenPair(securityUser); - } - /** * Removes temporary authentication-related data which may have been stored * in the session during the authentication process.. diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index b2bc3add26..36f64b3566 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -166,8 +166,8 @@ public class JwtTokenFactory { return securityUser; } - public JwtToken createPreVerificationToken(SecurityUser user) { - String token = setUpToken(user, Collections.singletonList(Authority.PRE_VERIFICATION_TOKEN.name()), settings.getPreVerificationTokenExpirationTime()) + public JwtToken createTwoFaPreVerificationToken(SecurityUser user, Integer expirationTime) { + String token = setUpToken(user, Collections.singletonList(Authority.PRE_VERIFICATION_TOKEN.name()), expirationTime) .claim(TENANT_ID, user.getTenantId().toString()) .compact(); return new AccessJwtToken(token); diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java index fcc2c8bcdf..a12c7ec0e8 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java @@ -25,7 +25,6 @@ import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; -import org.thingsboard.server.service.security.auth.mfa.config.account.SmsTwoFactorAuthAccountConfig; import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig; import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; import org.thingsboard.server.service.security.auth.mfa.config.provider.SmsTwoFactorAuthProviderConfig; @@ -40,12 +39,12 @@ import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +// TODO [viacheslav]: test sessionId public abstract class TwoFactorAuthTest extends AbstractControllerTest { @SpyBean From c2f4724ca26e293811fd36827b5ad1b6c919cde5 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Sat, 19 Mar 2022 10:08:53 +0200 Subject: [PATCH 007/276] Ability to define component form for advanced widget settings and widget data keys --- .../add-widget-dialog.component.ts | 4 +- .../dashboard-page/edit-widget.component.ts | 4 +- .../home/components/home-components.module.ts | 5 + .../data-key-config-dialog.component.html | 1 + .../data-key-config-dialog.component.ts | 1 + .../widget/data-key-config.component.html | 5 +- .../widget/data-key-config.component.ts | 13 +- .../components/widget/data-keys.component.ts | 4 + .../qrcode-widget-settings.component.html | 36 ++++ .../qrcode-widget-settings.component.ts | 67 ++++++ .../lib/settings/widget-settings.module.ts | 42 ++++ .../widget/widget-component.service.ts | 2 + .../widget/widget-config.component.html | 7 +- .../widget/widget-config.component.ts | 7 +- .../widget/widget-settings.component.html | 24 +++ .../widget/widget-settings.component.scss | 22 ++ .../widget/widget-settings.component.ts | 201 ++++++++++++++++++ .../home/models/widget-component.models.ts | 6 + .../pages/widget/widget-editor.component.html | 12 ++ ui-ngx/src/app/shared/models/widget.models.ts | 127 ++++++++++- .../assets/locale/locale.constant-en_US.json | 11 +- 21 files changed, 587 insertions(+), 14 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/widget-settings.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/widget-settings.component.ts diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts index 8e8745dd20..2e69f4adfd 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts @@ -91,7 +91,9 @@ export class AddWidgetDialogComponent extends DialogComponent
- - +
diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts index bb9290a1a1..03d72101f1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts @@ -72,6 +72,9 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con @Input() dataKeySettingsSchema: any; + @Input() + dataKeySettingsDirective: string; + @Input() showPostProcessing = true; @@ -121,11 +124,15 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con type: DataKeyType.alarm }); } - if (this.dataKeySettingsSchema && this.dataKeySettingsSchema.schema) { + if (this.dataKeySettingsSchema && this.dataKeySettingsSchema.schema || + this.dataKeySettingsDirective && this.dataKeySettingsDirective.length) { this.displayAdvanced = true; this.dataKeySettingsData = { - schema: this.dataKeySettingsSchema.schema, - form: this.dataKeySettingsSchema.form || ['*'] + schema: this.dataKeySettingsSchema?.schema || { + type: 'object', + properties: {} + }, + form: this.dataKeySettingsSchema?.form || ['*'] }; this.dataKeySettingsFormGroup = this.fb.group({ settings: [null, []] diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts index 781c30b02f..63e72905e7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts @@ -113,6 +113,9 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie @Input() datakeySettingsSchema: any; + @Input() + dataKeySettingsDirective: string; + @Input() callbacks: DataKeysCallbacks; @@ -395,6 +398,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie data: { dataKey: deepClone(key), dataKeySettingsSchema: this.datakeySettingsSchema, + dataKeySettingsDirective: this.dataKeySettingsDirective, entityAliasId: this.entityAliasId, showPostProcessing: this.widgetType !== widgetType.alarm, callbacks: this.callbacks diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.html new file mode 100644 index 0000000000..6efcc3611b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.html @@ -0,0 +1,36 @@ + +
+ + {{ 'widgets.qr-code.use-qr-code-text-function' | translate }} + + + widgets.qr-code.qr-code-text-pattern + + + {{ 'widgets.qr-code.qr-code-text-pattern-required' | translate }} + + +
+ + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.ts new file mode 100644 index 0000000000..c9ed32b85a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.ts @@ -0,0 +1,67 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + + +@Component({ + selector: 'tb-qrcode-widget-settings', + templateUrl: './qrcode-widget-settings.component.html', + styleUrls: [] +}) +export class QrCodeWidgetSettingsComponent extends WidgetSettingsComponent { + + qrCodeWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.qrCodeWidgetSettingsForm; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.qrCodeWidgetSettingsForm = this.fb.group({ + qrCodeTextPattern: [settings ? settings.qrCodeTextPattern : '${entityName}', []], + useQrCodeTextFunction: [settings ? settings.useQrCodeTextFunction : false, []], + qrCodeTextFunction: [settings ? settings.qrCodeTextFunction : 'return data[0][\'entityName\'];', []] + }); + } + + protected validatorTriggers(): string[] { + return ['useQrCodeTextFunction']; + } + + protected updateValidators(emitEvent: boolean) { + const useQrCodeTextFunction: boolean = this.qrCodeWidgetSettingsForm.get('useQrCodeTextFunction').value; + if (useQrCodeTextFunction) { + this.qrCodeWidgetSettingsForm.get('qrCodeTextPattern').setValidators([]); + this.qrCodeWidgetSettingsForm.get('qrCodeTextFunction').setValidators([Validators.required]); + } else { + this.qrCodeWidgetSettingsForm.get('qrCodeTextPattern').setValidators([Validators.required]); + this.qrCodeWidgetSettingsForm.get('qrCodeTextFunction').setValidators([]); + } + this.qrCodeWidgetSettingsForm.get('qrCodeTextPattern').updateValueAndValidity({emitEvent}); + this.qrCodeWidgetSettingsForm.get('qrCodeTextFunction').updateValueAndValidity({emitEvent}); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts new file mode 100644 index 0000000000..7a10d51465 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -0,0 +1,42 @@ +/// +/// Copyright © 2016-2022 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 { NgModule, Type } from '@angular/core'; +import { QrCodeWidgetSettingsComponent } from '@home/components/widget/lib/settings/qrcode-widget-settings.component'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { SharedHomeComponentsModule } from '@home/components/shared-home-components.module'; +import { IWidgetSettingsComponent } from '@shared/models/widget.models'; + +@NgModule({ + declarations: [ + QrCodeWidgetSettingsComponent + ], + imports: [ + CommonModule, + SharedModule, + SharedHomeComponentsModule + ], + exports: [ + QrCodeWidgetSettingsComponent + ] +}) +export class WidgetSettingsModule { +} + +export const widgetSettingsComponentsMap: {[key: string]: Type} = { + 'tb-qrcode-widget-settings': QrCodeWidgetSettingsComponent +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts index de88b73e3b..851854e331 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts @@ -107,6 +107,8 @@ export class WidgetComponentService { controllerScript: this.utils.editWidgetInfo.controllerScript, settingsSchema: this.utils.editWidgetInfo.settingsSchema, dataKeySettingsSchema: this.utils.editWidgetInfo.dataKeySettingsSchema, + settingsDirective: this.utils.editWidgetInfo.settingsDirective, + dataKeySettingsDirective: this.utils.editWidgetInfo.dataKeySettingsDirective, defaultConfig: this.utils.editWidgetInfo.defaultConfig }, new WidgetTypeId('1'), new TenantId( NULL_UUID ), 'customWidgetBundle', undefined ); diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index bb4ff8503e..9bfbcc71a9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -190,6 +190,7 @@ [optDataKeys]="modelValue?.typeParameters?.dataKeysOptional" [aliasController]="aliasController" [datakeySettingsSchema]="modelValue?.dataKeySettingsSchema" + [dataKeySettingsDirective]="modelValue?.dataKeySettingsDirective" [callbacks]="widgetConfigCallbacks" [entityAliasId]="datasourceControl.get('entityAliasId').value" [formControl]="datasourceControl.get('dataKeys')"> @@ -283,6 +284,7 @@ [datasourceType]="alarmSourceSettings.get('type').value" [aliasController]="aliasController" [datakeySettingsSchema]="modelValue?.dataKeySettingsSchema" + [dataKeySettingsDirective]="modelValue?.dataKeySettingsDirective" [callbacks]="widgetConfigCallbacks" [entityAliasId]="alarmSourceSettings.get('entityAliasId').value" formControlName="dataKeys"> @@ -476,9 +478,10 @@
- - +
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index c34698bafd..b3325e0de0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -605,7 +605,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont widgetSettingsFormData.schema = deepClone(emptySettingsSchema); widgetSettingsFormData.form = deepClone(defaultSettingsForm); widgetSettingsFormData.groupInfoes = deepClone(emptySettingsGroupInfoes); - widgetSettingsFormData.model = {}; + widgetSettingsFormData.model = settings || {}; } this.advancedSettings.patchValue({ settings: widgetSettingsFormData }, {emitEvent: false}); } @@ -713,7 +713,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont } public displayAdvanced(): boolean { - return !!this.modelValue && !!this.modelValue.settingsSchema && !!this.modelValue.settingsSchema.schema; + return !!this.modelValue && (!!this.modelValue.settingsSchema && !!this.modelValue.settingsSchema.schema || + !!this.modelValue.settingsDirective && !!this.modelValue.settingsDirective.length); } public dndDatasourceMoved(index: number) { @@ -913,7 +914,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont valid: false } }; - } else if (!this.advancedSettings.valid) { + } else if (!this.advancedSettings.valid || (this.displayAdvanced() && !this.modelValue.config.settings)) { return { advancedSettings: { valid: false diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.html new file mode 100644 index 0000000000..a9742c0545 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.html @@ -0,0 +1,24 @@ + +
+ +
{{definedDirectiveError}}
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.scss new file mode 100644 index 0000000000..2afac0944b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.scss @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2022 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. + */ +:host { + .tb-settings-directive-error { + font-size: 13px; + font-weight: 400; + color: rgb(221, 44, 0); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.ts new file mode 100644 index 0000000000..7b5e3f1b0f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.ts @@ -0,0 +1,201 @@ +/// +/// Copyright © 2016-2022 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 { + AfterViewInit, + Component, ComponentFactoryResolver, + ComponentRef, + forwardRef, + Input, + OnDestroy, + OnInit, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { + IRuleNodeConfigurationComponent, + RuleNodeConfiguration, + RuleNodeDefinition +} from '@shared/models/rule-node.models'; +import { Subscription } from 'rxjs'; +import { RuleChainService } from '@core/http/rule-chain.service'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { TranslateService } from '@ngx-translate/core'; +import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component'; +import { deepClone } from '@core/utils'; +import { RuleChainType } from '@shared/models/rule-chain.models'; +import { JsonFormComponent } from '@shared/components/json-form/json-form.component'; +import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; +import { IWidgetSettingsComponent, WidgetSettings } from '@shared/models/widget.models'; +import { widgetSettingsComponentsMap } from '@home/components/widget/lib/settings/widget-settings.module'; + +@Component({ + selector: 'tb-widget-settings', + templateUrl: './widget-settings.component.html', + styleUrls: ['./widget-settings.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => WidgetSettingsComponent), + multi: true + }] +}) +export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, OnDestroy, AfterViewInit { + + @ViewChild('definedSettingsContent', {read: ViewContainerRef, static: true}) definedSettingsContainer: ViewContainerRef; + + @ViewChild('jsonFormComponent') jsonFormComponent: JsonFormComponent; + + @Input() + disabled: boolean; + + settingsDirectiveValue: string; + + @Input() + set settingsDirective(settingsDirective: string) { + if (this.settingsDirectiveValue !== settingsDirective) { + this.settingsDirectiveValue = settingsDirective; + if (this.settingsDirectiveValue) { + this.validateDefinedDirective(); + } + } + } + + get settingsDirective(): string { + return this.settingsDirectiveValue; + } + + definedDirectiveError: string; + + widgetSettingsFormGroup: FormGroup; + + changeSubscription: Subscription; + + private definedSettingsComponentRef: ComponentRef; + private definedSettingsComponent: IWidgetSettingsComponent; + + private widgetSettingsFormData: JsonFormComponentData; + + private propagateChange = (v: any) => { }; + + constructor(private translate: TranslateService, + private cfr: ComponentFactoryResolver, + private fb: FormBuilder) { + this.widgetSettingsFormGroup = this.fb.group({ + settings: [null, Validators.required] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit(): void { + } + + ngOnDestroy(): void { + if (this.definedSettingsComponentRef) { + this.definedSettingsComponentRef.destroy(); + } + } + + ngAfterViewInit(): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.widgetSettingsFormGroup.disable({emitEvent: false}); + } else { + this.widgetSettingsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: JsonFormComponentData): void { + this.widgetSettingsFormData = value; + if (this.changeSubscription) { + this.changeSubscription.unsubscribe(); + this.changeSubscription = null; + } + if (this.definedSettingsComponent) { + this.definedSettingsComponent.settings = this.widgetSettingsFormData.model; + this.changeSubscription = this.definedSettingsComponent.settingsChanged.subscribe((settings) => { + this.updateModel(settings); + }); + } else { + this.widgetSettingsFormGroup.get('settings').patchValue(this.widgetSettingsFormData, {emitEvent: false}); + this.changeSubscription = this.widgetSettingsFormGroup.get('settings').valueChanges.subscribe( + (widgetSettingsFormData: JsonFormComponentData) => { + this.updateModel(widgetSettingsFormData.model); + } + ); + } + } + + useDefinedDirective(): boolean { + return this.settingsDirective && + this.settingsDirective.length && !this.definedDirectiveError; + } + + useJsonForm(): boolean { + return !this.settingsDirective || !this.settingsDirective.length; + } + + private updateModel(settings: WidgetSettings) { + this.widgetSettingsFormData.model = settings; + if (this.definedSettingsComponent || this.widgetSettingsFormGroup.valid) { + this.propagateChange(this.widgetSettingsFormData); + } else { + this.propagateChange(null); + } + } + + private validateDefinedDirective() { + if (this.definedSettingsComponentRef) { + this.definedSettingsComponentRef.destroy(); + this.definedSettingsComponentRef = null; + } + if (this.settingsDirective && this.settingsDirective.length) { + const componentType = widgetSettingsComponentsMap[this.settingsDirective]; + if (!componentType) { + this.definedDirectiveError = this.translate.instant('widget-config.settings-component-not-found', + {selector: this.settingsDirective}); + } else { + if (this.changeSubscription) { + this.changeSubscription.unsubscribe(); + this.changeSubscription = null; + } + this.definedSettingsContainer.clear(); + const factory = this.cfr.resolveComponentFactory(componentType); + this.definedSettingsComponentRef = this.definedSettingsContainer.createComponent(factory); + this.definedSettingsComponent = this.definedSettingsComponentRef.instance; + this.definedSettingsComponent.settings = this.widgetSettingsFormData?.model; + this.changeSubscription = this.definedSettingsComponent.settingsChanged.subscribe((settings) => { + this.updateModel(settings); + }); + } + } + } + + validate() { + if (this.useDefinedDirective()) { + this.definedSettingsComponent.validate(); + } + } +} diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 3bb727f4f5..49944cb0e5 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -424,6 +424,8 @@ export interface WidgetConfigComponentData { isDataEnabled: boolean; settingsSchema: JsonSettingsSchema; dataKeySettingsSchema: JsonSettingsSchema; + settingsDirective: string; + dataKeySettingsDirective: string; } export const MissingWidgetType: WidgetInfo = { @@ -510,6 +512,8 @@ export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo { controllerScript: widgetTypeEntity.descriptor.controllerScript, settingsSchema: widgetTypeEntity.descriptor.settingsSchema, dataKeySettingsSchema: widgetTypeEntity.descriptor.dataKeySettingsSchema, + settingsDirective: widgetTypeEntity.descriptor.settingsDirective, + dataKeySettingsDirective: widgetTypeEntity.descriptor.dataKeySettingsDirective, defaultConfig: widgetTypeEntity.descriptor.defaultConfig }; } @@ -536,6 +540,8 @@ export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: controllerScript: widgetInfo.controllerScript, settingsSchema: widgetInfo.settingsSchema, dataKeySettingsSchema: widgetInfo.dataKeySettingsSchema, + settingsDirective: widgetInfo.settingsDirective, + dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective, defaultConfig: widgetInfo.defaultConfig }; return { diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html index f3aa24f65c..ba0ded66cf 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html @@ -237,6 +237,18 @@ rows="2" maxlength="255"> {{descriptionInput.value?.length || 0}}/255 + + widget.settings-form-selector + + + + widget.data-key-settings-form-selector + +
diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 703cffcaee..45c8943753 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -25,6 +25,14 @@ import { EntityId } from '@shared/models/id/entity-id'; import * as moment_ from 'moment'; import { EntityDataPageLink, EntityFilter, KeyFilter } from '@shared/models/query/query.models'; import { PopoverPlacement } from '@shared/components/popover.models'; +import { PageComponent } from '@shared/components/page.component'; +import { AfterViewInit, Directive, EventEmitter, Inject, OnInit } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { AbstractControl, FormGroup } from '@angular/forms'; +import {RuleChainType} from "@shared/models/rule-chain.models"; +import {Observable} from "rxjs"; +import {RuleNodeConfiguration} from "@shared/models/rule-node.models"; export enum widgetType { timeseries = 'timeseries', @@ -143,6 +151,8 @@ export interface WidgetTypeDescriptor { controllerScript: string; settingsSchema?: string | any; dataKeySettingsSchema?: string | any; + settingsDirective?: string; + dataKeySettingsDirective?: string; defaultConfig: string; sizeX: number; sizeY: number; @@ -159,6 +169,7 @@ export interface WidgetTypeParameters { singleEntity?: boolean; warnOnPageDataOverflow?: boolean; ignoreDataUpdateOnIntervalTick?: boolean; + } export interface WidgetControllerDescriptor { @@ -483,6 +494,10 @@ export interface WidgetComparisonSettings { comparisonCustomIntervalValue?: number; } +export interface WidgetSettings { + [key: string]: any; +} + export interface WidgetConfig { title?: string; titleIcon?: string; @@ -512,7 +527,7 @@ export interface WidgetConfig { decimals?: number; noDataDisplayMessage?: string; actions?: {[actionSourceId: string]: Array}; - settings?: any; + settings?: WidgetSettings; alarmSource?: Datasource; alarmStatusList?: AlarmSearchStatus[]; alarmSeverityList?: AlarmSeverity[]; @@ -570,3 +585,113 @@ export interface WidgetSize { sizeX: number; sizeY: number; } + +export interface IWidgetSettingsComponent { + settings: WidgetSettings; + settingsChanged: Observable; + validate(); + [key: string]: any; +} + +@Directive() +// tslint:disable-next-line:directive-class-suffix +export abstract class WidgetSettingsComponent extends PageComponent implements + IWidgetSettingsComponent, OnInit, AfterViewInit { + + settingsValue: WidgetSettings; + + private settingsSet = false; + + set settings(value: WidgetSettings) { + this.settingsValue = value; + if (!this.settingsSet) { + this.settingsSet = true; + this.setupSettings(value); + } else { + this.updateSettings(value); + } + } + + get settings(): WidgetSettings { + return this.settingsValue; + } + + settingsChangedEmitter = new EventEmitter(); + settingsChanged = this.settingsChangedEmitter.asObservable(); + + protected constructor(@Inject(Store) protected store: Store) { + super(store); + } + + ngOnInit() {} + + ngAfterViewInit(): void { + setTimeout(() => { + if (!this.validateSettings()) { + this.settingsChangedEmitter.emit(null); + } + }, 0); + } + + validate() { + this.onValidate(); + } + + protected setupSettings(settings: WidgetSettings) { + this.onSettingsSet(this.prepareInputSettings(settings)); + this.updateValidators(false); + for (const trigger of this.validatorTriggers()) { + const path = trigger.split('.'); + let control: AbstractControl = this.settingsForm(); + for (const part of path) { + control = control.get(part); + } + control.valueChanges.subscribe(() => { + this.updateValidators(true, trigger); + }); + } + this.settingsForm().valueChanges.subscribe((updated: WidgetSettings) => { + this.onSettingsChanged(updated); + }); + } + + protected updateSettings(settings: WidgetSettings) { + this.settingsForm().reset(this.prepareInputSettings(settings), {emitEvent: false}); + this.updateValidators(false); + } + + protected updateValidators(emitEvent: boolean, trigger?: string) { + } + + protected validatorTriggers(): string[] { + return []; + } + + protected onSettingsChanged(updated: WidgetSettings) { + this.settingsValue = updated; + if (this.validateSettings()) { + this.settingsChangedEmitter.emit(this.prepareOutputSettings(updated)); + } else { + this.settingsChangedEmitter.emit(null); + } + } + + protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { + return settings; + } + + protected prepareOutputSettings(settings: WidgetSettings): WidgetSettings { + return settings; + } + + protected validateSettings(): boolean { + return this.settingsForm().valid; + } + + protected onValidate() {} + + protected abstract settingsForm(): FormGroup; + + protected abstract onSettingsSet(settings: WidgetSettings); + +} diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index aab433c992..2ee01bb553 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2985,6 +2985,8 @@ "widget-settings": "Widget settings", "description": "Description", "image-preview": "Image preview", + "settings-form-selector": "Settings form selector", + "data-key-settings-form-selector": "Data key settings form selector", "javascript": "Javascript", "js": "JS", "remove-widget-type-title": "Are you sure you want to remove the widget type '{{widgetName}}'?", @@ -3151,7 +3153,8 @@ "icon-size": "Icon size", "advanced-settings": "Advanced settings", "data-settings": "Data settings", - "no-data-display-message": "\"No data to display\" alternative message" + "no-data-display-message": "\"No data to display\" alternative message", + "settings-component-not-found": "Settings form component not found for selector '{{selector}}'" }, "widget-type": { "import": "Import widget type", @@ -3266,6 +3269,12 @@ "value": "Value" }, "invalid-qr-code-text": "Invalid input text for QR code. Input should have a string type", + "qr-code": { + "use-qr-code-text-function": "Use QR code text function", + "qr-code-text-pattern": "QR code text pattern (for ex. '${entityName} | ${keyName} - some text.')", + "qr-code-text-pattern-required": "QR code text pattern is required.", + "qr-code-text-function": "QR code text function" + }, "persistent-table": { "rpc-id": "RPC ID", "message-type": "Message type", From 0c36d4809c0e4d4cf7c680d839b42f5ed4f8d42b Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Sat, 19 Mar 2022 19:34:45 +0200 Subject: [PATCH 008/276] 2FA: rate limiting, validation, refactoring --- .../config/RateLimitProcessingFilter.java | 3 + .../TwoFactorAuthConfigController.java | 124 +++++++++++++ .../controller/TwoFactorAuthController.java | 142 +-------------- .../auth/mfa/DefaultTwoFactorAuthService.java | 151 ++++++++++++++++ .../auth/mfa/TwoFactorAuthService.java | 163 +----------------- .../DefaultTwoFactorAuthConfigManager.java | 139 +++++++++++++++ .../config/TwoFactorAuthConfigManager.java | 41 +++++ .../mfa/config/TwoFactorAuthSettings.java | 20 ++- .../EmailTwoFactorAuthAccountConfig.java | 13 +- .../SmsTwoFactorAuthAccountConfig.java | 2 + .../account/TwoFactorAuthAccountConfig.java | 6 +- .../OtpBasedTwoFactorAuthProviderConfig.java | 7 +- .../provider/TwoFactorAuthProviderConfig.java | 7 +- .../impl/OtpBasedTwoFactorAuthProvider.java | 14 +- .../auth/rest/RestAuthenticationProvider.java | 10 +- ...RestAwareAuthenticationSuccessHandler.java | 10 +- .../system/DefaultSystemSecurityService.java | 54 ++++-- .../system/SystemSecurityService.java | 4 + .../server/controller/TwoFactorAuthTest.java | 1 + .../server/dao/user/UserServiceImpl.java | 2 +- 20 files changed, 583 insertions(+), 330 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFactorAuthConfigManager.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthConfigManager.java diff --git a/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java b/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java index e2e6f8d982..2756b9a647 100644 --- a/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java +++ b/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java @@ -15,8 +15,11 @@ */ package org.thingsboard.server.config; +import io.github.bucket4j.Bucket4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.Cache; +import org.springframework.cache.jcache.JCacheCacheManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java new file mode 100644 index 0000000000..12f0824ff1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java @@ -0,0 +1,124 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.controller; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.client.j2se.MatrixToImageWriter; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.QRCodeWriter; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager; +import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; +import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig; +import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; +import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; +import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; +import org.thingsboard.server.service.security.model.SecurityUser; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; + +@RestController +@RequestMapping("/api/2fa") +@RequiredArgsConstructor +public class TwoFactorAuthConfigController extends BaseController { + + private final TwoFactorAuthConfigManager twoFactorAuthConfigManager; + private final TwoFactorAuthService twoFactorAuthService; + + + @GetMapping("/account/config") + @PreAuthorize("isAuthenticated()") + public TwoFactorAuthAccountConfig getTwoFaAccountConfig() throws ThingsboardException { + SecurityUser user = getCurrentUser(); + return twoFactorAuthConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId()).orElse(null); + } + + @PostMapping("/account/config/generate") + @PreAuthorize("isAuthenticated()") + public TwoFactorAuthAccountConfig generateTwoFaAccountConfig(@RequestParam TwoFactorAuthProviderType providerType) throws Exception { + SecurityUser user = getCurrentUser(); + return twoFactorAuthService.generateNewAccountConfig(user, providerType); + } + + /* TMP */ + @PostMapping("/account/config/generate/qr") + @PreAuthorize("isAuthenticated()") + public void generateTwoFaAccountConfigWithQr(@RequestParam TwoFactorAuthProviderType providerType, HttpServletResponse response) throws Exception { + TwoFactorAuthAccountConfig config = generateTwoFaAccountConfig(providerType); + if (providerType == TwoFactorAuthProviderType.TOTP) { + BitMatrix qr = new QRCodeWriter().encode(((TotpTwoFactorAuthAccountConfig) config).getAuthUrl(), BarcodeFormat.QR_CODE, 200, 200); + try (ServletOutputStream outputStream = response.getOutputStream()) { + MatrixToImageWriter.writeToStream(qr, "PNG", outputStream); + } + } + response.setHeader("config", JacksonUtil.toString(config)); + } + /* TMP */ + + @PostMapping("/account/config/submit") + @PreAuthorize("isAuthenticated()") + public void submitTwoFaAccountConfig(@Valid @RequestBody TwoFactorAuthAccountConfig accountConfig) throws Exception { + SecurityUser user = getCurrentUser(); + twoFactorAuthService.prepareVerificationCode(user, accountConfig, false); + } + + @PostMapping("/account/config") + @PreAuthorize("isAuthenticated()") + public void verifyAndSaveTwoFaAccountConfig(@Valid @RequestBody TwoFactorAuthAccountConfig accountConfig, + @RequestParam String verificationCode) throws Exception { + SecurityUser user = getCurrentUser(); + boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, verificationCode, accountConfig, false); + if (verificationSuccess) { + twoFactorAuthConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig); + } else { + throw new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.INVALID_ARGUMENTS); + } + } + + @DeleteMapping("/account/config") + @PreAuthorize("isAuthenticated()") + public void deleteTwoFactorAuthAccountConfig() throws ThingsboardException { + SecurityUser user = getCurrentUser(); + twoFactorAuthConfigManager.deleteTwoFaAccountConfig(user.getTenantId(), user.getId()); + } + + + @GetMapping("/settings") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public TwoFactorAuthSettings getTwoFactorAuthSettings() throws ThingsboardException { + return twoFactorAuthConfigManager.getTwoFaSettings(getTenantId()).orElse(null); + } + + @PostMapping("/settings") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public void saveTwoFactorAuthSettings(@RequestBody TwoFactorAuthSettings twoFactorAuthSettings) throws ThingsboardException { + twoFactorAuthConfigManager.saveTwoFaSettings(getTenantId(), twoFactorAuthSettings); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java index bf8f2a7054..58a7af3d7a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -15,42 +15,22 @@ */ package org.thingsboard.server.controller; -import com.google.zxing.BarcodeFormat; -import com.google.zxing.client.j2se.MatrixToImageWriter; -import com.google.zxing.common.BitMatrix; -import com.google.zxing.qrcode.QRCodeWriter; import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; -import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; -import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig; -import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; -import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; import org.thingsboard.server.service.security.model.JwtTokenPair; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; -import javax.servlet.ServletOutputStream; -import javax.servlet.http.HttpServletResponse; -import javax.validation.Valid; -import java.util.HashMap; -import java.util.Map; - /* * * TODO [viacheslav]: - * - Configurable softlock after XX (3) attempts: XX (15) mins - on session level * - Configurable hardlock (user blocking) after a total of XX (10) unsuccessful attempts - on user level * * FIXME [viacheslav]: @@ -69,7 +49,7 @@ import java.util.Map; * token to configure 2FA account config); also will need to make users configure 2FA during activation and password setup... * */ @RestController -@RequestMapping("/api") +@RequestMapping("/api/auth/2fa") @RequiredArgsConstructor public class TwoFactorAuthController extends BaseController { @@ -77,132 +57,22 @@ public class TwoFactorAuthController extends BaseController { private final JwtTokenFactory tokenFactory; - @GetMapping("/2fa/account/config") - @PreAuthorize("isAuthenticated()") - public TwoFactorAuthAccountConfig getTwoFactorAuthAccountConfig() throws ThingsboardException { - SecurityUser user = getCurrentUser(); - - return twoFactorAuthService.getTwoFaAccountConfig(user.getTenantId(), user.getId()).orElse(null); - } - - @PostMapping("/2fa/account/config/generate") - @PreAuthorize("isAuthenticated()") - public TwoFactorAuthAccountConfig generateTwoFactorAuthAccountConfig(@RequestParam TwoFactorAuthProviderType providerType) throws Exception { - SecurityUser user = getCurrentUser(); - - return twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), providerType, - (provider, providerConfig) -> { - return provider.generateNewAccountConfig(user, providerConfig); - }); - } - - // temporary endpoint for testing purposes - @PostMapping("/2fa/account/config/generate/qr") - @PreAuthorize("isAuthenticated()") - public void generateTwoFactorAuthAccountConfigWithQr(@RequestParam TwoFactorAuthProviderType providerType, HttpServletResponse response) throws Exception { - TwoFactorAuthAccountConfig config = generateTwoFactorAuthAccountConfig(providerType); - if (providerType == TwoFactorAuthProviderType.TOTP) { - BitMatrix qr = new QRCodeWriter().encode(((TotpTwoFactorAuthAccountConfig) config).getAuthUrl(), BarcodeFormat.QR_CODE, 200, 200); - try (ServletOutputStream outputStream = response.getOutputStream()) { - MatrixToImageWriter.writeToStream(qr, "PNG", outputStream); - } - } - response.setHeader("config", JacksonUtil.toString(config)); - } - - @PostMapping("/2fa/account/config/submit") - @PreAuthorize("isAuthenticated()") - public void submitTwoFactorAuthAccountConfig(@Valid @RequestBody TwoFactorAuthAccountConfig accountConfig) throws Exception { - SecurityUser user = getCurrentUser(); - - twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), accountConfig.getProviderType(), - (provider, providerConfig) -> { - provider.prepareVerificationCode(user, providerConfig, accountConfig); - }); - } - - @PostMapping("/2fa/account/config") - @PreAuthorize("isAuthenticated()") - public void verifyAndSaveTwoFactorAuthAccountConfig(@Valid @RequestBody TwoFactorAuthAccountConfig accountConfig, - @RequestParam String verificationCode) throws Exception { - SecurityUser user = getCurrentUser(); - - boolean verificationSuccess = twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), accountConfig.getProviderType(), - (provider, providerConfig) -> { - return provider.checkVerificationCode(user, verificationCode, providerConfig, accountConfig); - }); - - if (verificationSuccess) { - twoFactorAuthService.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig); - } else { - throw new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.INVALID_ARGUMENTS); - } - } - - - @GetMapping("/2fa/settings") - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - public TwoFactorAuthSettings getTwoFactorAuthSettings() throws ThingsboardException { - return twoFactorAuthService.getTwoFaSettings(getTenantId()).orElse(null); - } - - @PostMapping("/2fa/settings") - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - public void saveTwoFactorAuthSettings(@Valid @RequestBody TwoFactorAuthSettings twoFactorAuthSettings) throws ThingsboardException { - twoFactorAuthService.saveTwoFaSettings(getTenantId(), twoFactorAuthSettings); - } - - - private final Map verificationCodeSendRateLimits = new HashMap<>(); - private final Map verificationCodeCheckRateLimits = new HashMap<>(); - - @PostMapping("/auth/2fa/verification/send") + @PostMapping("/verification/send") @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") public void sendTwoFaVerificationCode() throws Exception { SecurityUser user = getCurrentUser(); - - TwoFactorAuthSettings twoFaSettings = twoFactorAuthService.getTwoFaSettings(user.getTenantId()).get(); - if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeSendRateLimit())) { - TbRateLimits rateLimits = verificationCodeSendRateLimits.computeIfAbsent(user.getSessionId(), sessionId -> { - return new TbRateLimits(twoFaSettings.getVerificationCodeSendRateLimit()); - }); - if (!rateLimits.tryConsume()) { - throw new ThingsboardException(ThingsboardErrorCode.TOO_MANY_REQUESTS); - } - } - - twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), user.getId(), - (provider, providerConfig, accountConfig) -> { - provider.prepareVerificationCode(user, providerConfig, accountConfig); - }); + twoFactorAuthService.prepareVerificationCode(user, true); } - @PostMapping("/auth/2fa/verification/check") + @PostMapping("/verification/check") @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") public JwtTokenPair checkTwoFaVerificationCode(@RequestParam String verificationCode) throws Exception { SecurityUser user = getCurrentUser(); - - - - // FIXME [viacheslav]: rate limits for verification code check - boolean verificationSuccess = twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), user.getId(), - (provider, providerConfig, accountConfig) -> { - return provider.checkVerificationCode(user, verificationCode, providerConfig, accountConfig); - }); - - + boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, verificationCode, true); if (verificationSuccess) { + // FIXME [viacheslav]: log login action return tokenFactory.createTokenPair(user); } else { - TwoFactorAuthSettings twoFaSettings = twoFactorAuthService.getTwoFaSettings(user.getTenantId()).get(); - if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeSendRateLimit())) { - TbRateLimits rateLimits = verificationCodeSendRateLimits.computeIfAbsent(user.getSessionId(), sessionId -> { - return new TbRateLimits(twoFaSettings.getVerificationCodeSendRateLimit()); - }); - if (!rateLimits.tryConsume()) { - throw new ThingsboardException(ThingsboardErrorCode.TOO_MANY_REQUESTS); - } - } throw new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.AUTHENTICATION); } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java new file mode 100644 index 0000000000..e54a39951f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java @@ -0,0 +1,151 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.security.auth.mfa; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.tools.TbRateLimits; +import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager; +import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; +import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; +import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig; +import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProvider; +import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.system.SystemSecurityService; + +import java.util.Collection; +import java.util.EnumMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +@Service +@RequiredArgsConstructor +public class DefaultTwoFactorAuthService implements TwoFactorAuthService { + + private final TwoFactorAuthConfigManager configManager; + private final SystemSecurityService systemSecurityService; + private final UserService userService; + private final Map> providers = new EnumMap<>(TwoFactorAuthProviderType.class); + + // FIXME [viacheslav]: remove from the map + // TODO [viacheslav]: these rate limits are local, and will work bad in the cluster + private final ConcurrentMap verificationCodeSendingRateLimits = new ConcurrentHashMap<>(); + private final ConcurrentMap verificationCodeCheckingRateLimits = new ConcurrentHashMap<>(); + + private static final ThingsboardException ACCOUNT_NOT_CONFIGURED = new ThingsboardException("2FA is not configured for account", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + private static final ThingsboardException PROVIDER_NOT_CONFIGURED = new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + private static final ThingsboardException PROVIDER_NOT_AVAILABLE = new ThingsboardException("2FA provider is not available", ThingsboardErrorCode.GENERAL); + + + @Override + public void prepareVerificationCode(SecurityUser securityUser, boolean rateLimit) throws Exception { + TwoFactorAuthAccountConfig accountConfig = configManager.getTwoFaAccountConfig(securityUser.getTenantId(), securityUser.getId()) + .orElseThrow(() -> ACCOUNT_NOT_CONFIGURED); + prepareVerificationCode(securityUser, accountConfig, rateLimit); + } + + @Override + public void prepareVerificationCode(SecurityUser securityUser, TwoFactorAuthAccountConfig accountConfig, boolean rateLimit) throws ThingsboardException { + TwoFactorAuthSettings twoFaSettings = configManager.getTwoFaSettings(securityUser.getTenantId()) + .orElseThrow(() -> PROVIDER_NOT_CONFIGURED); + if (rateLimit) { + if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeSendRateLimit())) { + TbRateLimits rateLimits = verificationCodeSendingRateLimits.computeIfAbsent(securityUser.getSessionId(), sessionId -> { + return new TbRateLimits(twoFaSettings.getVerificationCodeSendRateLimit()); + }); + if (!rateLimits.tryConsume()) { + throw new ThingsboardException("Too many verification code sending requests", ThingsboardErrorCode.TOO_MANY_REQUESTS); + } + } + } + + TwoFactorAuthProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType()) + .orElseThrow(() -> PROVIDER_NOT_CONFIGURED); + getTwoFaProvider(accountConfig.getProviderType()).prepareVerificationCode(securityUser, providerConfig, accountConfig); + } + + @Override + public boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, boolean rateLimit) throws ThingsboardException { + TwoFactorAuthAccountConfig accountConfig = configManager.getTwoFaAccountConfig(securityUser.getTenantId(), securityUser.getId()) + .orElseThrow(() -> ACCOUNT_NOT_CONFIGURED); + return checkVerificationCode(securityUser, verificationCode, accountConfig, rateLimit); + } + + @Override + public boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, TwoFactorAuthAccountConfig accountConfig, boolean rateLimit) throws ThingsboardException { + if (!userService.findUserCredentialsByUserId(securityUser.getTenantId(), securityUser.getId()).isEnabled()) { + throw new ThingsboardException("User is disabled", ThingsboardErrorCode.AUTHENTICATION); + } + + TwoFactorAuthSettings twoFaSettings = configManager.getTwoFaSettings(securityUser.getTenantId()) + .orElseThrow(() -> PROVIDER_NOT_CONFIGURED); + if (rateLimit) { + if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeCheckRateLimit())) { + TbRateLimits rateLimits = verificationCodeCheckingRateLimits.computeIfAbsent(securityUser.getSessionId(), sessionId -> { + return new TbRateLimits(twoFaSettings.getVerificationCodeCheckRateLimit()); + }); + if (!rateLimits.tryConsume()) { + throw new ThingsboardException("Too many verification code checking requests", ThingsboardErrorCode.TOO_MANY_REQUESTS); + } + } + } + + TwoFactorAuthProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType()) + .orElseThrow(() -> PROVIDER_NOT_CONFIGURED); + boolean verificationSuccess = getTwoFaProvider(accountConfig.getProviderType()).checkVerificationCode(securityUser, verificationCode, providerConfig, accountConfig); + if (rateLimit) { + systemSecurityService.validateTwoFaVerification(securityUser.getTenantId(), securityUser.getId(), verificationSuccess, twoFaSettings); + } + return verificationSuccess; + } + + @Override + public TwoFactorAuthAccountConfig generateNewAccountConfig(User user, TwoFactorAuthProviderType providerType) throws ThingsboardException { + TwoFactorAuthProviderConfig providerConfig = getTwoFaProviderConfig(user.getTenantId(), providerType); + return getTwoFaProvider(providerType).generateNewAccountConfig(user, providerConfig); + } + + + private TwoFactorAuthProviderConfig getTwoFaProviderConfig(TenantId tenantId, TwoFactorAuthProviderType providerType) throws ThingsboardException { + return configManager.getTwoFaSettings(tenantId) + .flatMap(twoFaSettings -> twoFaSettings.getProviderConfig(providerType)) + .orElseThrow(() -> PROVIDER_NOT_CONFIGURED); + } + + private TwoFactorAuthProvider getTwoFaProvider(TwoFactorAuthProviderType providerType) throws ThingsboardException { + return Optional.ofNullable(providers.get(providerType)) + .orElseThrow(() -> PROVIDER_NOT_AVAILABLE); + } + + @Autowired + private void setProviders(Collection> providers) { + providers.forEach(provider -> { + this.providers.put(provider.getType(), provider); + }); + } + +} + diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java index 501517ca88..ec2511e62a 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java @@ -15,171 +15,22 @@ */ package org.thingsboard.server.service.security.auth.mfa; -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.common.util.ThrowingBiConsumer; -import org.thingsboard.common.util.ThrowingBiFunction; -import org.thingsboard.common.util.ThrowingTripleConsumer; -import org.thingsboard.common.util.ThrowingTripleFunction; -import org.thingsboard.server.common.data.AdminSettings; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; -import org.thingsboard.server.common.data.kv.JsonDataEntry; -import org.thingsboard.server.dao.attributes.AttributesService; -import org.thingsboard.server.dao.settings.AdminSettingsService; -import org.thingsboard.server.dao.user.UserService; -import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; -import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig; -import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProvider; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; +import org.thingsboard.server.service.security.model.SecurityUser; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumMap; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ExecutionException; +public interface TwoFactorAuthService { -@Service -@RequiredArgsConstructor -public class TwoFactorAuthService { + void prepareVerificationCode(SecurityUser securityUser, boolean rateLimit) throws Exception; - private final UserService userService; - private final AdminSettingsService adminSettingsService; - private final AttributesService attributesService; - private final Map> providers = new EnumMap<>(TwoFactorAuthProviderType.class); + void prepareVerificationCode(SecurityUser securityUser, TwoFactorAuthAccountConfig accountConfig, boolean rateLimit) throws ThingsboardException; - protected static final String TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY = "twoFaConfig"; - protected static final String TWO_FACTOR_AUTH_SETTINGS_KEY = "twoFaSettings"; + boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, boolean rateLimit) throws ThingsboardException; + boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, TwoFactorAuthAccountConfig accountConfig, boolean rateLimit) throws ThingsboardException; - public R processByTwoFaProvider(TenantId tenantId, TwoFactorAuthProviderType providerType, ThrowingBiFunction, TwoFactorAuthProviderConfig, R> function) throws Exception { - TwoFactorAuthProviderConfig providerConfig = getTwoFaProviderConfig(tenantId, providerType) - .orElseThrow(() -> new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS)); - TwoFactorAuthProvider provider = getTwoFaProvider(providerType) - .orElseThrow(() -> new ThingsboardException("2FA provider is not available", ThingsboardErrorCode.ITEM_NOT_FOUND)); - - return function.apply(provider, providerConfig); - } - - public void processByTwoFaProvider(TenantId tenantId, TwoFactorAuthProviderType providerType, ThrowingBiConsumer, TwoFactorAuthProviderConfig> function) throws Exception { - processByTwoFaProvider(tenantId, providerType, (provider, providerConfig) -> { - function.accept(provider, providerConfig); - return null; - }); - } - - public R processByTwoFaProvider(TenantId tenantId, UserId userId, ThrowingTripleFunction, TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig, R> function) throws Exception { - TwoFactorAuthAccountConfig accountConfig = getTwoFaAccountConfig(tenantId, userId) - .orElseThrow(() -> new ThingsboardException("2FA is not configured for user", ThingsboardErrorCode.BAD_REQUEST_PARAMS)); - - TwoFactorAuthProviderConfig providerConfig = getTwoFaProviderConfig(tenantId, accountConfig.getProviderType()) - .orElseThrow(() -> new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS)); - TwoFactorAuthProvider provider = getTwoFaProvider(accountConfig.getProviderType()) - .orElseThrow(() -> new ThingsboardException("2FA provider is not available", ThingsboardErrorCode.ITEM_NOT_FOUND)); - - return function.apply(provider, providerConfig, accountConfig); - } - - public void processByTwoFaProvider(TenantId tenantId, UserId userId, ThrowingTripleConsumer, TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig> function) throws Exception { - processByTwoFaProvider(tenantId, userId, (provider, providerConfig, accountConfig) -> { - function.accept(provider, providerConfig, accountConfig); - return null; - }); - } - - - public Optional getTwoFaAccountConfig(TenantId tenantId, UserId userId) { - User user = userService.findUserById(tenantId, userId); - return Optional.ofNullable(user.getAdditionalInfo()) - .flatMap(additionalInfo -> Optional.ofNullable(additionalInfo.get(TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY)).filter(jsonNode -> !jsonNode.isNull())) - .map(jsonNode -> JacksonUtil.treeToValue(jsonNode, TwoFactorAuthAccountConfig.class)) - .filter(twoFactorAuthAccountConfig -> { - return getTwoFaProviderConfig(tenantId, twoFactorAuthAccountConfig.getProviderType()).isPresent(); - }); - } - - public void saveTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFactorAuthAccountConfig accountConfig) throws ThingsboardException { - getTwoFaProviderConfig(tenantId, accountConfig.getProviderType()) - .orElseThrow(() -> new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS)); - - User user = userService.findUserById(tenantId, userId); - ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(user.getAdditionalInfo()) - .orElseGet(JacksonUtil::newObjectNode); - additionalInfo.set(TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY, JacksonUtil.valueToTree(accountConfig)); - user.setAdditionalInfo(additionalInfo); - - userService.saveUser(user); - } - - public void deleteTwoFaAccountConfig(TenantId tenantId, UserId userId) { - User user = userService.findUserById(tenantId, userId); - ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(user.getAdditionalInfo()) - .orElseGet(JacksonUtil::newObjectNode); - additionalInfo.remove(TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY); - user.setAdditionalInfo(additionalInfo); - - userService.saveUser(user); - } - - - @SneakyThrows({InterruptedException.class, ExecutionException.class}) - public Optional getTwoFaSettings(TenantId tenantId) { - if (tenantId.equals(TenantId.SYS_TENANT_ID)) { - return Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY)) - .map(adminSettings -> JacksonUtil.treeToValue(adminSettings.getJsonValue(), TwoFactorAuthSettings.class)); - } else { - return attributesService.find(TenantId.SYS_TENANT_ID, tenantId, DataConstants.SERVER_SCOPE, TWO_FACTOR_AUTH_SETTINGS_KEY).get() - .map(adminSettingsAttribute -> JacksonUtil.fromString(adminSettingsAttribute.getJsonValue().get(), TwoFactorAuthSettings.class)) - .filter(tenantTwoFactorAuthSettings -> !tenantTwoFactorAuthSettings.isUseSystemTwoFactorAuthSettings()) - .or(() -> getTwoFaSettings(TenantId.SYS_TENANT_ID)); - } - } - - @SneakyThrows({InterruptedException.class, ExecutionException.class}) - public void saveTwoFaSettings(TenantId tenantId, TwoFactorAuthSettings twoFactorAuthSettings) { - if (tenantId.equals(TenantId.SYS_TENANT_ID)) { - AdminSettings settings = Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY)) - .orElseGet(() -> { - AdminSettings newSettings = new AdminSettings(); - newSettings.setKey(TWO_FACTOR_AUTH_SETTINGS_KEY); - return newSettings; - }); - settings.setJsonValue(JacksonUtil.valueToTree(twoFactorAuthSettings)); - adminSettingsService.saveAdminSettings(tenantId, settings); - } else { - attributesService.save(TenantId.SYS_TENANT_ID, tenantId, DataConstants.SERVER_SCOPE, Collections.singletonList( - new BaseAttributeKvEntry(new JsonDataEntry(TWO_FACTOR_AUTH_SETTINGS_KEY, JacksonUtil.toString(twoFactorAuthSettings)), System.currentTimeMillis()) - )).get(); - } - } - - - private
Optional> getTwoFaProvider(TwoFactorAuthProviderType providerType) { - return Optional.of((TwoFactorAuthProvider) providers.get(providerType)); - } - - private Optional getTwoFaProviderConfig(TenantId tenantId, TwoFactorAuthProviderType providerType) { - return getTwoFaSettings(tenantId) - .flatMap(twoFaSettings -> twoFaSettings.getProviderConfig(providerType)) - .map(providerConfig -> (C) providerConfig); - } - - @Autowired - private void setProviders(Collection> providers) { - providers.forEach(provider -> { - this.providers.put(provider.getType(), provider); - }); - } + TwoFactorAuthAccountConfig generateNewAccountConfig(User user, TwoFactorAuthProviderType providerType) throws ThingsboardException; } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFactorAuthConfigManager.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFactorAuthConfigManager.java new file mode 100644 index 0000000000..a96d25e520 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFactorAuthConfigManager.java @@ -0,0 +1,139 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.security.auth.mfa.config; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.service.ConstraintValidator; +import org.thingsboard.server.dao.settings.AdminSettingsService; +import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; +import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig; +import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; + +import java.util.Collections; +import java.util.Optional; +import java.util.concurrent.ExecutionException; + +@Service +@RequiredArgsConstructor +public class DefaultTwoFactorAuthConfigManager implements TwoFactorAuthConfigManager { + + private final UserService userService; + private final AdminSettingsService adminSettingsService; + private final AttributesService attributesService; + + protected static final String TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY = "twoFaConfig"; + protected static final String TWO_FACTOR_AUTH_SETTINGS_KEY = "twoFaSettings"; + + + @Override + public boolean isTwoFaEnabled(User user) { + return getTwoFaAccountConfig(user.getTenantId(), user.getId()).isPresent(); + } + + @Override + public Optional getTwoFaAccountConfig(TenantId tenantId, UserId userId) { + User user = userService.findUserById(tenantId, userId); + return Optional.ofNullable(user.getAdditionalInfo()) + .flatMap(additionalInfo -> Optional.ofNullable(additionalInfo.get(TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY)).filter(jsonNode -> !jsonNode.isNull())) + .map(jsonNode -> JacksonUtil.treeToValue(jsonNode, TwoFactorAuthAccountConfig.class)) + .filter(twoFactorAuthAccountConfig -> { + return getTwoFaProviderConfig(tenantId, twoFactorAuthAccountConfig.getProviderType()).isPresent(); + }); + } + + @Override + public void saveTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFactorAuthAccountConfig accountConfig) throws ThingsboardException { + getTwoFaProviderConfig(tenantId, accountConfig.getProviderType()) + .orElseThrow(() -> new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS)); + + User user = userService.findUserById(tenantId, userId); + ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(user.getAdditionalInfo()) + .orElseGet(JacksonUtil::newObjectNode); + additionalInfo.set(TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY, JacksonUtil.valueToTree(accountConfig)); + user.setAdditionalInfo(additionalInfo); + + userService.saveUser(user); + } + + @Override + public void deleteTwoFaAccountConfig(TenantId tenantId, UserId userId) { + User user = userService.findUserById(tenantId, userId); + ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(user.getAdditionalInfo()) + .orElseGet(JacksonUtil::newObjectNode); + additionalInfo.remove(TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY); + user.setAdditionalInfo(additionalInfo); + + userService.saveUser(user); + } + + + private Optional getTwoFaProviderConfig(TenantId tenantId, TwoFactorAuthProviderType providerType) { + return getTwoFaSettings(tenantId) + .flatMap(twoFaSettings -> twoFaSettings.getProviderConfig(providerType)); + } + + @SneakyThrows({InterruptedException.class, ExecutionException.class}) + @Override + public Optional getTwoFaSettings(TenantId tenantId) { + if (tenantId.equals(TenantId.SYS_TENANT_ID)) { + return Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY)) + .map(adminSettings -> JacksonUtil.treeToValue(adminSettings.getJsonValue(), TwoFactorAuthSettings.class)); + } else { + return attributesService.find(TenantId.SYS_TENANT_ID, tenantId, DataConstants.SERVER_SCOPE, TWO_FACTOR_AUTH_SETTINGS_KEY).get() + .map(adminSettingsAttribute -> JacksonUtil.fromString(adminSettingsAttribute.getJsonValue().get(), TwoFactorAuthSettings.class)) + .filter(tenantTwoFactorAuthSettings -> !tenantTwoFactorAuthSettings.isUseSystemTwoFactorAuthSettings()) + .or(() -> getTwoFaSettings(TenantId.SYS_TENANT_ID)); + } + } + + @SneakyThrows({InterruptedException.class, ExecutionException.class}) + @Override + public void saveTwoFaSettings(TenantId tenantId, TwoFactorAuthSettings twoFactorAuthSettings) { + if (tenantId.equals(TenantId.SYS_TENANT_ID) || !twoFactorAuthSettings.isUseSystemTwoFactorAuthSettings()) { + ConstraintValidator.validateFields(twoFactorAuthSettings); + } + if (tenantId.equals(TenantId.SYS_TENANT_ID)) { + AdminSettings settings = Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY)) + .orElseGet(() -> { + AdminSettings newSettings = new AdminSettings(); + newSettings.setKey(TWO_FACTOR_AUTH_SETTINGS_KEY); + return newSettings; + }); + settings.setJsonValue(JacksonUtil.valueToTree(twoFactorAuthSettings)); + adminSettingsService.saveAdminSettings(tenantId, settings); + } else { + attributesService.save(TenantId.SYS_TENANT_ID, tenantId, DataConstants.SERVER_SCOPE, Collections.singletonList( + new BaseAttributeKvEntry(new JsonDataEntry(TWO_FACTOR_AUTH_SETTINGS_KEY, JacksonUtil.toString(twoFactorAuthSettings)), System.currentTimeMillis()) + )).get(); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthConfigManager.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthConfigManager.java new file mode 100644 index 0000000000..94c18aa999 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthConfigManager.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.security.auth.mfa.config; + +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; + +import java.util.Optional; + +public interface TwoFactorAuthConfigManager { + + boolean isTwoFaEnabled(User user); + + Optional getTwoFaAccountConfig(TenantId tenantId, UserId userId); + + void saveTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFactorAuthAccountConfig accountConfig) throws ThingsboardException; + + void deleteTwoFaAccountConfig(TenantId tenantId, UserId userId); + + + Optional getTwoFaSettings(TenantId tenantId); + + void saveTwoFaSettings(TenantId tenantId, TwoFactorAuthSettings twoFactorAuthSettings); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java index 8b8927f721..e70742267b 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java @@ -15,12 +15,14 @@ */ package org.thingsboard.server.service.security.auth.mfa.config; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.checkerframework.checker.index.qual.NonNegative; import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; import javax.validation.Valid; +import javax.validation.constraints.AssertTrue; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; @@ -30,17 +32,21 @@ import java.util.Optional; @Data public class TwoFactorAuthSettings { - @NotNull - private Boolean useSystemTwoFactorAuthSettings; + private boolean useSystemTwoFactorAuthSettings; @Valid private List providers; - @Pattern(regexp = "\\d+:\\d+") - private String verificationCodeSendRateLimit; // 1:60 - one time in a minute - @Pattern(regexp = "\\d+:\\d+") - private String verificationCodeCheckRateLimit; // soft lockout, on session level + @ApiModelProperty(example = "1:60 (1 request per minute)") + @Pattern(regexp = "[^0]\\d+:[^0]\\d+", message = "Rate limit configuration is invalid") + private String verificationCodeSendRateLimit; + @ApiModelProperty(example = "3:900 (3 requests per 15 minutes)") + @Pattern(regexp = "[^0]\\d+:[^0]\\d+", message = "Rate limit configuration is invalid") + private String verificationCodeCheckRateLimit; @Min(0) - private Integer maxVerificationCodeSubmitAttemptsBeforeUserBlocking; + private int maxCodeVerificationFailuresBeforeUserLockout; + @ApiModelProperty(value = "in seconds") + @Min(1) + private int totalAllowedTimeForVerification; public Optional getProviderConfig(TwoFactorAuthProviderType providerType) { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/EmailTwoFactorAuthAccountConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/EmailTwoFactorAuthAccountConfig.java index c18e4c41c1..dfbba22113 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/EmailTwoFactorAuthAccountConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/EmailTwoFactorAuthAccountConfig.java @@ -17,13 +17,18 @@ package org.thingsboard.server.service.security.auth.mfa.config.account; import lombok.Data; import lombok.EqualsAndHashCode; +import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.Email; + @EqualsAndHashCode(callSuper = true) @Data public class EmailTwoFactorAuthAccountConfig extends OtpBasedTwoFactorAuthAccountConfig { - private boolean useAccountEmail; // TODO [viacheslav]: validate + private boolean useAccountEmail; + @Email(message = "Email is not valid") private String email; @Override @@ -31,4 +36,10 @@ public class EmailTwoFactorAuthAccountConfig extends OtpBasedTwoFactorAuthAccoun return TwoFactorAuthProviderType.EMAIL; } + + @AssertTrue(message = "Email must be specified") // TODO [viacheslav]: test ! + private boolean isValid() { + return useAccountEmail || StringUtils.isNotEmpty(email); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java index 82e3760e35..40e94899a7 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java @@ -20,12 +20,14 @@ import lombok.EqualsAndHashCode; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; @EqualsAndHashCode(callSuper = true) @Data public class SmsTwoFactorAuthAccountConfig extends OtpBasedTwoFactorAuthAccountConfig { @NotBlank + @Pattern(regexp = "^\\+[1-9]\\d{1,14}$", message = "Phone number is not of E.164 format") private String phoneNumber; @Override diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TwoFactorAuthAccountConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TwoFactorAuthAccountConfig.java index 141535eea8..44774bb2a3 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TwoFactorAuthAccountConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TwoFactorAuthAccountConfig.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.security.auth.mfa.config.account; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; @@ -26,8 +27,9 @@ import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthPr use = JsonTypeInfo.Id.NAME, property = "providerType") @JsonSubTypes({ - @JsonSubTypes.Type(value = TotpTwoFactorAuthAccountConfig.class, name = "TOTP"), - @JsonSubTypes.Type(value = SmsTwoFactorAuthAccountConfig.class, name = "SMS"), + @Type(name = "TOTP", value = TotpTwoFactorAuthAccountConfig.class ), + @Type(name = "SMS", value = SmsTwoFactorAuthAccountConfig.class), + @Type(name = "EMAIL", value = EmailTwoFactorAuthAccountConfig.class) }) public interface TwoFactorAuthAccountConfig { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/OtpBasedTwoFactorAuthProviderConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/OtpBasedTwoFactorAuthProviderConfig.java index c7169def48..fa1e5d7b43 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/OtpBasedTwoFactorAuthProviderConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/OtpBasedTwoFactorAuthProviderConfig.java @@ -15,9 +15,14 @@ */ package org.thingsboard.server.service.security.auth.mfa.config.provider; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; +import javax.validation.constraints.Min; + @Data public abstract class OtpBasedTwoFactorAuthProviderConfig implements TwoFactorAuthProviderConfig { - private Integer verificationCodeLifetime; // seconds + @ApiModelProperty(value = "in seconds", example = "60") + @Min(1) // TODO [viacheslav]: test + private int verificationCodeLifetime; } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TwoFactorAuthProviderConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TwoFactorAuthProviderConfig.java index a86bcee222..c94c403fd8 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TwoFactorAuthProviderConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TwoFactorAuthProviderConfig.java @@ -18,7 +18,9 @@ package org.thingsboard.server.service.security.auth.mfa.config.provider; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.thingsboard.server.service.security.auth.mfa.config.account.EmailTwoFactorAuthAccountConfig; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; @JsonIgnoreProperties(ignoreUnknown = true) @@ -26,8 +28,9 @@ import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthPr use = JsonTypeInfo.Id.NAME, property = "providerType") @JsonSubTypes({ - @JsonSubTypes.Type(value = TotpTwoFactorAuthProviderConfig.class, name = "TOTP"), - @JsonSubTypes.Type(value = SmsTwoFactorAuthProviderConfig.class, name = "SMS"), + @Type(name = "TOTP", value = TotpTwoFactorAuthProviderConfig.class), + @Type(name = "SMS", value = SmsTwoFactorAuthProviderConfig.class), + @Type(name = "EMAIL", value = EmailTwoFactorAuthAccountConfig.class) }) public interface TwoFactorAuthProviderConfig { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java index f8d07aedb2..e13e0925d5 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java @@ -40,8 +40,7 @@ public abstract class OtpBasedTwoFactorAuthProvider TimeUnit.SECONDS.toMillis(providerConfig.getVerificationCodeLifetime())) { + verificationCodesCache.evict(user.getSessionId()); + return false; + } + if (verificationCode.equals(correctVerificationCode.getValue()) + && correctVerificationCode.getConfig().equals(accountConfig)) { verificationCodesCache.evict(user.getSessionId()); return true; } @@ -65,6 +70,7 @@ public abstract class OtpBasedTwoFactorAuthProvider 0) { if (failedLoginAttempts > securitySettings.getMaxFailedLoginAttempts() && userCredentials.isEnabled()) { - userService.setUserCredentialsEnabled(TenantId.SYS_TENANT_ID, userCredentials.getUserId(), false); - if (StringUtils.isNoneBlank(securitySettings.getUserLockoutNotificationEmail())) { - try { - mailService.sendAccountLockoutEmail(username, securitySettings.getUserLockoutNotificationEmail(), securitySettings.getMaxFailedLoginAttempts()); - } catch (ThingsboardException e) { - log.warn("Can't send email regarding user account [{}] lockout to provided email [{}]", username, securitySettings.getUserLockoutNotificationEmail(), e); - } - } + lockAccount(userCredentials.getUserId(), username, securitySettings); throw new LockedException("Authentication Failed. Username was locked due to security policy."); } } @@ -143,6 +139,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService { throw new DisabledException("User is not active"); } + // FIXME [viacheslav]: don't do that in case of 2FA. maybe just move underlying setLastLoginTs to logLoginAction ? userService.onUserLoginSuccessful(tenantId, userCredentials.getUserId()); SecuritySettings securitySettings = self.getSecuritySettings(tenantId); @@ -156,6 +153,43 @@ public class DefaultSystemSecurityService implements SystemSecurityService { } } + @Override + public void validateTwoFaVerification(TenantId tenantId, UserId userId, boolean verificationSuccess, TwoFactorAuthSettings twoFaSettings) { + User user = userService.findUserById(tenantId, userId); + ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(user.getAdditionalInfo()) + .filter(jsonNode -> jsonNode instanceof ObjectNode) + .orElseGet(JacksonUtil::newObjectNode); + // TODO [viacheslav]: test ! + int failedVerificationAttempts = Optional.ofNullable(additionalInfo.get("failedTwoFaVerificationAttempts")) + .map(JsonNode::asInt).orElse(0); + + if (!verificationSuccess) { + failedVerificationAttempts++; + // TODO [viacheslav]: maybe use userService.onUserLoginIncorrectCredentials() + } else { + failedVerificationAttempts = 0; + // and set last login ts + } + + if (twoFaSettings.getMaxCodeVerificationFailuresBeforeUserLockout() > 0 + && failedVerificationAttempts >= twoFaSettings.getMaxCodeVerificationFailuresBeforeUserLockout()) { + userService.setUserCredentialsEnabled(TenantId.SYS_TENANT_ID, userId, false); + lockAccount(userId, user.getEmail(), self.getSecuritySettings(tenantId)); + throw new LockedException("User account was locked due to exceeded 2FA verification attempts"); + } + } + + private void lockAccount(UserId userId, String username, SecuritySettings securitySettings) { + userService.setUserCredentialsEnabled(TenantId.SYS_TENANT_ID, userId, false); + if (StringUtils.isNoneBlank(securitySettings.getUserLockoutNotificationEmail())) { + try { + mailService.sendAccountLockoutEmail(username, securitySettings.getUserLockoutNotificationEmail(), securitySettings.getMaxFailedLoginAttempts()); + } catch (ThingsboardException e) { + log.warn("Can't send email regarding user account [{}] lockout to provided email [{}]", username, securitySettings.getUserLockoutNotificationEmail(), e); + } + } + } + @Override public void validatePassword(TenantId tenantId, String password, UserCredentials userCredentials) throws DataValidationException { SecuritySettings securitySettings = self.getSecuritySettings(tenantId); diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java index 2cc19ccac6..453251bf03 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java @@ -18,9 +18,11 @@ package org.thingsboard.server.service.security.system; import org.springframework.security.core.AuthenticationException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.common.data.security.model.SecuritySettings; +import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; import javax.servlet.http.HttpServletRequest; @@ -32,6 +34,8 @@ public interface SystemSecurityService { void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String username, String password) throws AuthenticationException; + void validateTwoFaVerification(TenantId tenantId, UserId userId, boolean verificationSuccess, TwoFactorAuthSettings twoFaSettings); + void validatePassword(TenantId tenantId, String password, UserCredentials userCredentials) throws DataValidationException; String getBaseUrl(TenantId tenantId, CustomerId customerId, HttpServletRequest httpServletRequest); diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java index a12c7ec0e8..b338d0d99d 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java @@ -45,6 +45,7 @@ import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; // TODO [viacheslav]: test sessionId +// TODO [viacheslav]: test validation for all account configs, provider configs and two factor auth settings public abstract class TwoFactorAuthTest extends AbstractControllerTest { @SpyBean diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index d48520648e..1ee78d4068 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -301,7 +301,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic public void onUserLoginSuccessful(TenantId tenantId, UserId userId) { log.trace("Executing onUserLoginSuccessful [{}]", userId); User user = findUserById(tenantId, userId); - setLastLoginTs(user); + setLastLoginTs(user); // FIXME [viacheslav]: move to logLoginAction ? resetFailedLoginAttempts(user); saveUser(user); } From 052068d7f48cd1639355fc6bb89bece6d0b9b7ee Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Sun, 20 Mar 2022 09:21:41 +0200 Subject: [PATCH 009/276] Remove 2FA with email message --- .../controller/TwoFactorAuthController.java | 4 -- .../auth/mfa/DefaultTwoFactorAuthService.java | 3 +- .../EmailTwoFactorAuthAccountConfig.java | 45 ------------- .../account/TwoFactorAuthAccountConfig.java | 3 +- .../EmailTwoFactorAuthProviderConfig.java | 33 --------- .../provider/TwoFactorAuthProviderConfig.java | 4 +- .../impl/EmailTwoFactorAuthProvider.java | 67 ------------------- 7 files changed, 3 insertions(+), 156 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/EmailTwoFactorAuthAccountConfig.java delete mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/EmailTwoFactorAuthProviderConfig.java delete mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/EmailTwoFactorAuthProvider.java diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java index 58a7af3d7a..76cc8f2f14 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -29,11 +29,7 @@ import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; /* - * * TODO [viacheslav]: - * - Configurable hardlock (user blocking) after a total of XX (10) unsuccessful attempts - on user level - * - * FIXME [viacheslav]: * - Tests for 2FA * - Swagger documentation * diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java index e54a39951f..98cbeb0786 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java @@ -141,11 +141,10 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { } @Autowired - private void setProviders(Collection> providers) { + private void setProviders(Collection providers) { providers.forEach(provider -> { this.providers.put(provider.getType(), provider); }); } } - diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/EmailTwoFactorAuthAccountConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/EmailTwoFactorAuthAccountConfig.java deleted file mode 100644 index dfbba22113..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/EmailTwoFactorAuthAccountConfig.java +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.service.security.auth.mfa.config.account; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import org.apache.commons.lang3.StringUtils; -import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; - -import javax.validation.constraints.AssertTrue; -import javax.validation.constraints.Email; - -@EqualsAndHashCode(callSuper = true) -@Data -public class EmailTwoFactorAuthAccountConfig extends OtpBasedTwoFactorAuthAccountConfig { - - private boolean useAccountEmail; - @Email(message = "Email is not valid") - private String email; - - @Override - public TwoFactorAuthProviderType getProviderType() { - return TwoFactorAuthProviderType.EMAIL; - } - - - @AssertTrue(message = "Email must be specified") // TODO [viacheslav]: test ! - private boolean isValid() { - return useAccountEmail || StringUtils.isNotEmpty(email); - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TwoFactorAuthAccountConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TwoFactorAuthAccountConfig.java index 44774bb2a3..d1947a72be 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TwoFactorAuthAccountConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TwoFactorAuthAccountConfig.java @@ -28,8 +28,7 @@ import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthPr property = "providerType") @JsonSubTypes({ @Type(name = "TOTP", value = TotpTwoFactorAuthAccountConfig.class ), - @Type(name = "SMS", value = SmsTwoFactorAuthAccountConfig.class), - @Type(name = "EMAIL", value = EmailTwoFactorAuthAccountConfig.class) + @Type(name = "SMS", value = SmsTwoFactorAuthAccountConfig.class) }) public interface TwoFactorAuthAccountConfig { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/EmailTwoFactorAuthProviderConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/EmailTwoFactorAuthProviderConfig.java deleted file mode 100644 index a782f73a68..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/EmailTwoFactorAuthProviderConfig.java +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.service.security.auth.mfa.config.provider; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; - -@EqualsAndHashCode(callSuper = true) -@Data -public class EmailTwoFactorAuthProviderConfig extends OtpBasedTwoFactorAuthProviderConfig{ - - private String emailVerificationMessageTemplate; // FIXME [viacheslav]: - - @Override - public TwoFactorAuthProviderType getProviderType() { - return TwoFactorAuthProviderType.EMAIL; - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TwoFactorAuthProviderConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TwoFactorAuthProviderConfig.java index c94c403fd8..f912f43144 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TwoFactorAuthProviderConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TwoFactorAuthProviderConfig.java @@ -20,7 +20,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; -import org.thingsboard.server.service.security.auth.mfa.config.account.EmailTwoFactorAuthAccountConfig; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; @JsonIgnoreProperties(ignoreUnknown = true) @@ -29,8 +28,7 @@ import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthPr property = "providerType") @JsonSubTypes({ @Type(name = "TOTP", value = TotpTwoFactorAuthProviderConfig.class), - @Type(name = "SMS", value = SmsTwoFactorAuthProviderConfig.class), - @Type(name = "EMAIL", value = EmailTwoFactorAuthAccountConfig.class) + @Type(name = "SMS", value = SmsTwoFactorAuthProviderConfig.class) }) public interface TwoFactorAuthProviderConfig { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/EmailTwoFactorAuthProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/EmailTwoFactorAuthProvider.java deleted file mode 100644 index aa34d5b2d6..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/EmailTwoFactorAuthProvider.java +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.service.security.auth.mfa.provider.impl; - -import org.springframework.cache.CacheManager; -import org.springframework.stereotype.Service; -import org.thingsboard.rule.engine.api.MailService; -import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.security.auth.mfa.config.account.EmailTwoFactorAuthAccountConfig; -import org.thingsboard.server.service.security.auth.mfa.config.provider.EmailTwoFactorAuthProviderConfig; -import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; -import org.thingsboard.server.service.security.model.SecurityUser; - -@Service -@TbCoreComponent -public class EmailTwoFactorAuthProvider extends OtpBasedTwoFactorAuthProvider { - - private final MailService mailService; - - protected EmailTwoFactorAuthProvider(CacheManager cacheManager, MailService mailService) { - super(cacheManager); - this.mailService = mailService; - } - - - @Override - public EmailTwoFactorAuthAccountConfig generateNewAccountConfig(User user, EmailTwoFactorAuthProviderConfig providerConfig) { - EmailTwoFactorAuthAccountConfig accountConfig = new EmailTwoFactorAuthAccountConfig(); - accountConfig.setUseAccountEmail(true); - return accountConfig; - } - - @Override - protected void sendVerificationCode(SecurityUser user, String verificationCode, EmailTwoFactorAuthProviderConfig providerConfig, EmailTwoFactorAuthAccountConfig accountConfig) throws ThingsboardException { - String email; - if (accountConfig.isUseAccountEmail()) { - email = user.getEmail(); - } else { - email = accountConfig.getEmail(); - } - - // FIXME [viacheslav]: mail template for 2FA verification - mailService.sendEmail(user.getTenantId(), email, "subject", ""); - } - - - @Override - public TwoFactorAuthProviderType getType() { - return TwoFactorAuthProviderType.EMAIL; - } - -} From 20a4f3cc4c3f15a3d9114cb3cb60613bade4ce36 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Sun, 20 Mar 2022 14:13:54 +0200 Subject: [PATCH 010/276] 2FA: lock account after X unsuccessful verification attempts; refactoring --- .../controller/TwoFactorAuthController.java | 15 ++-- .../RefreshTokenAuthenticationProvider.java | 3 +- .../auth/mfa/DefaultTwoFactorAuthService.java | 38 ++++---- .../mfa/config/TwoFactorAuthSettings.java | 6 +- .../mfa/provider/TwoFactorAuthProvider.java | 4 +- .../impl/OtpBasedTwoFactorAuthProvider.java | 14 +-- .../impl/TotpTwoFactorAuthProvider.java | 2 +- .../auth/rest/RestAuthenticationProvider.java | 60 +------------ ...RestAwareAuthenticationSuccessHandler.java | 5 +- .../service/security/model/SecurityUser.java | 11 --- .../security/model/token/JwtTokenFactory.java | 6 -- .../system/DefaultSystemSecurityService.java | 88 +++++++++++++++---- .../system/SystemSecurityService.java | 11 ++- .../server/dao/user/UserService.java | 11 ++- .../server/dao/user/UserServiceImpl.java | 15 ++-- 15 files changed, 145 insertions(+), 144 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java index 76cc8f2f14..125febeb6a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -17,24 +17,25 @@ package org.thingsboard.server.controller; import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.model.JwtTokenPair; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; +import org.thingsboard.server.service.security.system.SystemSecurityService; /* * TODO [viacheslav]: * - Tests for 2FA * - Swagger documentation - * * */ -// TODO [viacheslav]: maybe get rid of sessionId concept.. /* * @@ -51,6 +52,7 @@ public class TwoFactorAuthController extends BaseController { private final TwoFactorAuthService twoFactorAuthService; private final JwtTokenFactory tokenFactory; + private final SystemSecurityService systemSecurityService; @PostMapping("/verification/send") @@ -62,14 +64,17 @@ public class TwoFactorAuthController extends BaseController { @PostMapping("/verification/check") @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") - public JwtTokenPair checkTwoFaVerificationCode(@RequestParam String verificationCode) throws Exception { + public JwtTokenPair checkTwoFaVerificationCode(@RequestParam String verificationCode, Authentication authentication) throws Exception { SecurityUser user = getCurrentUser(); boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, verificationCode, true); if (verificationSuccess) { - // FIXME [viacheslav]: log login action + systemSecurityService.logLoginAction(user, authentication, ActionType.LOGIN, null); return tokenFactory.createTokenPair(user); } else { - throw new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.AUTHENTICATION); + ThingsboardException error = new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.AUTHENTICATION); + // FIXME [viacheslav]: log login action: no authentication details + systemSecurityService.logLoginAction(user, authentication, ActionType.LOGIN, error); + throw error; } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java index b744adcdaf..665451b61f 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java @@ -33,11 +33,11 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; -import org.thingsboard.server.service.security.auth.TokenOutdatingService; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.auth.RefreshAuthenticationToken; +import org.thingsboard.server.service.security.auth.TokenOutdatingService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; @@ -66,7 +66,6 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide } else { securityUser = authenticateByPublicId(principal.getValue()); } - securityUser.setSessionId(unsafeUser.getSessionId()); if (tokenOutdatingService.isOutdated(rawAccessToken, securityUser.getId())) { throw new CredentialsExpiredException("Token is outdated"); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java index 98cbeb0786..a06184ff6c 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager; @@ -50,30 +51,29 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { private final UserService userService; private final Map> providers = new EnumMap<>(TwoFactorAuthProviderType.class); - // FIXME [viacheslav]: remove from the map // TODO [viacheslav]: these rate limits are local, and will work bad in the cluster - private final ConcurrentMap verificationCodeSendingRateLimits = new ConcurrentHashMap<>(); - private final ConcurrentMap verificationCodeCheckingRateLimits = new ConcurrentHashMap<>(); + private final ConcurrentMap verificationCodeSendingRateLimits = new ConcurrentHashMap<>(); + private final ConcurrentMap verificationCodeCheckingRateLimits = new ConcurrentHashMap<>(); - private static final ThingsboardException ACCOUNT_NOT_CONFIGURED = new ThingsboardException("2FA is not configured for account", ThingsboardErrorCode.BAD_REQUEST_PARAMS); - private static final ThingsboardException PROVIDER_NOT_CONFIGURED = new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS); - private static final ThingsboardException PROVIDER_NOT_AVAILABLE = new ThingsboardException("2FA provider is not available", ThingsboardErrorCode.GENERAL); + private static final ThingsboardException ACCOUNT_NOT_CONFIGURED_ERROR = new ThingsboardException("2FA is not configured for account", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + private static final ThingsboardException PROVIDER_NOT_CONFIGURED_ERROR = new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + private static final ThingsboardException PROVIDER_NOT_AVAILABLE_ERROR = new ThingsboardException("2FA provider is not available", ThingsboardErrorCode.GENERAL); @Override public void prepareVerificationCode(SecurityUser securityUser, boolean rateLimit) throws Exception { TwoFactorAuthAccountConfig accountConfig = configManager.getTwoFaAccountConfig(securityUser.getTenantId(), securityUser.getId()) - .orElseThrow(() -> ACCOUNT_NOT_CONFIGURED); + .orElseThrow(() -> ACCOUNT_NOT_CONFIGURED_ERROR); prepareVerificationCode(securityUser, accountConfig, rateLimit); } @Override public void prepareVerificationCode(SecurityUser securityUser, TwoFactorAuthAccountConfig accountConfig, boolean rateLimit) throws ThingsboardException { TwoFactorAuthSettings twoFaSettings = configManager.getTwoFaSettings(securityUser.getTenantId()) - .orElseThrow(() -> PROVIDER_NOT_CONFIGURED); + .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); if (rateLimit) { if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeSendRateLimit())) { - TbRateLimits rateLimits = verificationCodeSendingRateLimits.computeIfAbsent(securityUser.getSessionId(), sessionId -> { + TbRateLimits rateLimits = verificationCodeSendingRateLimits.computeIfAbsent(securityUser.getId(), sessionId -> { return new TbRateLimits(twoFaSettings.getVerificationCodeSendRateLimit()); }); if (!rateLimits.tryConsume()) { @@ -83,14 +83,14 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { } TwoFactorAuthProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType()) - .orElseThrow(() -> PROVIDER_NOT_CONFIGURED); + .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); getTwoFaProvider(accountConfig.getProviderType()).prepareVerificationCode(securityUser, providerConfig, accountConfig); } @Override public boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, boolean rateLimit) throws ThingsboardException { TwoFactorAuthAccountConfig accountConfig = configManager.getTwoFaAccountConfig(securityUser.getTenantId(), securityUser.getId()) - .orElseThrow(() -> ACCOUNT_NOT_CONFIGURED); + .orElseThrow(() -> ACCOUNT_NOT_CONFIGURED_ERROR); return checkVerificationCode(securityUser, verificationCode, accountConfig, rateLimit); } @@ -101,10 +101,10 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { } TwoFactorAuthSettings twoFaSettings = configManager.getTwoFaSettings(securityUser.getTenantId()) - .orElseThrow(() -> PROVIDER_NOT_CONFIGURED); + .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); if (rateLimit) { if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeCheckRateLimit())) { - TbRateLimits rateLimits = verificationCodeCheckingRateLimits.computeIfAbsent(securityUser.getSessionId(), sessionId -> { + TbRateLimits rateLimits = verificationCodeCheckingRateLimits.computeIfAbsent(securityUser.getId(), sessionId -> { return new TbRateLimits(twoFaSettings.getVerificationCodeCheckRateLimit()); }); if (!rateLimits.tryConsume()) { @@ -114,10 +114,14 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { } TwoFactorAuthProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType()) - .orElseThrow(() -> PROVIDER_NOT_CONFIGURED); + .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); boolean verificationSuccess = getTwoFaProvider(accountConfig.getProviderType()).checkVerificationCode(securityUser, verificationCode, providerConfig, accountConfig); if (rateLimit) { - systemSecurityService.validateTwoFaVerification(securityUser.getTenantId(), securityUser.getId(), verificationSuccess, twoFaSettings); + systemSecurityService.validateTwoFaVerification(securityUser, verificationSuccess, twoFaSettings); + if (verificationSuccess) { + verificationCodeCheckingRateLimits.remove(securityUser.getId()); + verificationCodeSendingRateLimits.remove(securityUser.getId()); + } } return verificationSuccess; } @@ -132,12 +136,12 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { private TwoFactorAuthProviderConfig getTwoFaProviderConfig(TenantId tenantId, TwoFactorAuthProviderType providerType) throws ThingsboardException { return configManager.getTwoFaSettings(tenantId) .flatMap(twoFaSettings -> twoFaSettings.getProviderConfig(providerType)) - .orElseThrow(() -> PROVIDER_NOT_CONFIGURED); + .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); } private TwoFactorAuthProvider getTwoFaProvider(TwoFactorAuthProviderType providerType) throws ThingsboardException { return Optional.ofNullable(providers.get(providerType)) - .orElseThrow(() -> PROVIDER_NOT_AVAILABLE); + .orElseThrow(() -> PROVIDER_NOT_AVAILABLE_ERROR); } @Autowired diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java index e70742267b..0866aafbe1 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java @@ -17,14 +17,11 @@ package org.thingsboard.server.service.security.auth.mfa.config; import io.swagger.annotations.ApiModelProperty; import lombok.Data; -import org.checkerframework.checker.index.qual.NonNegative; import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; import javax.validation.Valid; -import javax.validation.constraints.AssertTrue; import javax.validation.constraints.Min; -import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; import java.util.List; import java.util.Optional; @@ -42,9 +39,10 @@ public class TwoFactorAuthSettings { @ApiModelProperty(example = "3:900 (3 requests per 15 minutes)") @Pattern(regexp = "[^0]\\d+:[^0]\\d+", message = "Rate limit configuration is invalid") private String verificationCodeCheckRateLimit; + @ApiModelProperty(example = "10") @Min(0) private int maxCodeVerificationFailuresBeforeUserLockout; - @ApiModelProperty(value = "in seconds") + @ApiModelProperty(value = "in minutes", example = "60") @Min(1) private int totalAllowedTimeForVerification; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java index 858b5995f7..030482ac6d 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java @@ -25,9 +25,9 @@ public interface TwoFactorAuthProvider TimeUnit.SECONDS.toMillis(providerConfig.getVerificationCodeLifetime())) { - verificationCodesCache.evict(user.getSessionId()); + verificationCodesCache.evict(securityUser.getId()); return false; } if (verificationCode.equals(correctVerificationCode.getValue()) && correctVerificationCode.getConfig().equals(accountConfig)) { - verificationCodesCache.evict(user.getSessionId()); + verificationCodesCache.evict(securityUser.getId()); return true; } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFactorAuthProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFactorAuthProvider.java index 65574fc760..83767ee3d8 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFactorAuthProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFactorAuthProvider.java @@ -45,7 +45,7 @@ public class TotpTwoFactorAuthProvider implements TwoFactorAuthProvider authorities; private boolean enabled; private UserPrincipal userPrincipal; - private String sessionId; public SecurityUser() { super(); @@ -46,7 +44,6 @@ public class SecurityUser extends User { super(user); this.enabled = enabled; this.userPrincipal = userPrincipal; - this.sessionId = UUID.randomUUID().toString(); } public Collection getAuthorities() { @@ -74,12 +71,4 @@ public class SecurityUser extends User { this.userPrincipal = userPrincipal; } - public String getSessionId() { - return sessionId; - } - - public void setSessionId(String sessionId) { - this.sessionId = sessionId; - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index 36f64b3566..8121ebf834 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -60,7 +60,6 @@ public class JwtTokenFactory { private static final String IS_PUBLIC = "isPublic"; private static final String TENANT_ID = "tenantId"; private static final String CUSTOMER_ID = "customerId"; - private static final String SESSION_ID = "sessionId"; private final JwtSettings settings; @@ -116,7 +115,6 @@ public class JwtTokenFactory { } else if (securityUser.getAuthority() == Authority.SYS_ADMIN) { securityUser.setTenantId(TenantId.SYS_TENANT_ID); } - securityUser.setSessionId(claims.get(SESSION_ID, String.class)); if (securityUser.getAuthority() != Authority.PRE_VERIFICATION_TOKEN) { securityUser.setFirstName(claims.get(FIRST_NAME, String.class)); @@ -162,7 +160,6 @@ public class JwtTokenFactory { UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject); SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class)))); securityUser.setUserPrincipal(principal); - securityUser.setSessionId(claims.get(SESSION_ID, String.class)); return securityUser; } @@ -183,9 +180,6 @@ public class JwtTokenFactory { Claims claims = Jwts.claims().setSubject(principal.getValue()); claims.put(USER_ID, securityUser.getId().getId().toString()); claims.put(SCOPES, scopes); - if (securityUser.getSessionId() != null) { - claims.put(SESSION_ID, securityUser.getSessionId()); - } ZonedDateTime currentTime = ZonedDateTime.now(); diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java index 646562f9ed..6e6815ff5e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java @@ -34,6 +34,7 @@ import org.springframework.cache.annotation.Cacheable; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.DisabledException; import org.springframework.security.authentication.LockedException; +import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; @@ -41,6 +42,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; @@ -48,20 +50,23 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; +import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserServiceImpl; import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; +import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.exception.UserPasswordExpiredException; +import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.utils.MiscUtils; +import ua_parser.Client; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.concurrent.TimeUnit; import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTINGS_CACHE; @@ -82,6 +87,9 @@ public class DefaultSystemSecurityService implements SystemSecurityService { @Autowired private MailService mailService; + @Autowired + private AuditLogService auditLogService; + @Resource private SystemSecurityService self; @@ -124,7 +132,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService { @Override public void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String username, String password) throws AuthenticationException { if (!encoder.matches(password, userCredentials.getPassword())) { - int failedLoginAttempts = userService.onUserLoginIncorrectCredentials(tenantId, userCredentials.getUserId()); + int failedLoginAttempts = userService.increaseFailedLoginAttempts(tenantId, userCredentials.getUserId()); SecuritySettings securitySettings = self.getSecuritySettings(tenantId); if (securitySettings.getMaxFailedLoginAttempts() != null && securitySettings.getMaxFailedLoginAttempts() > 0) { if (failedLoginAttempts > securitySettings.getMaxFailedLoginAttempts() && userCredentials.isEnabled()) { @@ -139,8 +147,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService { throw new DisabledException("User is not active"); } - // FIXME [viacheslav]: don't do that in case of 2FA. maybe just move underlying setLastLoginTs to logLoginAction ? - userService.onUserLoginSuccessful(tenantId, userCredentials.getUserId()); + userService.resetFailedLoginAttempts(tenantId, userCredentials.getUserId()); SecuritySettings securitySettings = self.getSecuritySettings(tenantId); if (isPositiveInteger(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays())) { @@ -154,27 +161,21 @@ public class DefaultSystemSecurityService implements SystemSecurityService { } @Override - public void validateTwoFaVerification(TenantId tenantId, UserId userId, boolean verificationSuccess, TwoFactorAuthSettings twoFaSettings) { - User user = userService.findUserById(tenantId, userId); - ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(user.getAdditionalInfo()) - .filter(jsonNode -> jsonNode instanceof ObjectNode) - .orElseGet(JacksonUtil::newObjectNode); - // TODO [viacheslav]: test ! - int failedVerificationAttempts = Optional.ofNullable(additionalInfo.get("failedTwoFaVerificationAttempts")) - .map(JsonNode::asInt).orElse(0); + public void validateTwoFaVerification(SecurityUser securityUser, boolean verificationSuccess, TwoFactorAuthSettings twoFaSettings) { + TenantId tenantId = securityUser.getTenantId(); + UserId userId = securityUser.getId(); + int failedVerificationAttempts = 0; if (!verificationSuccess) { - failedVerificationAttempts++; - // TODO [viacheslav]: maybe use userService.onUserLoginIncorrectCredentials() + failedVerificationAttempts = userService.increaseFailedLoginAttempts(tenantId, userId); } else { - failedVerificationAttempts = 0; - // and set last login ts + userService.resetFailedLoginAttempts(tenantId, userId); } if (twoFaSettings.getMaxCodeVerificationFailuresBeforeUserLockout() > 0 && failedVerificationAttempts >= twoFaSettings.getMaxCodeVerificationFailuresBeforeUserLockout()) { userService.setUserCredentialsEnabled(TenantId.SYS_TENANT_ID, userId, false); - lockAccount(userId, user.getEmail(), self.getSecuritySettings(tenantId)); + lockAccount(userId, securityUser.getEmail(), self.getSecuritySettings(tenantId)); throw new LockedException("User account was locked due to exceeded 2FA verification attempts"); } } @@ -255,6 +256,59 @@ public class DefaultSystemSecurityService implements SystemSecurityService { return baseUrl; } + @Override + public void logLoginAction(User user, Authentication authentication, ActionType actionType, Exception e) { + String clientAddress = "Unknown"; + String browser = "Unknown"; + String os = "Unknown"; + String device = "Unknown"; + if (authentication != null && authentication.getDetails() != null) { + if (authentication.getDetails() instanceof RestAuthenticationDetails) { + RestAuthenticationDetails details = (RestAuthenticationDetails) authentication.getDetails(); + clientAddress = details.getClientAddress(); + if (details.getUserAgent() != null) { + Client userAgent = details.getUserAgent(); + if (userAgent.userAgent != null) { + browser = userAgent.userAgent.family; + if (userAgent.userAgent.major != null) { + browser += " " + userAgent.userAgent.major; + if (userAgent.userAgent.minor != null) { + browser += "." + userAgent.userAgent.minor; + if (userAgent.userAgent.patch != null) { + browser += "." + userAgent.userAgent.patch; + } + } + } + } + if (userAgent.os != null) { + os = userAgent.os.family; + if (userAgent.os.major != null) { + os += " " + userAgent.os.major; + if (userAgent.os.minor != null) { + os += "." + userAgent.os.minor; + if (userAgent.os.patch != null) { + os += "." + userAgent.os.patch; + if (userAgent.os.patchMinor != null) { + os += "." + userAgent.os.patchMinor; + } + } + } + } + } + if (userAgent.device != null) { + device = userAgent.device.family; + } + } + } + } + if (actionType == ActionType.LOGIN && e == null) { + userService.setLastLoginTs(user.getTenantId(), user.getId()); + } + auditLogService.logEntityAction( + user.getTenantId(), user.getCustomerId(), user.getId(), + user.getName(), user.getId(), null, actionType, e, clientAddress, browser, os, device); + } + private static boolean isPositiveInteger(Integer val) { return val != null && val.intValue() > 0; } diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java index 453251bf03..f6b401da4e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java @@ -15,14 +15,17 @@ */ package org.thingsboard.server.service.security.system; +import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.UserCredentials; -import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.common.data.security.model.SecuritySettings; +import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; +import org.thingsboard.server.service.security.model.SecurityUser; import javax.servlet.http.HttpServletRequest; @@ -34,10 +37,12 @@ public interface SystemSecurityService { void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String username, String password) throws AuthenticationException; - void validateTwoFaVerification(TenantId tenantId, UserId userId, boolean verificationSuccess, TwoFactorAuthSettings twoFaSettings); + void validateTwoFaVerification(SecurityUser securityUser, boolean verificationSuccess, TwoFactorAuthSettings twoFaSettings); void validatePassword(TenantId tenantId, String password, UserCredentials userCredentials) throws DataValidationException; String getBaseUrl(TenantId tenantId, CustomerId customerId, HttpServletRequest httpServletRequest); + void logLoginAction(User user, Authentication authentication, ActionType actionType, Exception e); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java index e8ddce587e..d5d29efa40 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java @@ -56,16 +56,19 @@ public interface UserService { PageData findUsersByTenantId(TenantId tenantId, PageLink pageLink); PageData findTenantAdmins(TenantId tenantId, PageLink pageLink); - + void deleteTenantAdmins(TenantId tenantId); PageData findCustomerUsers(TenantId tenantId, CustomerId customerId, PageLink pageLink); - + void deleteCustomerUsers(TenantId tenantId, CustomerId customerId); void setUserCredentialsEnabled(TenantId tenantId, UserId userId, boolean enabled); - void onUserLoginSuccessful(TenantId tenantId, UserId userId); + void resetFailedLoginAttempts(TenantId tenantId, UserId userId); + + int increaseFailedLoginAttempts(TenantId tenantId, UserId userId); + + void setLastLoginTs(TenantId tenantId, UserId userId); - int onUserLoginIncorrectCredentials(TenantId tenantId, UserId userId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index 1ee78d4068..b8c7b8e9f9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -298,34 +298,35 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic @Override - public void onUserLoginSuccessful(TenantId tenantId, UserId userId) { + public void resetFailedLoginAttempts(TenantId tenantId, UserId userId) { log.trace("Executing onUserLoginSuccessful [{}]", userId); User user = findUserById(tenantId, userId); - setLastLoginTs(user); // FIXME [viacheslav]: move to logLoginAction ? resetFailedLoginAttempts(user); saveUser(user); } - private void setLastLoginTs(User user) { + private void resetFailedLoginAttempts(User user) { JsonNode additionalInfo = user.getAdditionalInfo(); if (!(additionalInfo instanceof ObjectNode)) { additionalInfo = JacksonUtil.newObjectNode(); } - ((ObjectNode) additionalInfo).put(LAST_LOGIN_TS, System.currentTimeMillis()); + ((ObjectNode) additionalInfo).put(FAILED_LOGIN_ATTEMPTS, 0); user.setAdditionalInfo(additionalInfo); } - private void resetFailedLoginAttempts(User user) { + @Override + public void setLastLoginTs(TenantId tenantId, UserId userId) { + User user = findUserById(tenantId, userId); JsonNode additionalInfo = user.getAdditionalInfo(); if (!(additionalInfo instanceof ObjectNode)) { additionalInfo = JacksonUtil.newObjectNode(); } - ((ObjectNode) additionalInfo).put(FAILED_LOGIN_ATTEMPTS, 0); + ((ObjectNode) additionalInfo).put(LAST_LOGIN_TS, System.currentTimeMillis()); user.setAdditionalInfo(additionalInfo); } @Override - public int onUserLoginIncorrectCredentials(TenantId tenantId, UserId userId) { + public int increaseFailedLoginAttempts(TenantId tenantId, UserId userId) { log.trace("Executing onUserLoginIncorrectCredentials [{}]", userId); User user = findUserById(tenantId, userId); int failedLoginAttempts = increaseFailedLoginAttempts(user); From ea7f559e23bd8429f6d10835b99318dd77f17c33 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Sun, 20 Mar 2022 14:59:25 +0200 Subject: [PATCH 011/276] 2FA: log login action, fix user lockout --- .../controller/TwoFactorAuthController.java | 10 +++-- .../mfa/config/TwoFactorAuthSettings.java | 2 +- .../TotpTwoFactorAuthAccountConfig.java | 2 +- .../auth/rest/RestAuthenticationProvider.java | 6 +-- .../system/DefaultSystemSecurityService.java | 44 +++++++++---------- .../system/SystemSecurityService.java | 3 +- .../resources/templates/account.lockout.ftl | 2 +- .../server/controller/TwoFactorAuthTest.java | 2 +- 8 files changed, 35 insertions(+), 36 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java index 125febeb6a..1b1983cdf8 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -26,11 +26,14 @@ import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; +import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.model.JwtTokenPair; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.system.SystemSecurityService; +import javax.servlet.http.HttpServletRequest; + /* * TODO [viacheslav]: * - Tests for 2FA @@ -64,16 +67,15 @@ public class TwoFactorAuthController extends BaseController { @PostMapping("/verification/check") @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") - public JwtTokenPair checkTwoFaVerificationCode(@RequestParam String verificationCode, Authentication authentication) throws Exception { + public JwtTokenPair checkTwoFaVerificationCode(@RequestParam String verificationCode, HttpServletRequest servletRequest) throws Exception { SecurityUser user = getCurrentUser(); boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, verificationCode, true); if (verificationSuccess) { - systemSecurityService.logLoginAction(user, authentication, ActionType.LOGIN, null); + systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, null); return tokenFactory.createTokenPair(user); } else { ThingsboardException error = new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.AUTHENTICATION); - // FIXME [viacheslav]: log login action: no authentication details - systemSecurityService.logLoginAction(user, authentication, ActionType.LOGIN, error); + systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, error); throw error; } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java index 0866aafbe1..ae39065e74 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java @@ -41,7 +41,7 @@ public class TwoFactorAuthSettings { private String verificationCodeCheckRateLimit; @ApiModelProperty(example = "10") @Min(0) - private int maxCodeVerificationFailuresBeforeUserLockout; + private int maxVerificationFailuresBeforeUserLockout; @ApiModelProperty(value = "in minutes", example = "60") @Min(1) private int totalAllowedTimeForVerification; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java index 78ab4cf80b..93975d49ad 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java @@ -25,7 +25,7 @@ import javax.validation.constraints.Pattern; public class TotpTwoFactorAuthAccountConfig implements TwoFactorAuthAccountConfig { @NotBlank -// @Pattern(regexp = ) // TODO [viacheslav]: validate otp auth url by pattern +// @Pattern(regexp = "otpauth://totp/") // FIXME [viacheslav]: validate otp auth url by pattern private String authUrl; @Override diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java index 8429c70506..71d16d46d0 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java @@ -85,7 +85,7 @@ public class RestAuthenticationProvider implements AuthenticationProvider { if (twoFactorAuthConfigManager.isTwoFaEnabled(securityUser)) { return new MfaAuthenticationToken(securityUser); } else { - systemSecurityService.logLoginAction((User) authentication.getPrincipal(), authentication, ActionType.LOGIN, null); + systemSecurityService.logLoginAction((User) authentication.getPrincipal(), authentication.getDetails(), ActionType.LOGIN, null); } } else { String publicId = userPrincipal.getValue(); @@ -111,7 +111,7 @@ public class RestAuthenticationProvider implements AuthenticationProvider { try { systemSecurityService.validateUserCredentials(user.getTenantId(), userCredentials, username, password); } catch (LockedException e) { - systemSecurityService.logLoginAction(user, authentication, ActionType.LOCKOUT, null); + systemSecurityService.logLoginAction(user, authentication.getDetails(), ActionType.LOCKOUT, null); throw e; } @@ -120,7 +120,7 @@ public class RestAuthenticationProvider implements AuthenticationProvider { return new SecurityUser(user, userCredentials.isEnabled(), userPrincipal); } catch (Exception e) { - systemSecurityService.logLoginAction(user, authentication, ActionType.LOGIN, e); + systemSecurityService.logLoginAction(user, authentication.getDetails(), ActionType.LOGIN, e); throw e; } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java index 6e6815ff5e..1f19b9c19b 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java @@ -34,7 +34,6 @@ import org.springframework.cache.annotation.Cacheable; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.DisabledException; import org.springframework.security.authentication.LockedException; -import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; @@ -136,7 +135,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService { SecuritySettings securitySettings = self.getSecuritySettings(tenantId); if (securitySettings.getMaxFailedLoginAttempts() != null && securitySettings.getMaxFailedLoginAttempts() > 0) { if (failedLoginAttempts > securitySettings.getMaxFailedLoginAttempts() && userCredentials.isEnabled()) { - lockAccount(userCredentials.getUserId(), username, securitySettings); + lockAccount(userCredentials.getUserId(), username, securitySettings.getUserLockoutNotificationEmail(), securitySettings.getMaxFailedLoginAttempts()); throw new LockedException("Authentication Failed. Username was locked due to security policy."); } } @@ -172,21 +171,22 @@ public class DefaultSystemSecurityService implements SystemSecurityService { userService.resetFailedLoginAttempts(tenantId, userId); } - if (twoFaSettings.getMaxCodeVerificationFailuresBeforeUserLockout() > 0 - && failedVerificationAttempts >= twoFaSettings.getMaxCodeVerificationFailuresBeforeUserLockout()) { + if (twoFaSettings.getMaxVerificationFailuresBeforeUserLockout() > 0 + && failedVerificationAttempts >= twoFaSettings.getMaxVerificationFailuresBeforeUserLockout()) { userService.setUserCredentialsEnabled(TenantId.SYS_TENANT_ID, userId, false); - lockAccount(userId, securityUser.getEmail(), self.getSecuritySettings(tenantId)); + SecuritySettings securitySettings = self.getSecuritySettings(tenantId); + lockAccount(userId, securityUser.getEmail(), securitySettings.getUserLockoutNotificationEmail(), twoFaSettings.getMaxVerificationFailuresBeforeUserLockout()); throw new LockedException("User account was locked due to exceeded 2FA verification attempts"); } } - private void lockAccount(UserId userId, String username, SecuritySettings securitySettings) { + private void lockAccount(UserId userId, String username, String userLockoutNotificationEmail, Integer maxFailedLoginAttempts) { userService.setUserCredentialsEnabled(TenantId.SYS_TENANT_ID, userId, false); - if (StringUtils.isNoneBlank(securitySettings.getUserLockoutNotificationEmail())) { + if (StringUtils.isNotBlank(userLockoutNotificationEmail)) { try { - mailService.sendAccountLockoutEmail(username, securitySettings.getUserLockoutNotificationEmail(), securitySettings.getMaxFailedLoginAttempts()); + mailService.sendAccountLockoutEmail(username, userLockoutNotificationEmail, maxFailedLoginAttempts); } catch (ThingsboardException e) { - log.warn("Can't send email regarding user account [{}] lockout to provided email [{}]", username, securitySettings.getUserLockoutNotificationEmail(), e); + log.warn("Can't send email regarding user account [{}] lockout to provided email [{}]", username, userLockoutNotificationEmail, e); } } } @@ -257,23 +257,22 @@ public class DefaultSystemSecurityService implements SystemSecurityService { } @Override - public void logLoginAction(User user, Authentication authentication, ActionType actionType, Exception e) { + public void logLoginAction(User user, Object authenticationDetails, ActionType actionType, Exception e) { String clientAddress = "Unknown"; String browser = "Unknown"; String os = "Unknown"; String device = "Unknown"; - if (authentication != null && authentication.getDetails() != null) { - if (authentication.getDetails() instanceof RestAuthenticationDetails) { - RestAuthenticationDetails details = (RestAuthenticationDetails) authentication.getDetails(); - clientAddress = details.getClientAddress(); - if (details.getUserAgent() != null) { - Client userAgent = details.getUserAgent(); - if (userAgent.userAgent != null) { - browser = userAgent.userAgent.family; - if (userAgent.userAgent.major != null) { - browser += " " + userAgent.userAgent.major; - if (userAgent.userAgent.minor != null) { - browser += "." + userAgent.userAgent.minor; + if (authenticationDetails instanceof RestAuthenticationDetails) { + RestAuthenticationDetails details = (RestAuthenticationDetails) authenticationDetails; + clientAddress = details.getClientAddress(); + if (details.getUserAgent() != null) { + Client userAgent = details.getUserAgent(); + if (userAgent.userAgent != null) { + browser = userAgent.userAgent.family; + if (userAgent.userAgent.major != null) { + browser += " " + userAgent.userAgent.major; + if (userAgent.userAgent.minor != null) { + browser += "." + userAgent.userAgent.minor; if (userAgent.userAgent.patch != null) { browser += "." + userAgent.userAgent.patch; } @@ -300,7 +299,6 @@ public class DefaultSystemSecurityService implements SystemSecurityService { } } } - } if (actionType == ActionType.LOGIN && e == null) { userService.setLastLoginTs(user.getTenantId(), user.getId()); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java index f6b401da4e..4c14e45ae6 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.security.system; -import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; @@ -43,6 +42,6 @@ public interface SystemSecurityService { String getBaseUrl(TenantId tenantId, CustomerId customerId, HttpServletRequest httpServletRequest); - void logLoginAction(User user, Authentication authentication, ActionType actionType, Exception e); + void logLoginAction(User user, Object authenticationDetails, ActionType actionType, Exception e); } diff --git a/application/src/main/resources/templates/account.lockout.ftl b/application/src/main/resources/templates/account.lockout.ftl index 9832f8323d..fa21653cb9 100644 --- a/application/src/main/resources/templates/account.lockout.ftl +++ b/application/src/main/resources/templates/account.lockout.ftl @@ -88,7 +88,7 @@ background-color: #f6f6f6; - Thingsboard user account ${lockoutAccount} has been lockout due to failed credentials were provided more than ${maxFailedLoginAttempts} times. + Thingsboard user account ${lockoutAccount} has been locked out due to multiple authentication failures (more than ${maxFailedLoginAttempts}). diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java index b338d0d99d..69bd49f6f5 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java @@ -44,8 +44,8 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -// TODO [viacheslav]: test sessionId // TODO [viacheslav]: test validation for all account configs, provider configs and two factor auth settings +// TODO [viacheslav]: test authentication details, log login action, last login ts, rate limiting, user blocking, etc public abstract class TwoFactorAuthTest extends AbstractControllerTest { @SpyBean From 062af3af810f47f10dc719f7be883e420968e800 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Sun, 20 Mar 2022 15:43:38 +0200 Subject: [PATCH 012/276] 2FA: cleanup code --- .../server/config/JwtSettings.java | 6 ------ .../config/RateLimitProcessingFilter.java | 3 --- .../TwoFactorAuthConfigController.java | 2 ++ .../controller/TwoFactorAuthController.java | 2 ++ .../RefreshTokenAuthenticationProvider.java | 2 +- .../auth/mfa/DefaultTwoFactorAuthService.java | 20 ++++++++++-------- .../auth/mfa/TwoFactorAuthService.java | 8 +++---- .../provider/TwoFactorAuthProviderType.java | 3 +-- .../impl/OtpBasedTwoFactorAuthProvider.java | 5 +++-- .../auth/rest/RestAuthenticationProvider.java | 2 +- ...RestAwareAuthenticationSuccessHandler.java | 6 +++--- .../security/model/token/JwtTokenFactory.java | 2 +- .../system/DefaultSystemSecurityService.java | 3 ++- .../src/main/resources/thingsboard.yml | 4 +--- .../common/util/ThrowingBiConsumer.java | 21 ------------------- .../common/util/ThrowingBiFunction.java | 21 ------------------- .../common/util/ThrowingTripleConsumer.java | 21 ------------------- .../common/util/ThrowingTripleFunction.java | 21 ------------------- .../rule/engine/api/util/TbNodeUtils.java | 6 +----- 19 files changed, 33 insertions(+), 125 deletions(-) delete mode 100644 common/util/src/main/java/org/thingsboard/common/util/ThrowingBiConsumer.java delete mode 100644 common/util/src/main/java/org/thingsboard/common/util/ThrowingBiFunction.java delete mode 100644 common/util/src/main/java/org/thingsboard/common/util/ThrowingTripleConsumer.java delete mode 100644 common/util/src/main/java/org/thingsboard/common/util/ThrowingTripleFunction.java diff --git a/application/src/main/java/org/thingsboard/server/config/JwtSettings.java b/application/src/main/java/org/thingsboard/server/config/JwtSettings.java index 1c15c4c150..95e510612a 100644 --- a/application/src/main/java/org/thingsboard/server/config/JwtSettings.java +++ b/application/src/main/java/org/thingsboard/server/config/JwtSettings.java @@ -44,10 +44,4 @@ public class JwtSettings { */ private Integer refreshTokenExpTime; - /** - * Issued when 2FA is being used. - * Valid only for 2FA verification code checking. - * */ - private Integer preVerificationTokenExpirationTime; - } diff --git a/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java b/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java index 2756b9a647..e2e6f8d982 100644 --- a/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java +++ b/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java @@ -15,11 +15,8 @@ */ package org.thingsboard.server.config; -import io.github.bucket4j.Bucket4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.cache.Cache; -import org.springframework.cache.jcache.JCacheCacheManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java index 12f0824ff1..5d3e3bf116 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java @@ -31,6 +31,7 @@ import org.springframework.web.bind.annotation.RestController; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager; import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig; @@ -45,6 +46,7 @@ import javax.validation.Valid; @RestController @RequestMapping("/api/2fa") +@TbCoreComponent @RequiredArgsConstructor public class TwoFactorAuthConfigController extends BaseController { diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java index 1b1983cdf8..ff6417a3aa 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -25,6 +25,7 @@ import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.model.JwtTokenPair; @@ -50,6 +51,7 @@ import javax.servlet.http.HttpServletRequest; * */ @RestController @RequestMapping("/api/auth/2fa") +@TbCoreComponent @RequiredArgsConstructor public class TwoFactorAuthController extends BaseController { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java index 665451b61f..8003cfd012 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java @@ -33,11 +33,11 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.service.security.auth.TokenOutdatingService; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.auth.RefreshAuthenticationToken; -import org.thingsboard.server.service.security.auth.TokenOutdatingService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java index a06184ff6c..c8117ec792 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager; import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; @@ -44,6 +45,7 @@ import java.util.concurrent.ConcurrentMap; @Service @RequiredArgsConstructor +@TbCoreComponent public class DefaultTwoFactorAuthService implements TwoFactorAuthService { private final TwoFactorAuthConfigManager configManager; @@ -61,17 +63,17 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { @Override - public void prepareVerificationCode(SecurityUser securityUser, boolean rateLimit) throws Exception { + public void prepareVerificationCode(SecurityUser securityUser, boolean checkLimits) throws Exception { TwoFactorAuthAccountConfig accountConfig = configManager.getTwoFaAccountConfig(securityUser.getTenantId(), securityUser.getId()) .orElseThrow(() -> ACCOUNT_NOT_CONFIGURED_ERROR); - prepareVerificationCode(securityUser, accountConfig, rateLimit); + prepareVerificationCode(securityUser, accountConfig, checkLimits); } @Override - public void prepareVerificationCode(SecurityUser securityUser, TwoFactorAuthAccountConfig accountConfig, boolean rateLimit) throws ThingsboardException { + public void prepareVerificationCode(SecurityUser securityUser, TwoFactorAuthAccountConfig accountConfig, boolean checkLimits) throws ThingsboardException { TwoFactorAuthSettings twoFaSettings = configManager.getTwoFaSettings(securityUser.getTenantId()) .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); - if (rateLimit) { + if (checkLimits) { if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeSendRateLimit())) { TbRateLimits rateLimits = verificationCodeSendingRateLimits.computeIfAbsent(securityUser.getId(), sessionId -> { return new TbRateLimits(twoFaSettings.getVerificationCodeSendRateLimit()); @@ -88,21 +90,21 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { } @Override - public boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, boolean rateLimit) throws ThingsboardException { + public boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, boolean checkLimits) throws ThingsboardException { TwoFactorAuthAccountConfig accountConfig = configManager.getTwoFaAccountConfig(securityUser.getTenantId(), securityUser.getId()) .orElseThrow(() -> ACCOUNT_NOT_CONFIGURED_ERROR); - return checkVerificationCode(securityUser, verificationCode, accountConfig, rateLimit); + return checkVerificationCode(securityUser, verificationCode, accountConfig, checkLimits); } @Override - public boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, TwoFactorAuthAccountConfig accountConfig, boolean rateLimit) throws ThingsboardException { + public boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, TwoFactorAuthAccountConfig accountConfig, boolean checkLimits) throws ThingsboardException { if (!userService.findUserCredentialsByUserId(securityUser.getTenantId(), securityUser.getId()).isEnabled()) { throw new ThingsboardException("User is disabled", ThingsboardErrorCode.AUTHENTICATION); } TwoFactorAuthSettings twoFaSettings = configManager.getTwoFaSettings(securityUser.getTenantId()) .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); - if (rateLimit) { + if (checkLimits) { if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeCheckRateLimit())) { TbRateLimits rateLimits = verificationCodeCheckingRateLimits.computeIfAbsent(securityUser.getId(), sessionId -> { return new TbRateLimits(twoFaSettings.getVerificationCodeCheckRateLimit()); @@ -116,7 +118,7 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { TwoFactorAuthProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType()) .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); boolean verificationSuccess = getTwoFaProvider(accountConfig.getProviderType()).checkVerificationCode(securityUser, verificationCode, providerConfig, accountConfig); - if (rateLimit) { + if (checkLimits) { systemSecurityService.validateTwoFaVerification(securityUser, verificationSuccess, twoFaSettings); if (verificationSuccess) { verificationCodeCheckingRateLimits.remove(securityUser.getId()); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java index ec2511e62a..e07ac2b2fa 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java @@ -23,13 +23,13 @@ import org.thingsboard.server.service.security.model.SecurityUser; public interface TwoFactorAuthService { - void prepareVerificationCode(SecurityUser securityUser, boolean rateLimit) throws Exception; + void prepareVerificationCode(SecurityUser securityUser, boolean checkLimits) throws Exception; - void prepareVerificationCode(SecurityUser securityUser, TwoFactorAuthAccountConfig accountConfig, boolean rateLimit) throws ThingsboardException; + void prepareVerificationCode(SecurityUser securityUser, TwoFactorAuthAccountConfig accountConfig, boolean checkLimits) throws ThingsboardException; - boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, boolean rateLimit) throws ThingsboardException; + boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, boolean checkLimits) throws ThingsboardException; - boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, TwoFactorAuthAccountConfig accountConfig, boolean rateLimit) throws ThingsboardException; + boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, TwoFactorAuthAccountConfig accountConfig, boolean checkLimits) throws ThingsboardException; TwoFactorAuthAccountConfig generateNewAccountConfig(User user, TwoFactorAuthProviderType providerType) throws ThingsboardException; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProviderType.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProviderType.java index 4ddaf836e1..9a4a3672a7 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProviderType.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProviderType.java @@ -17,6 +17,5 @@ package org.thingsboard.server.service.security.auth.mfa.provider; public enum TwoFactorAuthProviderType { TOTP, - SMS, - EMAIL + SMS } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java index 0438c53e15..ab6c15e56b 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java @@ -57,7 +57,7 @@ public abstract class OtpBasedTwoFactorAuthProvider 0 diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index ba6dc222ab..188d2bfe26 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -127,8 +127,6 @@ security: jwt: tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:9000}" # Number of seconds (2.5 hours) refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:604800}" # Number of seconds (1 week) - # Number of seconds. Issued when 2FA is being used; valid only for checking 2FA verification code after which usual token pair is issued - preVerificationTokenExpirationTime: "${JWT_PRE_VERIFICATION_TOKEN_EXPIRATION_TIME:30}" tokenIssuer: "${JWT_TOKEN_ISSUER:thingsboard.io}" tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}" # Enable/disable access to Tenant Administrators JWT token by System Administrator or Customer Users JWT token by Tenant Administrator @@ -428,7 +426,7 @@ caffeine: timeToLiveInMinutes: "${CACHE_SPECS_EDGES_TTL:1440}" maxSize: "${CACHE_SPECS_EDGES_MAX_SIZE:10000}" twoFaVerificationCodes: - timeToLiveInMinutes: "${CACHE_SPECS_TWO_FA_VERIFICATION_CODES_TTL:1}" + timeToLiveInMinutes: "${CACHE_SPECS_TWO_FA_VERIFICATION_CODES_TTL:60}" maxSize: "${CACHE_SPECS_TWO_FA_VERIFICATION_CODES_MAX_SIZE:100000}" redis: diff --git a/common/util/src/main/java/org/thingsboard/common/util/ThrowingBiConsumer.java b/common/util/src/main/java/org/thingsboard/common/util/ThrowingBiConsumer.java deleted file mode 100644 index 269f9f75cb..0000000000 --- a/common/util/src/main/java/org/thingsboard/common/util/ThrowingBiConsumer.java +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.common.util; - -@FunctionalInterface -public interface ThrowingBiConsumer { - void accept(A a, B b) throws Exception; -} diff --git a/common/util/src/main/java/org/thingsboard/common/util/ThrowingBiFunction.java b/common/util/src/main/java/org/thingsboard/common/util/ThrowingBiFunction.java deleted file mode 100644 index 32058df26e..0000000000 --- a/common/util/src/main/java/org/thingsboard/common/util/ThrowingBiFunction.java +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.common.util; - -@FunctionalInterface -public interface ThrowingBiFunction { - R apply(A a, B b) throws Exception; -} diff --git a/common/util/src/main/java/org/thingsboard/common/util/ThrowingTripleConsumer.java b/common/util/src/main/java/org/thingsboard/common/util/ThrowingTripleConsumer.java deleted file mode 100644 index 5230da4863..0000000000 --- a/common/util/src/main/java/org/thingsboard/common/util/ThrowingTripleConsumer.java +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.common.util; - -@FunctionalInterface -public interface ThrowingTripleConsumer { - void accept(A a, B b, C c) throws Exception; -} diff --git a/common/util/src/main/java/org/thingsboard/common/util/ThrowingTripleFunction.java b/common/util/src/main/java/org/thingsboard/common/util/ThrowingTripleFunction.java deleted file mode 100644 index cade9d1717..0000000000 --- a/common/util/src/main/java/org/thingsboard/common/util/ThrowingTripleFunction.java +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.common.util; - -@FunctionalInterface -public interface ThrowingTripleFunction { - R apply(A a, B b, C c) throws Exception; -} diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java index 32ee585820..bc38db7fd6 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java @@ -94,11 +94,7 @@ public class TbNodeUtils { } public static String processPattern(String pattern, TbMsgMetaData metaData) { - String result = pattern; - for (Map.Entry keyVal : metaData.values().entrySet()) { - result = processVar(result, keyVal.getKey(), keyVal.getValue()); - } - return result; + return processTemplate(pattern, metaData.values()); } public static String processTemplate(String template, Map data) { From 1250fa2ba077c57329a30f20b8a8f7c4691a18d7 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Mon, 21 Mar 2022 14:09:51 +0200 Subject: [PATCH 013/276] Tests for 2FA config management; refactoring --- .../TwoFactorAuthConfigController.java | 2 +- .../controller/TwoFactorAuthController.java | 7 +- .../auth/mfa/DefaultTwoFactorAuthService.java | 6 +- .../DefaultTwoFactorAuthConfigManager.java | 37 +- .../config/TwoFactorAuthConfigManager.java | 7 +- .../mfa/config/TwoFactorAuthSettings.java | 10 +- .../OtpBasedTwoFactorAuthAccountConfig.java | 3 + .../SmsTwoFactorAuthAccountConfig.java | 4 +- .../TotpTwoFactorAuthAccountConfig.java | 6 +- .../OtpBasedTwoFactorAuthProviderConfig.java | 2 +- .../SmsTwoFactorAuthProviderConfig.java | 2 +- .../impl/OtpBasedTwoFactorAuthProvider.java | 6 +- .../auth/rest/RestAuthenticationProvider.java | 2 +- ...RestAwareAuthenticationSuccessHandler.java | 6 +- .../server/controller/AbstractWebTest.java | 5 +- .../controller/TwoFactorAuthConfigTest.java | 525 ++++++++++++++++++ .../server/controller/TwoFactorAuthTest.java | 245 +------- .../sql/TwoFactorAuthConfigSqlTest.java | 23 + 18 files changed, 621 insertions(+), 277 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java create mode 100644 application/src/test/java/org/thingsboard/server/controller/sql/TwoFactorAuthConfigSqlTest.java diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java index 5d3e3bf116..527c862c95 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java @@ -114,7 +114,7 @@ public class TwoFactorAuthConfigController extends BaseController { @GetMapping("/settings") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") public TwoFactorAuthSettings getTwoFactorAuthSettings() throws ThingsboardException { - return twoFactorAuthConfigManager.getTwoFaSettings(getTenantId()).orElse(null); + return twoFactorAuthConfigManager.getTwoFaSettings(getTenantId(), false).orElse(null); } @PostMapping("/settings") diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java index ff6417a3aa..d858f64899 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -37,14 +37,9 @@ import javax.servlet.http.HttpServletRequest; /* * TODO [viacheslav]: - * - Tests for 2FA * - Swagger documentation - * */ - -/* - * * - * TODO (later): + * TODO [viacheslav] (later): * - 2FA entries should be secured against code injection by code validation * - ability to force users to use 2FA (maybe on log in, do not give them token pair but to give temporary * token to configure 2FA account config); also will need to make users configure 2FA during activation and password setup... diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java index c8117ec792..1012f27d25 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java @@ -71,7 +71,7 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { @Override public void prepareVerificationCode(SecurityUser securityUser, TwoFactorAuthAccountConfig accountConfig, boolean checkLimits) throws ThingsboardException { - TwoFactorAuthSettings twoFaSettings = configManager.getTwoFaSettings(securityUser.getTenantId()) + TwoFactorAuthSettings twoFaSettings = configManager.getTwoFaSettings(securityUser.getTenantId(), true) .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); if (checkLimits) { if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeSendRateLimit())) { @@ -102,7 +102,7 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { throw new ThingsboardException("User is disabled", ThingsboardErrorCode.AUTHENTICATION); } - TwoFactorAuthSettings twoFaSettings = configManager.getTwoFaSettings(securityUser.getTenantId()) + TwoFactorAuthSettings twoFaSettings = configManager.getTwoFaSettings(securityUser.getTenantId(), true) .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); if (checkLimits) { if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeCheckRateLimit())) { @@ -136,7 +136,7 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { private TwoFactorAuthProviderConfig getTwoFaProviderConfig(TenantId tenantId, TwoFactorAuthProviderType providerType) throws ThingsboardException { - return configManager.getTwoFaSettings(tenantId) + return configManager.getTwoFaSettings(tenantId, true) .flatMap(twoFaSettings -> twoFaSettings.getProviderConfig(providerType)) .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFactorAuthConfigManager.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFactorAuthConfigManager.java index a96d25e520..bd486a6af0 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFactorAuthConfigManager.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFactorAuthConfigManager.java @@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.service.ConstraintValidator; +import org.thingsboard.server.dao.settings.AdminSettingsDao; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; @@ -47,6 +48,7 @@ public class DefaultTwoFactorAuthConfigManager implements TwoFactorAuthConfigMan private final UserService userService; private final AdminSettingsService adminSettingsService; + private final AdminSettingsDao adminSettingsDao; private final AttributesService attributesService; protected static final String TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY = "twoFaConfig"; @@ -54,8 +56,8 @@ public class DefaultTwoFactorAuthConfigManager implements TwoFactorAuthConfigMan @Override - public boolean isTwoFaEnabled(User user) { - return getTwoFaAccountConfig(user.getTenantId(), user.getId()).isPresent(); + public boolean isTwoFaEnabled(TenantId tenantId, UserId userId) { + return getTwoFaAccountConfig(tenantId, userId).isPresent(); } @Override @@ -96,21 +98,26 @@ public class DefaultTwoFactorAuthConfigManager implements TwoFactorAuthConfigMan private Optional getTwoFaProviderConfig(TenantId tenantId, TwoFactorAuthProviderType providerType) { - return getTwoFaSettings(tenantId) + return getTwoFaSettings(tenantId, true) .flatMap(twoFaSettings -> twoFaSettings.getProviderConfig(providerType)); } @SneakyThrows({InterruptedException.class, ExecutionException.class}) @Override - public Optional getTwoFaSettings(TenantId tenantId) { + public Optional getTwoFaSettings(TenantId tenantId, boolean sysadminSettingsAsDefault) { if (tenantId.equals(TenantId.SYS_TENANT_ID)) { - return Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY)) + return Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, TWO_FACTOR_AUTH_SETTINGS_KEY)) .map(adminSettings -> JacksonUtil.treeToValue(adminSettings.getJsonValue(), TwoFactorAuthSettings.class)); } else { - return attributesService.find(TenantId.SYS_TENANT_ID, tenantId, DataConstants.SERVER_SCOPE, TWO_FACTOR_AUTH_SETTINGS_KEY).get() - .map(adminSettingsAttribute -> JacksonUtil.fromString(adminSettingsAttribute.getJsonValue().get(), TwoFactorAuthSettings.class)) - .filter(tenantTwoFactorAuthSettings -> !tenantTwoFactorAuthSettings.isUseSystemTwoFactorAuthSettings()) - .or(() -> getTwoFaSettings(TenantId.SYS_TENANT_ID)); + Optional tenantTwoFaSettings = attributesService.find(TenantId.SYS_TENANT_ID, tenantId, + DataConstants.SERVER_SCOPE, TWO_FACTOR_AUTH_SETTINGS_KEY).get() + .map(adminSettingsAttribute -> JacksonUtil.fromString(adminSettingsAttribute.getJsonValue().get(), TwoFactorAuthSettings.class)); + if (sysadminSettingsAsDefault) { + if (tenantTwoFaSettings.isEmpty() || tenantTwoFaSettings.get().isUseSystemTwoFactorAuthSettings()) { + return getTwoFaSettings(TenantId.SYS_TENANT_ID, false); + } + } + return tenantTwoFaSettings; } } @@ -136,4 +143,16 @@ public class DefaultTwoFactorAuthConfigManager implements TwoFactorAuthConfigMan } } + @SneakyThrows({InterruptedException.class, ExecutionException.class}) + @Override + public void deleteTwoFaSettings(TenantId tenantId) { + if (tenantId.equals(TenantId.SYS_TENANT_ID)) { + Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY)) + .ifPresent(adminSettings -> adminSettingsDao.removeById(tenantId, adminSettings.getId().getId())); + } else { + attributesService.removeAll(TenantId.SYS_TENANT_ID, tenantId, DataConstants.SERVER_SCOPE, + Collections.singletonList(TWO_FACTOR_AUTH_SETTINGS_KEY)).get(); + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthConfigManager.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthConfigManager.java index 94c18aa999..96189bf584 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthConfigManager.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthConfigManager.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.security.auth.mfa.config; -import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; @@ -25,7 +24,7 @@ import java.util.Optional; public interface TwoFactorAuthConfigManager { - boolean isTwoFaEnabled(User user); + boolean isTwoFaEnabled(TenantId tenantId, UserId userId); Optional getTwoFaAccountConfig(TenantId tenantId, UserId userId); @@ -34,8 +33,10 @@ public interface TwoFactorAuthConfigManager { void deleteTwoFaAccountConfig(TenantId tenantId, UserId userId); - Optional getTwoFaSettings(TenantId tenantId); + Optional getTwoFaSettings(TenantId tenantId, boolean sysadminSettingsAsDefault); void saveTwoFaSettings(TenantId tenantId, TwoFactorAuthSettings twoFactorAuthSettings); + void deleteTwoFaSettings(TenantId tenantId); + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java index ae39065e74..72da35e744 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java @@ -34,17 +34,17 @@ public class TwoFactorAuthSettings { private List providers; @ApiModelProperty(example = "1:60 (1 request per minute)") - @Pattern(regexp = "[^0]\\d+:[^0]\\d+", message = "Rate limit configuration is invalid") + @Pattern(regexp = "[1-9]\\d*:[1-9]\\d*", message = "verification code send rate limit configuration is invalid") private String verificationCodeSendRateLimit; @ApiModelProperty(example = "3:900 (3 requests per 15 minutes)") - @Pattern(regexp = "[^0]\\d+:[^0]\\d+", message = "Rate limit configuration is invalid") + @Pattern(regexp = "[1-9]\\d*:[1-9]\\d*", message = "verification code check rate limit configuration is invalid") private String verificationCodeCheckRateLimit; @ApiModelProperty(example = "10") - @Min(0) + @Min(value = 0, message = "maximum number of verification failure before user lockout must be positive") private int maxVerificationFailuresBeforeUserLockout; @ApiModelProperty(value = "in minutes", example = "60") - @Min(1) - private int totalAllowedTimeForVerification; + @Min(value = 1, message = "total amount of time allotted for verification must be greater than 0") + private Integer totalAllowedTimeForVerification; public Optional getProviderConfig(TwoFactorAuthProviderType providerType) { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/OtpBasedTwoFactorAuthAccountConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/OtpBasedTwoFactorAuthAccountConfig.java index 80b832a831..ef090c63b4 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/OtpBasedTwoFactorAuthAccountConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/OtpBasedTwoFactorAuthAccountConfig.java @@ -15,5 +15,8 @@ */ package org.thingsboard.server.service.security.auth.mfa.config.account; +import lombok.Data; + +@Data public abstract class OtpBasedTwoFactorAuthAccountConfig implements TwoFactorAuthAccountConfig { } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java index 40e94899a7..c921c5aafe 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java @@ -26,8 +26,8 @@ import javax.validation.constraints.Pattern; @Data public class SmsTwoFactorAuthAccountConfig extends OtpBasedTwoFactorAuthAccountConfig { - @NotBlank - @Pattern(regexp = "^\\+[1-9]\\d{1,14}$", message = "Phone number is not of E.164 format") + @NotBlank(message = "phone number cannot be blank") + @Pattern(regexp = "^\\+[1-9]\\d{1,14}$", message = "phone number is not of E.164 format") private String phoneNumber; @Override diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java index 93975d49ad..7c92955043 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.security.auth.mfa.config.account; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; @@ -24,8 +25,9 @@ import javax.validation.constraints.Pattern; @Data public class TotpTwoFactorAuthAccountConfig implements TwoFactorAuthAccountConfig { - @NotBlank -// @Pattern(regexp = "otpauth://totp/") // FIXME [viacheslav]: validate otp auth url by pattern + @ApiModelProperty(example = "otpauth://totp/ThingsBoard:tenant@thingsboard.org?issuer=ThingsBoard&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII") + @NotBlank(message = "OTP auth URL cannot be blank") + @Pattern(regexp = "otpauth://totp/(\\S+?):(\\S+?)\\?issuer=(\\S+?)&secret=(\\w+?)", message = "OTP auth url is invalid") private String authUrl; @Override diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/OtpBasedTwoFactorAuthProviderConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/OtpBasedTwoFactorAuthProviderConfig.java index fa1e5d7b43..597c3e1e46 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/OtpBasedTwoFactorAuthProviderConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/OtpBasedTwoFactorAuthProviderConfig.java @@ -23,6 +23,6 @@ import javax.validation.constraints.Min; @Data public abstract class OtpBasedTwoFactorAuthProviderConfig implements TwoFactorAuthProviderConfig { @ApiModelProperty(value = "in seconds", example = "60") - @Min(1) // TODO [viacheslav]: test + @Min(value = 1, message = "verification code lifetime is required") private int verificationCodeLifetime; } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java index 3fe9e77ce7..8b34419041 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java @@ -26,7 +26,7 @@ import javax.validation.constraints.Pattern; @Data public class SmsTwoFactorAuthProviderConfig extends OtpBasedTwoFactorAuthProviderConfig { - @NotBlank + @NotBlank(message = "verification message template is required") @Pattern(regexp = ".*\\$\\{verificationCode}.*", message = "template must contain verification code") private String smsVerificationMessageTemplate; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java index ab6c15e56b..db6653ffe9 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java @@ -57,7 +57,7 @@ public abstract class OtpBasedTwoFactorAuthProvider Optional.ofNullable(settings.getTotalAllowedTimeForVerification())).orElse(30)); tokenPair.setToken(tokenFactory.createPreVerificationToken(securityUser, preVerificationTokenLifetime).getToken()); tokenPair.setRefreshToken(null); tokenPair.setScope(Authority.PRE_VERIFICATION_TOKEN); diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 32d55a71dd..efae835421 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -66,6 +66,7 @@ import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeCon import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; @@ -124,6 +125,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected String username; protected TenantId tenantId; + protected UserId tenantAdminUserId; @SuppressWarnings("rawtypes") private HttpMessageConverter mappingJackson2HttpMessageConverter; @@ -186,7 +188,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { tenantAdmin.setTenantId(tenantId); tenantAdmin.setEmail(TENANT_ADMIN_EMAIL); - createUserAndLogin(tenantAdmin, TENANT_ADMIN_PASSWORD); + tenantAdmin = createUserAndLogin(tenantAdmin, TENANT_ADMIN_PASSWORD); + tenantAdminUserId = tenantAdmin.getId(); Customer customer = new Customer(); customer.setTitle("Customer"); diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java new file mode 100644 index 0000000000..01e2bc496e --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java @@ -0,0 +1,525 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.controller; + +import org.jboss.aerogear.security.otp.Totp; +import org.jboss.aerogear.security.otp.api.Base32; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.cache.CacheManager; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; +import org.thingsboard.rule.engine.api.SmsService; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager; +import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; +import org.thingsboard.server.service.security.auth.mfa.config.account.SmsTwoFactorAuthAccountConfig; +import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig; +import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; +import org.thingsboard.server.service.security.auth.mfa.config.provider.SmsTwoFactorAuthProviderConfig; +import org.thingsboard.server.service.security.auth.mfa.config.provider.TotpTwoFactorAuthProviderConfig; +import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig; +import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; +import org.thingsboard.server.service.security.auth.mfa.provider.impl.OtpBasedTwoFactorAuthProvider; +import org.thingsboard.server.service.security.auth.mfa.provider.impl.TotpTwoFactorAuthProvider; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { + + @SpyBean + private TotpTwoFactorAuthProvider totpTwoFactorAuthProvider; + @MockBean + private SmsService smsService; + @Autowired + private CacheManager cacheManager; + @Autowired + private TwoFactorAuthConfigManager twoFactorAuthConfigManager; + + @Before + public void beforeEach() throws Exception { + loginSysAdmin(); + } + + @After + public void afterEach() { + twoFactorAuthConfigManager.deleteTwoFaSettings(TenantId.SYS_TENANT_ID); + twoFactorAuthConfigManager.deleteTwoFaSettings(tenantId); + } + + + @Test + public void testSaveTwoFaSettings() throws Exception { + loginSysAdmin(); + testSaveTestTwoFaSettings(); + + loginTenantAdmin(); + testSaveTestTwoFaSettings(); + } + + private void testSaveTestTwoFaSettings() throws Exception { + TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig(); + totpTwoFaProviderConfig.setIssuerName("tb"); + SmsTwoFactorAuthProviderConfig smsTwoFaProviderConfig = new SmsTwoFactorAuthProviderConfig(); + smsTwoFaProviderConfig.setSmsVerificationMessageTemplate("${verificationCode}"); + smsTwoFaProviderConfig.setVerificationCodeLifetime(60); + + TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings(); + twoFaSettings.setProviders(List.of(totpTwoFaProviderConfig, smsTwoFaProviderConfig)); + twoFaSettings.setVerificationCodeSendRateLimit("1:60"); + twoFaSettings.setVerificationCodeCheckRateLimit("3:900"); + twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10); + twoFaSettings.setTotalAllowedTimeForVerification(60); + + doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk()); + + TwoFactorAuthSettings savedTwoFaSettings = readResponse(doGet("/api/2fa/settings").andExpect(status().isOk()), TwoFactorAuthSettings.class); + + assertThat(savedTwoFaSettings.getProviders()).hasSize(2); + assertThat(savedTwoFaSettings.getProviders()).contains(totpTwoFaProviderConfig, smsTwoFaProviderConfig); + } + + @Test + public void testSaveTwoFaSettings_validationError() throws Exception { + loginTenantAdmin(); + + TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings(); + twoFaSettings.setProviders(Collections.emptyList()); + twoFaSettings.setVerificationCodeSendRateLimit("ab:aba"); + twoFaSettings.setVerificationCodeCheckRateLimit("0:12"); + twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(-1); + twoFaSettings.setTotalAllowedTimeForVerification(0); + + String errorMessage = getErrorMessage(doPost("/api/2fa/settings", twoFaSettings) + .andExpect(status().isBadRequest())); + + assertThat(errorMessage).contains( + "verification code send rate limit configuration is invalid", + "verification code check rate limit configuration is invalid", + "maximum number of verification failure before user lockout must be positive", + "total amount of time allotted for verification must be greater than 0" + ); + + twoFaSettings.setUseSystemTwoFactorAuthSettings(true); + doPost("/api/2fa/settings", twoFaSettings) + .andExpect(status().isOk()); + + twoFaSettings.setVerificationCodeSendRateLimit(null); + twoFaSettings.setVerificationCodeCheckRateLimit(null); + twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(0); + twoFaSettings.setTotalAllowedTimeForVerification(null); + + doPost("/api/2fa/settings", twoFaSettings) + .andExpect(status().isOk()); + } + + @Test + public void testGetTwoFaSettings_useSysadminSettingsAsDefault() throws Exception { + loginSysAdmin(); + TwoFactorAuthSettings sysadminTwoFaSettings = new TwoFactorAuthSettings(); + TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig(); + totpTwoFaProviderConfig.setIssuerName("tb"); + sysadminTwoFaSettings.setProviders(Collections.singletonList(totpTwoFaProviderConfig)); + sysadminTwoFaSettings.setMaxVerificationFailuresBeforeUserLockout(25); + doPost("/api/2fa/settings", sysadminTwoFaSettings).andExpect(status().isOk()); + + loginTenantAdmin(); + TwoFactorAuthSettings tenantTwoFaSettings = new TwoFactorAuthSettings(); + tenantTwoFaSettings.setUseSystemTwoFactorAuthSettings(true); + tenantTwoFaSettings.setProviders(Collections.emptyList()); + doPost("/api/2fa/settings", tenantTwoFaSettings).andExpect(status().isOk()); + TwoFactorAuthSettings twoFaSettings = readResponse(doGet("/api/2fa/settings").andExpect(status().isOk()), TwoFactorAuthSettings.class); + assertThat(twoFaSettings).isEqualTo(tenantTwoFaSettings); + + doPost("/api/2fa/account/config/generate?providerType=TOTP") + .andExpect(status().isOk()); + + tenantTwoFaSettings.setUseSystemTwoFactorAuthSettings(false); + tenantTwoFaSettings.setProviders(Collections.emptyList()); + tenantTwoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10); + doPost("/api/2fa/settings", tenantTwoFaSettings).andExpect(status().isOk()); + twoFaSettings = readResponse(doGet("/api/2fa/settings").andExpect(status().isOk()), TwoFactorAuthSettings.class); + assertThat(twoFaSettings).isEqualTo(tenantTwoFaSettings); + + assertThat(getErrorMessage(doPost("/api/2fa/account/config/generate?providerType=TOTP") + .andExpect(status().isBadRequest()))).containsIgnoringCase("provider is not configured"); + + loginSysAdmin(); + sysadminTwoFaSettings.setProviders(Collections.emptyList()); + doPost("/api/2fa/settings", sysadminTwoFaSettings).andExpect(status().isOk()); + loginTenantAdmin(); + tenantTwoFaSettings.setUseSystemTwoFactorAuthSettings(true); + tenantTwoFaSettings.setProviders(Collections.singletonList(totpTwoFaProviderConfig)); + doPost("/api/2fa/settings", tenantTwoFaSettings).andExpect(status().isOk()); + + assertThat(getErrorMessage(doPost("/api/2fa/account/config/generate?providerType=TOTP") + .andExpect(status().isBadRequest()))).containsIgnoringCase("provider is not configured"); + + tenantTwoFaSettings.setUseSystemTwoFactorAuthSettings(false); + doPost("/api/2fa/settings", tenantTwoFaSettings).andExpect(status().isOk()); + + doPost("/api/2fa/account/config/generate?providerType=TOTP") + .andExpect(status().isOk()); + + loginSysAdmin(); + twoFaSettings = readResponse(doGet("/api/2fa/settings").andExpect(status().isOk()), TwoFactorAuthSettings.class); + assertThat(twoFaSettings).isEqualTo(sysadminTwoFaSettings); + } + + @Test + public void testSaveTotpTwoFaProviderConfig_validationError() throws Exception { + TotpTwoFactorAuthProviderConfig invalidTotpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig(); + invalidTotpTwoFaProviderConfig.setIssuerName(" "); + + String errorResponse = saveTwoFaSettingsAndGetError(invalidTotpTwoFaProviderConfig); + assertThat(errorResponse).containsIgnoringCase("issuer name must not be blank"); + } + + @Test + public void testSaveSmsTwoFaProviderConfig_validationError() throws Exception { + SmsTwoFactorAuthProviderConfig invalidSmsTwoFaProviderConfig = new SmsTwoFactorAuthProviderConfig(); + invalidSmsTwoFaProviderConfig.setSmsVerificationMessageTemplate("does not contain verification code"); + invalidSmsTwoFaProviderConfig.setVerificationCodeLifetime(60); + + String errorResponse = saveTwoFaSettingsAndGetError(invalidSmsTwoFaProviderConfig); + assertThat(errorResponse).containsIgnoringCase("must contain verification code"); + + invalidSmsTwoFaProviderConfig.setSmsVerificationMessageTemplate(null); + invalidSmsTwoFaProviderConfig.setVerificationCodeLifetime(0); + errorResponse = saveTwoFaSettingsAndGetError(invalidSmsTwoFaProviderConfig); + assertThat(errorResponse).containsIgnoringCase("verification message template is required"); + assertThat(errorResponse).containsIgnoringCase("verification code lifetime is required"); + } + + private String saveTwoFaSettingsAndGetError(TwoFactorAuthProviderConfig invalidTwoFaProviderConfig) throws Exception { + TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings(); + twoFaSettings.setProviders(Collections.singletonList(invalidTwoFaProviderConfig)); + + return getErrorMessage(doPost("/api/2fa/settings", twoFaSettings) + .andExpect(status().isBadRequest())); + } + + @Test + public void testSaveTwoFaAccountConfig_providerNotConfigured() throws Exception { + configureSmsTwoFaProvider("${verificationCode}"); + + loginTenantAdmin(); + + TwoFactorAuthProviderType notConfiguredProviderType = TwoFactorAuthProviderType.TOTP; + String errorMessage = getErrorMessage(doPost("/api/2fa/account/config/generate?providerType=" + notConfiguredProviderType) + .andExpect(status().isBadRequest())); + assertThat(errorMessage).containsIgnoringCase("provider is not configured"); + + TotpTwoFactorAuthAccountConfig notConfiguredProviderAccountConfig = new TotpTwoFactorAuthAccountConfig(); + notConfiguredProviderAccountConfig.setAuthUrl("otpauth://totp/aba:aba?issuer=aba&secret=ABA"); + errorMessage = getErrorMessage(doPost("/api/2fa/account/config/submit", notConfiguredProviderAccountConfig)); + assertThat(errorMessage).containsIgnoringCase("provider is not configured"); + } + + @Test + public void testGenerateTotpTwoFaAccountConfig() throws Exception { + TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider(); + + loginTenantAdmin(); + + assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), String.class)).isNullOrEmpty(); + generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig); + } + + @Test + public void testSubmitTotpTwoFaAccountConfig() throws Exception { + TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider(); + + loginTenantAdmin(); + + TotpTwoFactorAuthAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig); + doPost("/api/2fa/account/config/submit", generatedTotpTwoFaAccountConfig).andExpect(status().isOk()); + verify(totpTwoFactorAuthProvider).prepareVerificationCode(argThat(user -> user.getEmail().equals(TENANT_ADMIN_EMAIL)), + eq(totpTwoFaProviderConfig), eq(generatedTotpTwoFaAccountConfig)); + } + + @Test + public void testSubmitTotpTwoFaAccountConfig_validationError() throws Exception { + configureTotpTwoFaProvider(); + + loginTenantAdmin(); + + TotpTwoFactorAuthAccountConfig totpTwoFaAccountConfig = new TotpTwoFactorAuthAccountConfig(); + totpTwoFaAccountConfig.setAuthUrl(null); + + String errorMessage = getErrorMessage(doPost("/api/2fa/account/config/submit", totpTwoFaAccountConfig) + .andExpect(status().isBadRequest())); + assertThat(errorMessage).containsIgnoringCase("otp auth url cannot be blank"); + + totpTwoFaAccountConfig.setAuthUrl("otpauth://totp/T B: aba"); + errorMessage = getErrorMessage(doPost("/api/2fa/account/config/submit", totpTwoFaAccountConfig) + .andExpect(status().isBadRequest())); + assertThat(errorMessage).containsIgnoringCase("otp auth url is invalid"); + + totpTwoFaAccountConfig.setAuthUrl("otpauth://totp/ThingsBoard%20(Tenant):tenant@thingsboard.org?issuer=ThingsBoard+%28Tenant%29&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII"); + doPost("/api/2fa/account/config/submit", totpTwoFaAccountConfig) + .andExpect(status().isOk()); + } + + @Test + public void testVerifyAndSaveTotpTwoFaAccountConfig() throws Exception { + TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider(); + + loginTenantAdmin(); + + TotpTwoFactorAuthAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig); + + String secret = UriComponentsBuilder.fromUriString(generatedTotpTwoFaAccountConfig.getAuthUrl()).build() + .getQueryParams().getFirst("secret"); + String correctVerificationCode = new Totp(secret).now(); + + doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, generatedTotpTwoFaAccountConfig) + .andExpect(status().isOk()); + + TwoFactorAuthAccountConfig twoFaAccountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFactorAuthAccountConfig.class); + assertThat(twoFaAccountConfig).isEqualTo(generatedTotpTwoFaAccountConfig); + } + + @Test + public void testVerifyAndSaveTotpTwoFaAccountConfig_incorrectVerificationCode() throws Exception { + TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider(); + + loginTenantAdmin(); + + TotpTwoFactorAuthAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig); + + String incorrectVerificationCode = "100000"; + String errorMessage = getErrorMessage(doPost("/api/2fa/account/config?verificationCode=" + incorrectVerificationCode, generatedTotpTwoFaAccountConfig) + .andExpect(status().isBadRequest())); + + assertThat(errorMessage).containsIgnoringCase("verification code is incorrect"); + } + + private TotpTwoFactorAuthAccountConfig generateTotpTwoFaAccountConfig(TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig) throws Exception { + TwoFactorAuthAccountConfig generatedTwoFaAccountConfig = readResponse(doPost("/api/2fa/account/config/generate?providerType=TOTP") + .andExpect(status().isOk()), TwoFactorAuthAccountConfig.class); + assertThat(generatedTwoFaAccountConfig).isInstanceOf(TotpTwoFactorAuthAccountConfig.class); + + assertThat(((TotpTwoFactorAuthAccountConfig) generatedTwoFaAccountConfig)).satisfies(accountConfig -> { + UriComponents otpAuthUrl = UriComponentsBuilder.fromUriString(accountConfig.getAuthUrl()).build(); + assertThat(otpAuthUrl.getScheme()).isEqualTo("otpauth"); + assertThat(otpAuthUrl.getHost()).isEqualTo("totp"); + assertThat(otpAuthUrl.getQueryParams().getFirst("issuer")).isEqualTo(totpTwoFaProviderConfig.getIssuerName()); + assertThat(otpAuthUrl.getPath()).isEqualTo("/%s:%s", totpTwoFaProviderConfig.getIssuerName(), TENANT_ADMIN_EMAIL); + assertThat(otpAuthUrl.getQueryParams().getFirst("secret")).satisfies(secretKey -> { + assertDoesNotThrow(() -> Base32.decode(secretKey)); + }); + }); + return (TotpTwoFactorAuthAccountConfig) generatedTwoFaAccountConfig; + } + + @Test + public void testGetTwoFaAccountConfig_whenProviderNotConfigured() throws Exception { + testVerifyAndSaveTotpTwoFaAccountConfig(); + assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), + TotpTwoFactorAuthAccountConfig.class)).isNotNull(); + + loginSysAdmin(); + + saveProvidersConfigs(); + + assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), String.class)) + .isNullOrEmpty(); + } + + @Test + public void testGenerateSmsTwoFaAccountConfig() throws Exception { + configureSmsTwoFaProvider("${verificationCode}"); + doPost("/api/2fa/account/config/generate?providerType=SMS") + .andExpect(status().isOk()); + } + + @Test + public void testSubmitSmsTwoFaAccountConfig() throws Exception { + String verificationMessageTemplate = "Here is your verification code: ${verificationCode}"; + configureSmsTwoFaProvider(verificationMessageTemplate); + + loginTenantAdmin(); + + SmsTwoFactorAuthAccountConfig smsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig(); + smsTwoFaAccountConfig.setPhoneNumber("+38054159785"); + + doPost("/api/2fa/account/config/submit", smsTwoFaAccountConfig).andExpect(status().isOk()); + + String verificationCode = cacheManager.getCache(CacheConstants.TWO_FA_VERIFICATION_CODES_CACHE) + .get(tenantAdminUserId, OtpBasedTwoFactorAuthProvider.Otp.class).getValue(); + + verify(smsService).sendSms(eq(tenantId), any(), argThat(phoneNumbers -> { + return phoneNumbers[0].equals(smsTwoFaAccountConfig.getPhoneNumber()); + }), eq("Here is your verification code: " + verificationCode)); + } + + @Test + public void testSubmitSmsTwoFaAccountConfig_validationError() throws Exception { + configureSmsTwoFaProvider("${verificationCode}"); + + SmsTwoFactorAuthAccountConfig smsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig(); + String blankPhoneNumber = ""; + smsTwoFaAccountConfig.setPhoneNumber(blankPhoneNumber); + + String errorMessage = getErrorMessage(doPost("/api/2fa/account/config/submit", smsTwoFaAccountConfig) + .andExpect(status().isBadRequest())); + assertThat(errorMessage).containsIgnoringCase("phone number cannot be blank"); + + String nonE164PhoneNumber = "8754868"; + smsTwoFaAccountConfig.setPhoneNumber(nonE164PhoneNumber); + + errorMessage = getErrorMessage(doPost("/api/2fa/account/config/submit", smsTwoFaAccountConfig) + .andExpect(status().isBadRequest())); + assertThat(errorMessage).containsIgnoringCase("phone number is not of E.164 format"); + } + + @Test + public void testVerifyAndSaveSmsTwoFaAccountConfig() throws Exception { + configureSmsTwoFaProvider("${verificationCode}"); + + loginTenantAdmin(); + + SmsTwoFactorAuthAccountConfig smsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig(); + smsTwoFaAccountConfig.setPhoneNumber("+38051889445"); + + ArgumentCaptor verificationCodeCaptor = ArgumentCaptor.forClass(String.class); + doPost("/api/2fa/account/config/submit", smsTwoFaAccountConfig).andExpect(status().isOk()); + + verify(smsService).sendSms(eq(tenantId), any(), argThat(phoneNumbers -> { + return phoneNumbers[0].equals(smsTwoFaAccountConfig.getPhoneNumber()); + }), verificationCodeCaptor.capture()); + + String correctVerificationCode = verificationCodeCaptor.getValue(); + doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, smsTwoFaAccountConfig) + .andExpect(status().isOk()); + + TwoFactorAuthAccountConfig accountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFactorAuthAccountConfig.class); + assertThat(accountConfig).isEqualTo(smsTwoFaAccountConfig); + } + + @Test + public void testVerifyAndSaveSmsTwoFaAccountConfig_incorrectVerificationCode() throws Exception { + configureSmsTwoFaProvider("${verificationCode}"); + + loginTenantAdmin(); + + SmsTwoFactorAuthAccountConfig smsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig(); + smsTwoFaAccountConfig.setPhoneNumber("+38051889445"); + + String errorMessage = getErrorMessage(doPost("/api/2fa/account/config?verificationCode=100000", smsTwoFaAccountConfig) + .andExpect(status().isBadRequest())); + assertThat(errorMessage).containsIgnoringCase("verification code is incorrect"); + } + + @Test + public void testVerifyAndSaveSmsTwoFaAccountConfig_differentAccountConfigs() throws Exception { + configureSmsTwoFaProvider("${verificationCode}"); + loginTenantAdmin(); + + SmsTwoFactorAuthAccountConfig initialSmsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig(); + initialSmsTwoFaAccountConfig.setPhoneNumber("+38051889445"); + + ArgumentCaptor verificationCodeCaptor = ArgumentCaptor.forClass(String.class); + doPost("/api/2fa/account/config/submit", initialSmsTwoFaAccountConfig).andExpect(status().isOk()); + + verify(smsService).sendSms(eq(tenantId), any(), argThat(phoneNumbers -> { + return phoneNumbers[0].equals(initialSmsTwoFaAccountConfig.getPhoneNumber()); + }), verificationCodeCaptor.capture()); + + String correctVerificationCode = verificationCodeCaptor.getValue(); + + SmsTwoFactorAuthAccountConfig anotherSmsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig(); + anotherSmsTwoFaAccountConfig.setPhoneNumber("+38111111111"); + String errorMessage = getErrorMessage(doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, anotherSmsTwoFaAccountConfig) + .andExpect(status().isBadRequest())); + assertThat(errorMessage).containsIgnoringCase("verification code is incorrect"); + + doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, initialSmsTwoFaAccountConfig) + .andExpect(status().isOk()); + TwoFactorAuthAccountConfig accountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFactorAuthAccountConfig.class); + assertThat(accountConfig).isEqualTo(initialSmsTwoFaAccountConfig); + } + + private TotpTwoFactorAuthProviderConfig configureTotpTwoFaProvider() throws Exception { + TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig(); + totpTwoFaProviderConfig.setIssuerName("tb"); + + saveProvidersConfigs(totpTwoFaProviderConfig); + return totpTwoFaProviderConfig; + } + + private SmsTwoFactorAuthProviderConfig configureSmsTwoFaProvider(String verificationMessageTemplate) throws Exception { + SmsTwoFactorAuthProviderConfig smsTwoFaProviderConfig = new SmsTwoFactorAuthProviderConfig(); + smsTwoFaProviderConfig.setSmsVerificationMessageTemplate(verificationMessageTemplate); + smsTwoFaProviderConfig.setVerificationCodeLifetime(60); + + saveProvidersConfigs(smsTwoFaProviderConfig); + return smsTwoFaProviderConfig; + } + + private void saveProvidersConfigs(TwoFactorAuthProviderConfig... providerConfigs) throws Exception { + TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings(); + + twoFaSettings.setProviders(Arrays.stream(providerConfigs).collect(Collectors.toList())); + doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk()); + } + + @Test + public void testIsTwoFaEnabled() throws ThingsboardException { + SmsTwoFactorAuthAccountConfig accountConfig = new SmsTwoFactorAuthAccountConfig(); + accountConfig.setPhoneNumber("+380505050"); + twoFactorAuthConfigManager.saveTwoFaAccountConfig(tenantId, tenantAdminUserId, accountConfig); + + assertThat(twoFactorAuthConfigManager.isTwoFaEnabled(tenantId, tenantAdminUserId)).isTrue(); + } + + @Test + public void testDeleteTwoFaAccountConfig() throws Exception { + SmsTwoFactorAuthAccountConfig accountConfig = new SmsTwoFactorAuthAccountConfig(); + accountConfig.setPhoneNumber("+380505050"); + twoFactorAuthConfigManager.saveTwoFaAccountConfig(tenantId, tenantAdminUserId, accountConfig); + + loginTenantAdmin(); + + TwoFactorAuthAccountConfig savedAccountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFactorAuthAccountConfig.class); + assertThat(savedAccountConfig).isEqualTo(accountConfig); + + doDelete("/api/2fa/account/config").andExpect(status().isOk()); + + assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), String.class)) + .isNullOrEmpty(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java index 69bd49f6f5..4a9888ada1 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java @@ -15,242 +15,15 @@ */ package org.thingsboard.server.controller; -import org.jboss.aerogear.security.otp.Totp; -import org.jboss.aerogear.security.otp.api.Base32; -import org.junit.Before; -import org.junit.Test; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.mock.mockito.SpyBean; -import org.springframework.web.util.UriComponents; -import org.springframework.web.util.UriComponentsBuilder; -import org.thingsboard.rule.engine.api.SmsService; -import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; -import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig; -import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; -import org.thingsboard.server.service.security.auth.mfa.config.provider.SmsTwoFactorAuthProviderConfig; -import org.thingsboard.server.service.security.auth.mfa.config.provider.TotpTwoFactorAuthProviderConfig; -import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig; -import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; -import org.thingsboard.server.service.security.auth.mfa.provider.impl.TotpTwoFactorAuthProvider; - -import java.util.Arrays; -import java.util.Collections; -import java.util.stream.Collectors; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -// TODO [viacheslav]: test validation for all account configs, provider configs and two factor auth settings -// TODO [viacheslav]: test authentication details, log login action, last login ts, rate limiting, user blocking, etc +/* +* TODO [viacheslav] +* check validation of the verification code +* test rate limits +* test code expiration +* test pre-verification token lifetime +* test user blocking +* test log login action, lastLoginTs, and authentication details +* */ public abstract class TwoFactorAuthTest extends AbstractControllerTest { - @SpyBean - private TotpTwoFactorAuthProvider totpTwoFactorAuthProvider; - @MockBean - private SmsService smsService; - - @Before - public void beforeEach() throws Exception { - loginSysAdmin(); - } - - - @Test - public void testSaveTwoFaSettings() throws Exception { - loginSysAdmin(); - testSaveTestTwoFaSettings(); - - loginTenantAdmin(); - testSaveTestTwoFaSettings(); - } - - private void testSaveTestTwoFaSettings() throws Exception { - TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig(); - totpTwoFaProviderConfig.setIssuerName("tb"); - SmsTwoFactorAuthProviderConfig smsTwoFaProviderConfig = new SmsTwoFactorAuthProviderConfig(); - smsTwoFaProviderConfig.setSmsVerificationMessageTemplate("${verificationCode}"); - - saveProvidersConfigs(totpTwoFaProviderConfig, smsTwoFaProviderConfig); - - TwoFactorAuthSettings savedTwoFaSettings = readResponse(doGet("/api/2fa/settings").andExpect(status().isOk()), TwoFactorAuthSettings.class); - - assertThat(savedTwoFaSettings.getProviders()).hasSize(2); - assertThat(savedTwoFaSettings.getProviders()).contains(totpTwoFaProviderConfig, smsTwoFaProviderConfig); - } - - @Test - public void testSaveTotpTwoFaProviderConfig_validationError() throws Exception { - TotpTwoFactorAuthProviderConfig invalidTotpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig(); - invalidTotpTwoFaProviderConfig.setIssuerName(" "); - - String errorResponse = saveTwoFaSettingsAndGetError(invalidTotpTwoFaProviderConfig); - assertThat(errorResponse).containsIgnoringCase("issuer name must not be blank"); - } - - @Test - public void testSaveSmsTwoFaProviderConfig_validationError() throws Exception { - SmsTwoFactorAuthProviderConfig invalidSmsTwoFaProviderConfig = new SmsTwoFactorAuthProviderConfig(); - invalidSmsTwoFaProviderConfig.setSmsVerificationMessageTemplate("does not contain verification code"); - - String errorResponse = saveTwoFaSettingsAndGetError(invalidSmsTwoFaProviderConfig); - assertThat(errorResponse).containsIgnoringCase("must contain verification code"); - } - - private String saveTwoFaSettingsAndGetError(TwoFactorAuthProviderConfig invalidTwoFaProviderConfig) throws Exception { - TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings(); - twoFaSettings.setProviders(Collections.singletonList(invalidTwoFaProviderConfig)); - - return getErrorMessage(doPost("/api/2fa/settings", twoFaSettings) - .andExpect(status().isBadRequest())); - } - - @Test - public void testSaveTwoFaAccountConfig_providerNotConfigured() throws Exception { - configureSmsTwoFaProvider("${verificationCode}"); - - loginTenantAdmin(); - - TwoFactorAuthProviderType notConfiguredProviderType = TwoFactorAuthProviderType.TOTP; - String errorMessage = getErrorMessage(doPost("/api/2fa/account/config/generate?providerType=" + notConfiguredProviderType) - .andExpect(status().isBadRequest())); - assertThat(errorMessage).containsIgnoringCase("provider is not configured"); - - TotpTwoFactorAuthAccountConfig notConfiguredProviderAccountConfig = new TotpTwoFactorAuthAccountConfig(); - notConfiguredProviderAccountConfig.setAuthUrl("aba"); - errorMessage = getErrorMessage(doPost("/api/2fa/account/config/submit", notConfiguredProviderAccountConfig)); - assertThat(errorMessage).containsIgnoringCase("provider is not configured"); - } - - @Test - public void testGenerateTotpTwoFaAccountConfig() throws Exception { - TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider(); - - loginTenantAdmin(); - - assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), String.class)).isNullOrEmpty(); - generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig); - } - - @Test - public void testSubmitTotpTwoFaAccountConfig() throws Exception { - TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider(); - - loginTenantAdmin(); - - TotpTwoFactorAuthAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig); - doPost("/api/2fa/account/config/submit", generatedTotpTwoFaAccountConfig).andExpect(status().isOk()); - verify(totpTwoFactorAuthProvider).prepareVerificationCode(argThat(user -> user.getEmail().equals(TENANT_ADMIN_EMAIL)), - eq(totpTwoFaProviderConfig), eq(generatedTotpTwoFaAccountConfig)); - } - - @Test - public void testVerifyAndSaveTotpTwoFaAccountConfig() throws Exception { - TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider(); - - loginTenantAdmin(); - - TotpTwoFactorAuthAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig); - - String secret = UriComponentsBuilder.fromUriString(generatedTotpTwoFaAccountConfig.getAuthUrl()).build() - .getQueryParams().getFirst("secret"); - String correctVerificationCode = new Totp(secret).now(); - - doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, generatedTotpTwoFaAccountConfig) - .andExpect(status().isOk()); - - TwoFactorAuthAccountConfig twoFaAccountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFactorAuthAccountConfig.class); - assertThat(twoFaAccountConfig).isEqualTo(generatedTotpTwoFaAccountConfig); - } - - @Test - public void testVerifyAndSaveTotpTwoFaAccountConfig_incorrectVerificationCode() throws Exception { - TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider(); - - loginTenantAdmin(); - - TotpTwoFactorAuthAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig); - - String incorrectVerificationCode = "100000"; - String errorMessage = getErrorMessage(doPost("/api/2fa/account/config?verificationCode=" + incorrectVerificationCode, generatedTotpTwoFaAccountConfig) - .andExpect(status().isBadRequest())); - - assertThat(errorMessage).containsIgnoringCase("verification code is incorrect"); - } - - private TotpTwoFactorAuthAccountConfig generateTotpTwoFaAccountConfig(TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig) throws Exception { - TwoFactorAuthAccountConfig generatedTwoFaAccountConfig = readResponse(doPost("/api/2fa/account/config/generate?providerType=TOTP") - .andExpect(status().isOk()), TwoFactorAuthAccountConfig.class); - assertThat(generatedTwoFaAccountConfig).isInstanceOf(TotpTwoFactorAuthAccountConfig.class); - - assertThat(((TotpTwoFactorAuthAccountConfig) generatedTwoFaAccountConfig)).satisfies(accountConfig -> { - UriComponents otpAuthUrl = UriComponentsBuilder.fromUriString(accountConfig.getAuthUrl()).build(); - assertThat(otpAuthUrl.getScheme()).isEqualTo("otpauth"); - assertThat(otpAuthUrl.getHost()).isEqualTo("totp"); - assertThat(otpAuthUrl.getQueryParams().getFirst("issuer")).isEqualTo(totpTwoFaProviderConfig.getIssuerName()); - assertThat(otpAuthUrl.getPath()).isEqualTo("/%s:%s", totpTwoFaProviderConfig.getIssuerName(), TENANT_ADMIN_EMAIL); - assertThat(otpAuthUrl.getQueryParams().getFirst("secret")).satisfies(secretKey -> { - assertDoesNotThrow(() -> Base32.decode(secretKey)); - }); - }); - return (TotpTwoFactorAuthAccountConfig) generatedTwoFaAccountConfig; - } - - - @Test - public void testGetTwoFaAccountConfig_whenProviderNotConfigured() throws Exception { - testVerifyAndSaveTotpTwoFaAccountConfig(); - assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), - TotpTwoFactorAuthAccountConfig.class)).isNotNull(); - - loginSysAdmin(); - - saveProvidersConfigs(); - - assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), String.class)) - .isNullOrEmpty(); - } - -// @Test -// public void testSubmitSmsTwoFaAccountConfig() throws Exception { -// String verificationMessageTemplate = "Here is your verification code: ${verificationCode}"; -// SmsTwoFactorAuthProviderConfig smsTwoFaProviderConfig = configureSmsTwoFaProvider(verificationMessageTemplate); -// -// SmsTwoFactorAuthAccountConfig smsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig(); -// smsTwoFaAccountConfig.setPhoneNumber("+38054159785"); -// -// String verificationCode = ""; ? -// -// verify(smsService).sendSms(eq(tenantId), any(), argThat(phoneNumbers -> { -// return phoneNumbers[0].equals(smsTwoFaAccountConfig.getPhoneNumber()) -// }), eq("Here is your verification code: " + verificationCode)); -// } - - - - private TotpTwoFactorAuthProviderConfig configureTotpTwoFaProvider() throws Exception { - TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig(); - totpTwoFaProviderConfig.setIssuerName("tb"); - - saveProvidersConfigs(totpTwoFaProviderConfig); - return totpTwoFaProviderConfig; - } - - private SmsTwoFactorAuthProviderConfig configureSmsTwoFaProvider(String verificationMessageTemplate) throws Exception { - SmsTwoFactorAuthProviderConfig smsTwoFaProviderConfig = new SmsTwoFactorAuthProviderConfig(); - smsTwoFaProviderConfig.setSmsVerificationMessageTemplate(verificationMessageTemplate); - - saveProvidersConfigs(smsTwoFaProviderConfig); - return smsTwoFaProviderConfig; - } - - private void saveProvidersConfigs(TwoFactorAuthProviderConfig... providerConfigs) throws Exception { - TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings(); - twoFaSettings.setProviders(Arrays.stream(providerConfigs).collect(Collectors.toList())); - doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk()); - } - } diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/TwoFactorAuthConfigSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/TwoFactorAuthConfigSqlTest.java new file mode 100644 index 0000000000..591f995e9d --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/sql/TwoFactorAuthConfigSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.controller.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.controller.TwoFactorAuthConfigTest; + +@DaoSqlTest +public class TwoFactorAuthConfigSqlTest extends TwoFactorAuthConfigTest { +} From bc6c38c36cfc269ac3a8d0b98d48337e661a1fa0 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 22 Mar 2022 15:46:30 +0200 Subject: [PATCH 014/276] Change `isAuthenticated()` check to `hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')` in controllers --- .../server/controller/AuthController.java | 6 +++--- .../server/controller/DashboardController.java | 4 ++-- .../controller/TwoFactorAuthConfigController.java | 14 +++++++------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index 4887bc205d..72be55a135 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -82,7 +82,7 @@ public class AuthController extends BaseController { @ApiOperation(value = "Get current User (getUser)", notes = "Get the information about the User which credentials are used to perform this REST API call.") - @PreAuthorize("isAuthenticated()") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/auth/user", method = RequestMethod.GET) public @ResponseBody User getUser() throws ThingsboardException { @@ -96,7 +96,7 @@ public class AuthController extends BaseController { @ApiOperation(value = "Logout (logout)", notes = "Special API call to record the 'logout' of the user to the Audit Logs. Since platform uses [JWT](https://jwt.io/), the actual logout is the procedure of clearing the [JWT](https://jwt.io/) token on the client side. ") - @PreAuthorize("isAuthenticated()") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/auth/logout", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) public void logout(HttpServletRequest request) throws ThingsboardException { @@ -105,7 +105,7 @@ public class AuthController extends BaseController { @ApiOperation(value = "Change password for current User (changePassword)", notes = "Change the password for the User which credentials are used to perform this REST API call. Be aware that previously generated [JWT](https://jwt.io/) tokens will be still valid until they expire.") - @PreAuthorize("isAuthenticated()") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/auth/changePassword", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) public ObjectNode changePassword( diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index 49c33045a5..72b0aaa76e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -671,7 +671,7 @@ public class DashboardController extends BaseController { "If 'homeDashboardId' parameter is not set on the User and Customer levels then checks the same parameter for the Tenant that owns the user. " + DASHBOARD_DEFINITION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) - @PreAuthorize("isAuthenticated()") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/dashboard/home", method = RequestMethod.GET) @ResponseBody public HomeDashboard getHomeDashboard() throws ThingsboardException { @@ -708,7 +708,7 @@ public class DashboardController extends BaseController { "If 'homeDashboardId' parameter is not set on the User and Customer levels then checks the same parameter for the Tenant that owns the user. " + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) - @PreAuthorize("isAuthenticated()") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/dashboard/home/info", method = RequestMethod.GET) @ResponseBody public HomeDashboardInfo getHomeDashboardInfo() throws ThingsboardException { diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java index 527c862c95..85991aab32 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java @@ -32,12 +32,12 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager; import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig; import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; -import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.model.SecurityUser; import javax.servlet.ServletOutputStream; @@ -55,14 +55,14 @@ public class TwoFactorAuthConfigController extends BaseController { @GetMapping("/account/config") - @PreAuthorize("isAuthenticated()") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") public TwoFactorAuthAccountConfig getTwoFaAccountConfig() throws ThingsboardException { SecurityUser user = getCurrentUser(); return twoFactorAuthConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId()).orElse(null); } @PostMapping("/account/config/generate") - @PreAuthorize("isAuthenticated()") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") public TwoFactorAuthAccountConfig generateTwoFaAccountConfig(@RequestParam TwoFactorAuthProviderType providerType) throws Exception { SecurityUser user = getCurrentUser(); return twoFactorAuthService.generateNewAccountConfig(user, providerType); @@ -70,7 +70,7 @@ public class TwoFactorAuthConfigController extends BaseController { /* TMP */ @PostMapping("/account/config/generate/qr") - @PreAuthorize("isAuthenticated()") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") public void generateTwoFaAccountConfigWithQr(@RequestParam TwoFactorAuthProviderType providerType, HttpServletResponse response) throws Exception { TwoFactorAuthAccountConfig config = generateTwoFaAccountConfig(providerType); if (providerType == TwoFactorAuthProviderType.TOTP) { @@ -84,14 +84,14 @@ public class TwoFactorAuthConfigController extends BaseController { /* TMP */ @PostMapping("/account/config/submit") - @PreAuthorize("isAuthenticated()") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") public void submitTwoFaAccountConfig(@Valid @RequestBody TwoFactorAuthAccountConfig accountConfig) throws Exception { SecurityUser user = getCurrentUser(); twoFactorAuthService.prepareVerificationCode(user, accountConfig, false); } @PostMapping("/account/config") - @PreAuthorize("isAuthenticated()") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") public void verifyAndSaveTwoFaAccountConfig(@Valid @RequestBody TwoFactorAuthAccountConfig accountConfig, @RequestParam String verificationCode) throws Exception { SecurityUser user = getCurrentUser(); @@ -104,7 +104,7 @@ public class TwoFactorAuthConfigController extends BaseController { } @DeleteMapping("/account/config") - @PreAuthorize("isAuthenticated()") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") public void deleteTwoFactorAuthAccountConfig() throws ThingsboardException { SecurityUser user = getCurrentUser(); twoFactorAuthConfigManager.deleteTwoFaAccountConfig(user.getTenantId(), user.getId()); From 8bbe6bafd8f1127bfd047f16cc9b349de59eabd2 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 22 Mar 2022 15:49:57 +0200 Subject: [PATCH 015/276] Refactor 2FA; add refilling setting to TbRateLimits --- .../controller/TwoFactorAuthController.java | 6 +++-- .../auth/mfa/DefaultTwoFactorAuthService.java | 24 +++++++++++++----- .../mfa/config/TwoFactorAuthSettings.java | 2 +- .../impl/OtpBasedTwoFactorAuthProvider.java | 1 - .../impl/SmsTwoFactorAuthProvider.java | 2 +- ...RestAwareAuthenticationSuccessHandler.java | 4 +-- .../security/model/token/JwtTokenFactory.java | 25 +++++++++++-------- .../server/common/msg/tools/TbRateLimits.java | 10 +++++--- .../server/dao/user/UserServiceImpl.java | 1 + 9 files changed, 48 insertions(+), 27 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java index d858f64899..b95e1d6099 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -17,7 +17,6 @@ package org.thingsboard.server.controller; import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -25,6 +24,7 @@ import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; @@ -53,6 +53,7 @@ public class TwoFactorAuthController extends BaseController { private final TwoFactorAuthService twoFactorAuthService; private final JwtTokenFactory tokenFactory; private final SystemSecurityService systemSecurityService; + private final UserService userService; @PostMapping("/verification/send") @@ -69,9 +70,10 @@ public class TwoFactorAuthController extends BaseController { boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, verificationCode, true); if (verificationSuccess) { systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, null); + user = new SecurityUser(userService.findUserById(user.getTenantId(), user.getId()), true, user.getUserPrincipal()); return tokenFactory.createTokenPair(user); } else { - ThingsboardException error = new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.AUTHENTICATION); + ThingsboardException error = new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.BAD_REQUEST_PARAMS); systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, error); throw error; } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java index 1012f27d25..ee9e252831 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java @@ -16,9 +16,10 @@ package org.thingsboard.server.service.security.auth.mfa; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.LockedException; import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -76,7 +77,7 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { if (checkLimits) { if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeSendRateLimit())) { TbRateLimits rateLimits = verificationCodeSendingRateLimits.computeIfAbsent(securityUser.getId(), sessionId -> { - return new TbRateLimits(twoFaSettings.getVerificationCodeSendRateLimit()); + return new TbRateLimits(twoFaSettings.getVerificationCodeSendRateLimit(), true); }); if (!rateLimits.tryConsume()) { throw new ThingsboardException("Too many verification code sending requests", ThingsboardErrorCode.TOO_MANY_REQUESTS); @@ -107,19 +108,30 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { if (checkLimits) { if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeCheckRateLimit())) { TbRateLimits rateLimits = verificationCodeCheckingRateLimits.computeIfAbsent(securityUser.getId(), sessionId -> { - return new TbRateLimits(twoFaSettings.getVerificationCodeCheckRateLimit()); + return new TbRateLimits(twoFaSettings.getVerificationCodeCheckRateLimit(), true); }); if (!rateLimits.tryConsume()) { throw new ThingsboardException("Too many verification code checking requests", ThingsboardErrorCode.TOO_MANY_REQUESTS); } } } - TwoFactorAuthProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType()) .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); - boolean verificationSuccess = getTwoFaProvider(accountConfig.getProviderType()).checkVerificationCode(securityUser, verificationCode, providerConfig, accountConfig); + + boolean verificationSuccess; + if (StringUtils.isNumeric(verificationCode) && verificationCode.length() == 6) { + verificationSuccess = getTwoFaProvider(accountConfig.getProviderType()).checkVerificationCode(securityUser, verificationCode, providerConfig, accountConfig); + } else { + verificationSuccess = false; + } if (checkLimits) { - systemSecurityService.validateTwoFaVerification(securityUser, verificationSuccess, twoFaSettings); + try { + systemSecurityService.validateTwoFaVerification(securityUser, verificationSuccess, twoFaSettings); + } catch (LockedException e) { + verificationCodeCheckingRateLimits.remove(securityUser.getId()); + verificationCodeSendingRateLimits.remove(securityUser.getId()); + throw new ThingsboardException(e.getMessage(), ThingsboardErrorCode.AUTHENTICATION); + } if (verificationSuccess) { verificationCodeCheckingRateLimits.remove(securityUser.getId()); verificationCodeSendingRateLimits.remove(securityUser.getId()); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java index 72da35e744..65e7ce2c04 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java @@ -42,7 +42,7 @@ public class TwoFactorAuthSettings { @ApiModelProperty(example = "10") @Min(value = 0, message = "maximum number of verification failure before user lockout must be positive") private int maxVerificationFailuresBeforeUserLockout; - @ApiModelProperty(value = "in minutes", example = "60") + @ApiModelProperty(value = "in seconds", example = "3600 (60 minutes)") @Min(value = 1, message = "total amount of time allotted for verification must be greater than 0") private Integer totalAllowedTimeForVerification; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java index db6653ffe9..edeaf7ba3d 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java @@ -65,7 +65,6 @@ public abstract class OtpBasedTwoFactorAuthProvider Optional.ofNullable(settings.getTotalAllowedTimeForVerification())).orElse(30)); + int preVerificationTokenLifetime = twoFactorAuthConfigManager.getTwoFaSettings(securityUser.getTenantId(), true) + .flatMap(settings -> Optional.ofNullable(settings.getTotalAllowedTimeForVerification())).orElse((int) TimeUnit.MINUTES.toSeconds(30)); tokenPair.setToken(tokenFactory.createPreVerificationToken(securityUser, preVerificationTokenLifetime).getToken()); tokenPair.setRefreshToken(null); tokenPair.setScope(Authority.PRE_VERIFICATION_TOKEN); diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index 5cfb461b44..a8e6f9cb5c 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -115,21 +115,22 @@ public class JwtTokenFactory { } else if (securityUser.getAuthority() == Authority.SYS_ADMIN) { securityUser.setTenantId(TenantId.SYS_TENANT_ID); } + String customerId = claims.get(CUSTOMER_ID, String.class); + if (customerId != null) { + securityUser.setCustomerId(new CustomerId(UUID.fromString(customerId))); + } + UserPrincipal principal; if (securityUser.getAuthority() != Authority.PRE_VERIFICATION_TOKEN) { securityUser.setFirstName(claims.get(FIRST_NAME, String.class)); securityUser.setLastName(claims.get(LAST_NAME, String.class)); securityUser.setEnabled(claims.get(ENABLED, Boolean.class)); boolean isPublic = claims.get(IS_PUBLIC, Boolean.class); - UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject); - securityUser.setUserPrincipal(principal); - String customerId = claims.get(CUSTOMER_ID, String.class); - if (customerId != null) { - securityUser.setCustomerId(new CustomerId(UUID.fromString(customerId))); - } + principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject); } else { - securityUser.setUserPrincipal(new UserPrincipal(UserPrincipal.Type.USER_NAME, subject)); + principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, subject); } + securityUser.setUserPrincipal(principal); return securityUser; } @@ -164,10 +165,12 @@ public class JwtTokenFactory { } public JwtToken createPreVerificationToken(SecurityUser user, Integer expirationTime) { - String token = setUpToken(user, Collections.singletonList(Authority.PRE_VERIFICATION_TOKEN.name()), expirationTime) - .claim(TENANT_ID, user.getTenantId().toString()) - .compact(); - return new AccessJwtToken(token); + JwtBuilder jwtBuilder = setUpToken(user, Collections.singletonList(Authority.PRE_VERIFICATION_TOKEN.name()), expirationTime) + .claim(TENANT_ID, user.getTenantId().toString()); + if (user.getCustomerId() != null) { + jwtBuilder.claim(CUSTOMER_ID, user.getCustomerId().toString()); + } + return new AccessJwtToken(jwtBuilder.compact()); } private JwtBuilder setUpToken(SecurityUser securityUser, List scopes, long expirationTime) { diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimits.java b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimits.java index afcbe1b3b9..f7a75381c9 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimits.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimits.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.msg.tools; import io.github.bucket4j.Bandwidth; import io.github.bucket4j.Bucket4j; +import io.github.bucket4j.Refill; import io.github.bucket4j.local.LocalBucket; import io.github.bucket4j.local.LocalBucketBuilder; @@ -29,12 +30,17 @@ public class TbRateLimits { private final LocalBucket bucket; public TbRateLimits(String limitsConfiguration) { + this(limitsConfiguration, false); + } + + public TbRateLimits(String limitsConfiguration, boolean refillIntervally) { LocalBucketBuilder builder = Bucket4j.builder(); boolean initialized = false; for (String limitSrc : limitsConfiguration.split(",")) { long capacity = Long.parseLong(limitSrc.split(":")[0]); long duration = Long.parseLong(limitSrc.split(":")[1]); - builder.addLimit(Bandwidth.simple(capacity, Duration.ofSeconds(duration))); + Refill refill = refillIntervally ? Refill.intervally(capacity, Duration.ofSeconds(duration)) : Refill.greedy(capacity, Duration.ofSeconds(duration)); + builder.addLimit(Bandwidth.classic(capacity, refill)); initialized = true; } if (initialized) { @@ -42,8 +48,6 @@ public class TbRateLimits { } else { throw new IllegalArgumentException("Failed to parse rate limits configuration: " + limitsConfiguration); } - - } public boolean tryConsume() { diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index b8c7b8e9f9..869a4b4700 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -323,6 +323,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic } ((ObjectNode) additionalInfo).put(LAST_LOGIN_TS, System.currentTimeMillis()); user.setAdditionalInfo(additionalInfo); + saveUser(user); } @Override From 5ed306881f3d934b57d43cea9b0ffd4bd4e1032e Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 22 Mar 2022 15:54:11 +0200 Subject: [PATCH 016/276] Tests for 2FA --- .../controller/TwoFactorAuthController.java | 8 +- .../controller/TwoFactorAuthConfigTest.java | 14 +- .../server/controller/TwoFactorAuthTest.java | 375 +++++++++++++++++- 3 files changed, 375 insertions(+), 22 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java index b95e1d6099..b93df6e751 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -36,13 +36,7 @@ import org.thingsboard.server.service.security.system.SystemSecurityService; import javax.servlet.http.HttpServletRequest; /* - * TODO [viacheslav]: - * - Swagger documentation - * - * TODO [viacheslav] (later): - * - 2FA entries should be secured against code injection by code validation - * - ability to force users to use 2FA (maybe on log in, do not give them token pair but to give temporary - * token to configure 2FA account config); also will need to make users configure 2FA during activation and password setup... + * FIXME [viacheslav]: Swagger documentation * */ @RestController @RequestMapping("/api/auth/2fa") diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java index 01e2bc496e..38ff2e0702 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java @@ -29,7 +29,6 @@ import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager; import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; @@ -100,7 +99,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { twoFaSettings.setVerificationCodeSendRateLimit("1:60"); twoFaSettings.setVerificationCodeCheckRateLimit("3:900"); twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10); - twoFaSettings.setTotalAllowedTimeForVerification(60); + twoFaSettings.setTotalAllowedTimeForVerification(3600); doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk()); @@ -497,9 +496,10 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { } @Test - public void testIsTwoFaEnabled() throws ThingsboardException { + public void testIsTwoFaEnabled() throws Exception { + configureSmsTwoFaProvider("${verificationCode}"); SmsTwoFactorAuthAccountConfig accountConfig = new SmsTwoFactorAuthAccountConfig(); - accountConfig.setPhoneNumber("+380505050"); + accountConfig.setPhoneNumber("+38050505050"); twoFactorAuthConfigManager.saveTwoFaAccountConfig(tenantId, tenantAdminUserId, accountConfig); assertThat(twoFactorAuthConfigManager.isTwoFaEnabled(tenantId, tenantAdminUserId)).isTrue(); @@ -507,12 +507,14 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { @Test public void testDeleteTwoFaAccountConfig() throws Exception { + configureSmsTwoFaProvider("${verificationCode}"); SmsTwoFactorAuthAccountConfig accountConfig = new SmsTwoFactorAuthAccountConfig(); - accountConfig.setPhoneNumber("+380505050"); - twoFactorAuthConfigManager.saveTwoFaAccountConfig(tenantId, tenantAdminUserId, accountConfig); + accountConfig.setPhoneNumber("+38050505050"); loginTenantAdmin(); + twoFactorAuthConfigManager.saveTwoFaAccountConfig(tenantId, tenantAdminUserId, accountConfig); + TwoFactorAuthAccountConfig savedAccountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFactorAuthAccountConfig.class); assertThat(savedAccountConfig).isEqualTo(accountConfig); diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java index 4a9888ada1..09d1208df3 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java @@ -15,15 +15,372 @@ */ package org.thingsboard.server.controller; -/* -* TODO [viacheslav] -* check validation of the verification code -* test rate limits -* test code expiration -* test pre-verification token lifetime -* test user blocking -* test log login action, lastLoginTs, and authentication details -* */ +import com.fasterxml.jackson.databind.JsonNode; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.jboss.aerogear.security.otp.Totp; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.thingsboard.rule.engine.api.SmsService; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.audit.ActionStatus; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.audit.AuditLog; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.SortOrder; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.audit.AuditLogService; +import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; +import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager; +import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; +import org.thingsboard.server.service.security.auth.mfa.config.account.SmsTwoFactorAuthAccountConfig; +import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig; +import org.thingsboard.server.service.security.auth.mfa.config.provider.SmsTwoFactorAuthProviderConfig; +import org.thingsboard.server.service.security.auth.mfa.config.provider.TotpTwoFactorAuthProviderConfig; +import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig; +import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; +import org.thingsboard.server.service.security.auth.rest.LoginRequest; +import org.thingsboard.server.service.security.model.JwtTokenPair; + +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + public abstract class TwoFactorAuthTest extends AbstractControllerTest { + @Autowired + private TwoFactorAuthConfigManager twoFactorAuthConfigManager; + @Autowired + private TwoFactorAuthService twoFactorAuthService; + @MockBean + private SmsService smsService; + @Autowired + private AuditLogService auditLogService; + @Autowired + private UserService userService; + + private User user; + private String username; + private String password; + + @Before + public void beforeEach() throws Exception { + username = "mfa@tb.io"; + password = "psswrd"; + + user = new User(); + user.setAuthority(Authority.TENANT_ADMIN); + user.setEmail(username); + user.setTenantId(tenantId); + + loginSysAdmin(); + user = createUser(user, password); + } + + @After + public void afterEach() { + twoFactorAuthConfigManager.deleteTwoFaSettings(tenantId); + twoFactorAuthConfigManager.deleteTwoFaSettings(TenantId.SYS_TENANT_ID); + } + + @Test + public void testTwoFa_totp() throws Exception { + TotpTwoFactorAuthAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa(); + + logInWithPreVerificationToken(); + + doPost("/api/auth/2fa/verification/send") + .andExpect(status().isOk()); + + String correctVerificationCode = getCorrectTotp(totpTwoFaAccountConfig); + + JsonNode tokenPair = readResponse(doPost("/api/auth/2fa/verification/check?verificationCode=" + correctVerificationCode) + .andExpect(status().isOk()), JsonNode.class); + validateAndSetJwtToken(tokenPair, username); + + User currentUser = readResponse(doGet("/api/auth/user") + .andExpect(status().isOk()), User.class); + assertThat(currentUser.getId()).isEqualTo(user.getId()); + } + + @Test + public void testTwoFa_sms() throws Exception { + configureSmsTwoFa(); + + logInWithPreVerificationToken(); + + doPost("/api/auth/2fa/verification/send") + .andExpect(status().isOk()); + + ArgumentCaptor verificationCodeCaptor = ArgumentCaptor.forClass(String.class); + verify(smsService).sendSms(eq(tenantId), any(), any(), verificationCodeCaptor.capture()); + String correctVerificationCode = verificationCodeCaptor.getValue(); + + JsonNode tokenPair = readResponse(doPost("/api/auth/2fa/verification/check?verificationCode=" + correctVerificationCode) + .andExpect(status().isOk()), JsonNode.class); + validateAndSetJwtToken(tokenPair, username); + + User currentUser = readResponse(doGet("/api/auth/user") + .andExpect(status().isOk()), User.class); + assertThat(currentUser.getId()).isEqualTo(user.getId()); + } + + @Test + public void testTwoFaPreVerificationTokenLifetime() throws Exception { + configureTotpTwoFa(twoFaSettings -> { + twoFaSettings.setTotalAllowedTimeForVerification(5); + }); + + logInWithPreVerificationToken(); + + await("expiration of the pre-verification token") + .atLeast(Duration.ofSeconds(3).plusMillis(500)) + .atMost(Duration.ofSeconds(6)) + .untilAsserted(() -> { + doPost("/api/auth/2fa/verification/send") + .andExpect(status().isUnauthorized()); + }); + } + + @Test + public void testCheckVerificationCode_userBlocked() throws Exception { + configureTotpTwoFa(twoFaSettings -> { + twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10); + }); + + logInWithPreVerificationToken(); + + Stream.generate(() -> RandomStringUtils.randomNumeric(6)) + .limit(9) + .forEach(incorrectVerificationCode -> { + try { + String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=" + incorrectVerificationCode) + .andExpect(status().isBadRequest())); + assertThat(errorMessage).containsIgnoringCase("verification code is incorrect"); + } catch (Exception e) { + fail(); + } + }); + + String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=" + RandomStringUtils.randomNumeric(6)) + .andExpect(status().isUnauthorized())); + assertThat(errorMessage).containsIgnoringCase("account was locked due to exceeded 2fa verification attempts"); + + errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=" + RandomStringUtils.randomNumeric(6)) + .andExpect(status().isUnauthorized())); + assertThat(errorMessage).containsIgnoringCase("user is disabled"); + } + + @Test + public void testSendVerificationCode_rateLimit() throws Exception { + configureTotpTwoFa(twoFaSettings -> { + twoFaSettings.setVerificationCodeSendRateLimit("3:10"); + }); + + logInWithPreVerificationToken(); + + for (int i = 0; i < 3; i++) { + doPost("/api/auth/2fa/verification/send") + .andExpect(status().isOk()); + } + + String rateLimitExceededError = getErrorMessage(doPost("/api/auth/2fa/verification/send") + .andExpect(status().isTooManyRequests())); + assertThat(rateLimitExceededError).containsIgnoringCase("too many verification code sending requests"); + + await("verification code sending rate limit resetting") + .atLeast(Duration.ofSeconds(8)) + .atMost(Duration.ofSeconds(12)) + .untilAsserted(() -> { + doPost("/api/auth/2fa/verification/send") + .andExpect(status().isOk()); + }); + } + + @Test + public void testCheckVerificationCode_rateLimit() throws Exception { + TotpTwoFactorAuthAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa(twoFaSettings -> { + twoFaSettings.setVerificationCodeCheckRateLimit("3:10"); + }); + + logInWithPreVerificationToken(); + + for (int i = 0; i < 3; i++) { + String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=incorrect") + .andExpect(status().isBadRequest())); + assertThat(incorrectVerificationCodeError).containsIgnoringCase("verification code is incorrect"); + } + + String rateLimitExceededError = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=incorrect") + .andExpect(status().isTooManyRequests())); + assertThat(rateLimitExceededError).containsIgnoringCase("too many verification code checking requests"); + + await("verification code checking rate limit resetting") + .atLeast(Duration.ofSeconds(8)) + .atMost(Duration.ofSeconds(12)) + .untilAsserted(() -> { + String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=incorrect") + .andExpect(status().isBadRequest())); + assertThat(incorrectVerificationCodeError).containsIgnoringCase("verification code is incorrect"); + }); + + doPost("/api/auth/2fa/verification/check?verificationCode=" + getCorrectTotp(totpTwoFaAccountConfig)) + .andExpect(status().isOk()); + } + + @Test + public void testCheckVerificationCode_invalidVerificationCode() throws Exception { + configureTotpTwoFa(); + logInWithPreVerificationToken(); + + for (String invalidVerificationCode : new String[]{"1234567", "ab1212", "12311 ", "oewkriwejqf"}) { + String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=" + invalidVerificationCode) + .andExpect(status().isBadRequest())); + assertThat(errorMessage).containsIgnoringCase("verification code is incorrect"); + } + } + + @Test + public void testCheckVerificationCode_codeExpiration() throws Exception { + configureSmsTwoFa(smsTwoFaProviderConfig -> { + smsTwoFaProviderConfig.setVerificationCodeLifetime(10); + }); + + logInWithPreVerificationToken(); + + ArgumentCaptor verificationCodeCaptor = ArgumentCaptor.forClass(String.class); + doPost("/api/auth/2fa/verification/send").andExpect(status().isOk()); + verify(smsService).sendSms(eq(tenantId), any(), any(), verificationCodeCaptor.capture()); + + String correctVerificationCode = verificationCodeCaptor.getValue(); + + await("verification code expiration") + .pollDelay(10, TimeUnit.SECONDS) + .atLeast(10, TimeUnit.SECONDS) + .atMost(12, TimeUnit.SECONDS) + .untilAsserted(() -> { + String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=" + correctVerificationCode) + .andExpect(status().isBadRequest())); + assertThat(incorrectVerificationCodeError).containsIgnoringCase("verification code is incorrect"); + }); + } + + @Test + public void testTwoFa_logLoginAction() throws Exception { + TotpTwoFactorAuthAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa(); + + logInWithPreVerificationToken(); + await("async audit log saving").during(1, TimeUnit.SECONDS); + assertThat(getLogInAuditLogs()).isEmpty(); + assertThat(userService.findUserById(tenantId, user.getId()).getAdditionalInfo() + .get("lastLoginTs")).isNull(); + + doPost("/api/auth/2fa/verification/check?verificationCode=incorrect") + .andExpect(status().isBadRequest()); + + await("async audit log saving").atMost(1, TimeUnit.SECONDS) + .until(() -> getLogInAuditLogs().size() == 1); + assertThat(getLogInAuditLogs().get(0)).satisfies(failedLogInAuditLog -> { + assertThat(failedLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.FAILURE); + assertThat(failedLogInAuditLog.getActionFailureDetails()).containsIgnoringCase("verification code is incorrect"); + assertThat(failedLogInAuditLog.getUserName()).isEqualTo(username); + }); + + doPost("/api/auth/2fa/verification/check?verificationCode=" + getCorrectTotp(totpTwoFaAccountConfig)) + .andExpect(status().isOk()); + await("async audit log saving").atMost(1, TimeUnit.SECONDS) + .until(() -> getLogInAuditLogs().size() == 2); + assertThat(getLogInAuditLogs().get(0)).satisfies(successfulLogInAuditLog -> { + assertThat(successfulLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.SUCCESS); + assertThat(successfulLogInAuditLog.getUserName()).isEqualTo(username); + }); + assertThat(userService.findUserById(tenantId, user.getId()).getAdditionalInfo() + .get("lastLoginTs").asLong()) + .isGreaterThan(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(3)); + } + + private List getLogInAuditLogs() { + return auditLogService.findAuditLogsByTenantIdAndUserId(tenantId, user.getId(), List.of(ActionType.LOGIN), + new TimePageLink(new PageLink(10, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC)), 0L, System.currentTimeMillis())).getData(); + } + + @Test + public void testAuthWithoutTwoFaAccountConfig() throws ThingsboardException { + configureTotpTwoFa(); + twoFactorAuthConfigManager.deleteTwoFaAccountConfig(tenantId, user.getId()); + + assertDoesNotThrow(() -> { + login(username, password); + }); + } + + private void logInWithPreVerificationToken() throws Exception { + LoginRequest loginRequest = new LoginRequest(username, password); + + JwtTokenPair response = readResponse(doPost("/api/auth/login", loginRequest).andExpect(status().isOk()), JwtTokenPair.class); + assertThat(response.getToken()).isNotNull(); + assertThat(response.getRefreshToken()).isNull(); + assertThat(response.getScope()).isEqualTo(Authority.PRE_VERIFICATION_TOKEN); + + this.token = response.getToken(); + } + + private TotpTwoFactorAuthAccountConfig configureTotpTwoFa(Consumer... customizer) throws ThingsboardException { + TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig(); + totpTwoFaProviderConfig.setIssuerName("tb"); + + TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings(); + twoFaSettings.setUseSystemTwoFactorAuthSettings(false); + twoFaSettings.setProviders(Arrays.stream(new TwoFactorAuthProviderConfig[]{totpTwoFaProviderConfig}).collect(Collectors.toList())); + Arrays.stream(customizer).forEach(c -> c.accept(twoFaSettings)); + twoFactorAuthConfigManager.saveTwoFaSettings(tenantId, twoFaSettings); + + TotpTwoFactorAuthAccountConfig totpTwoFaAccountConfig = (TotpTwoFactorAuthAccountConfig) twoFactorAuthService.generateNewAccountConfig(user, TwoFactorAuthProviderType.TOTP); + twoFactorAuthConfigManager.saveTwoFaAccountConfig(tenantId, user.getId(), totpTwoFaAccountConfig); + return totpTwoFaAccountConfig; + } + + private SmsTwoFactorAuthAccountConfig configureSmsTwoFa(Consumer... customizer) throws ThingsboardException { + SmsTwoFactorAuthProviderConfig smsTwoFaProviderConfig = new SmsTwoFactorAuthProviderConfig(); + smsTwoFaProviderConfig.setVerificationCodeLifetime(60); + smsTwoFaProviderConfig.setSmsVerificationMessageTemplate("${verificationCode}"); + Arrays.stream(customizer).forEach(c -> c.accept(smsTwoFaProviderConfig)); + + TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings(); + twoFaSettings.setUseSystemTwoFactorAuthSettings(false); + twoFaSettings.setProviders(Arrays.stream(new TwoFactorAuthProviderConfig[]{smsTwoFaProviderConfig}).collect(Collectors.toList())); + twoFactorAuthConfigManager.saveTwoFaSettings(tenantId, twoFaSettings); + + SmsTwoFactorAuthAccountConfig smsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig(); + smsTwoFaAccountConfig.setPhoneNumber("+38050505050"); + twoFactorAuthConfigManager.saveTwoFaAccountConfig(tenantId, user.getId(), smsTwoFaAccountConfig); + return smsTwoFaAccountConfig; + } + + private String getCorrectTotp(TotpTwoFactorAuthAccountConfig totpTwoFaAccountConfig) { + String secret = StringUtils.substringAfterLast(totpTwoFaAccountConfig.getAuthUrl(), "secret="); + return new Totp(secret).now(); + } + } From ec6a7ae5a4013e886a2ca2f6626ebc09bfff6900 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 22 Mar 2022 17:13:21 +0200 Subject: [PATCH 017/276] Add dashboard and widget reference to widget settings form. Add default value method. --- .../json/system/widget_bundles/cards.json | 3 ++- .../add-widget-dialog.component.html | 5 ++-- .../dashboard-page/edit-widget.component.html | 5 ++-- .../data-key-config-dialog.component.html | 2 ++ .../data-key-config-dialog.component.ts | 5 +++- .../widget/data-key-config.component.html | 2 ++ .../widget/data-key-config.component.ts | 9 ++++++- .../components/widget/data-keys.component.ts | 11 +++++++- .../qrcode-widget-settings.component.html | 3 ++- .../qrcode-widget-settings.component.ts | 26 ++++++++++++++----- .../widget/widget-config.component.html | 6 +++++ .../widget/widget-config.component.ts | 25 ++++++++---------- .../widget/widget-settings.component.ts | 14 ++++++++-- ui-ngx/src/app/shared/models/widget.models.ts | 17 +++++++++--- 14 files changed, 96 insertions(+), 37 deletions(-) diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index e852bcc9d5..90159f1b2c 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -163,8 +163,9 @@ "templateHtml": "\n", "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}", "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.qrCodeWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n datasourcesOptional: true\n };\n}\n\nself.onDestroy = function() {\n}\n\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"QR Code\",\n \"properties\": {\n \"qrCodeTextPattern\": {\n \"title\": \"QR code text pattern (for ex. '${entityName} | ${keyName} - some text.')\",\n \"type\": \"string\",\n \"default\": \"${entityName}\"\n },\n \"useQrCodeTextFunction\": {\n \"title\": \"Use QR code text function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"qrCodeTextFunction\": {\n \"title\": \"QR code text function: f(data)\",\n \"type\": \"string\",\n \"default\": \"return data[0]['entityName'];\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useQrCodeTextFunction\",\n {\n \"key\": \"qrCodeTextPattern\",\n \"condition\": \"model.useQrCodeTextFunction !== true\"\n },\n {\n \"key\": \"qrCodeTextFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/qrcode/qrcode_text_fn\",\n \"condition\": \"model.useQrCodeTextFunction === true\"\n }\n ]\n}\n", + "settingsSchema": "{}\n", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-qrcode-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7036904308224163,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"qrCodeTextPattern\":\"${entityName}\",\"useQrCodeTextFunction\":false,\"qrCodeTextFunction\":\"return data[0] ? data[0]['entityName'] : '';\"},\"title\":\"QR Code\"}" } }, diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html index 27c1e58b7d..99f5e6ebfd 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html @@ -33,9 +33,8 @@ diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html index 1305f08980..c3b80e541f 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html @@ -20,9 +20,8 @@ diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html index 371a8f6456..9cfe9611fc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html @@ -33,6 +33,8 @@ [dataKeySettingsSchema]="data.dataKeySettingsSchema" [dataKeySettingsDirective]="data.dataKeySettingsDirective" [entityAliasId]="data.entityAliasId" + [dashboard]="data.dashboard" + [widget]="data.widget" [showPostProcessing]="data.showPostProcessing" [callbacks]="data.callbacks" formControlName="dataKey"> diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts index 48957543d5..4e88f389dc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts @@ -22,14 +22,17 @@ import { AppState } from '@core/core.state'; import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@shared/components/dialog.component'; -import { DataKey } from '@shared/models/widget.models'; +import { DataKey, Widget } from '@shared/models/widget.models'; import { DataKeysCallbacks } from './data-keys.component.models'; import { DataKeyConfigComponent } from '@home/components/widget/data-key-config.component'; +import { Dashboard } from '@shared/models/dashboard.models'; export interface DataKeyConfigDialogData { dataKey: DataKey; dataKeySettingsSchema: any; dataKeySettingsDirective: string; + dashboard: Dashboard; + widget: Widget; entityAliasId?: string; showPostProcessing?: boolean; callbacks?: DataKeysCallbacks; diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html index eba2fc6bc9..f6e9acda1b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html @@ -100,6 +100,8 @@
diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts index 03d72101f1..9c23be8144 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts @@ -18,7 +18,7 @@ import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@an import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { DataKey } from '@shared/models/widget.models'; +import { DataKey, Widget } from '@shared/models/widget.models'; import { ControlValueAccessor, FormBuilder, @@ -41,6 +41,7 @@ import { alarmFields } from '@shared/models/alarm.models'; import { JsFuncComponent } from '@shared/components/js-func.component'; import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; import { WidgetService } from '@core/http/widget.service'; +import { Dashboard } from '@shared/models/dashboard.models'; @Component({ selector: 'tb-data-key-config', @@ -69,6 +70,12 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con @Input() callbacks: DataKeysCallbacks; + @Input() + dashboard: Dashboard; + + @Input() + widget: Widget; + @Input() dataKeySettingsSchema: any; diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts index 63e72905e7..75f15327c5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts @@ -46,7 +46,7 @@ import { MatAutocomplete } from '@angular/material/autocomplete'; import { MatChipInputEvent, MatChipList } from '@angular/material/chips'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; -import { DataKey, DatasourceType, widgetType } from '@shared/models/widget.models'; +import { DataKey, DatasourceType, Widget, widgetType } from '@shared/models/widget.models'; import { IAliasController } from '@core/api/widget-api.models'; import { DataKeysCallbacks } from './data-keys.component.models'; import { alarmFields } from '@shared/models/alarm.models'; @@ -61,6 +61,7 @@ import { } from '@home/components/widget/data-key-config-dialog.component'; import { deepClone } from '@core/utils'; import { MatChipDropEvent } from '@app/shared/components/mat-chip-draggable.directive'; +import { Dashboard } from '@shared/models/dashboard.models'; @Component({ selector: 'tb-data-keys', @@ -116,6 +117,12 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie @Input() dataKeySettingsDirective: string; + @Input() + dashboard: Dashboard; + + @Input() + widget: Widget; + @Input() callbacks: DataKeysCallbacks; @@ -399,6 +406,8 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie dataKey: deepClone(key), dataKeySettingsSchema: this.datakeySettingsSchema, dataKeySettingsDirective: this.dataKeySettingsDirective, + dashboard: this.dashboard, + widget: this.widget, entityAliasId: this.entityAliasId, showPostProcessing: this.widgetType !== widgetType.alarm, callbacks: this.callbacks diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.html index 6efcc3611b..29a10e1d58 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.html @@ -28,7 +28,8 @@
- diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.ts index c9ed32b85a..06e8d7d079 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.ts @@ -14,12 +14,12 @@ /// limitations under the License. /// -import { Component } from '@angular/core'; +import { Component, ViewChild } from '@angular/core'; import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; - +import { JsFuncComponent } from '@shared/components/js-func.component'; @Component({ selector: 'tb-qrcode-widget-settings', @@ -28,6 +28,8 @@ import { AppState } from '@core/core.state'; }) export class QrCodeWidgetSettingsComponent extends WidgetSettingsComponent { + @ViewChild('qrCodeTextFunctionComponent', {static: true}) qrCodeTextFunctionComponent: JsFuncComponent; + qrCodeWidgetSettingsForm: FormGroup; constructor(protected store: Store, @@ -39,11 +41,19 @@ export class QrCodeWidgetSettingsComponent extends WidgetSettingsComponent { return this.qrCodeWidgetSettingsForm; } + protected defaultSettings(): WidgetSettings { + return { + qrCodeTextPattern: '${entityName}', + useQrCodeTextFunction: false, + qrCodeTextFunction: 'return data[0][\'entityName\'];' + }; + } + protected onSettingsSet(settings: WidgetSettings) { this.qrCodeWidgetSettingsForm = this.fb.group({ - qrCodeTextPattern: [settings ? settings.qrCodeTextPattern : '${entityName}', []], - useQrCodeTextFunction: [settings ? settings.useQrCodeTextFunction : false, []], - qrCodeTextFunction: [settings ? settings.qrCodeTextFunction : 'return data[0][\'entityName\'];', []] + qrCodeTextPattern: [settings.qrCodeTextPattern, []], + useQrCodeTextFunction: [settings.useQrCodeTextFunction, []], + qrCodeTextFunction: [settings.qrCodeTextFunction, []] }); } @@ -55,7 +65,9 @@ export class QrCodeWidgetSettingsComponent extends WidgetSettingsComponent { const useQrCodeTextFunction: boolean = this.qrCodeWidgetSettingsForm.get('useQrCodeTextFunction').value; if (useQrCodeTextFunction) { this.qrCodeWidgetSettingsForm.get('qrCodeTextPattern').setValidators([]); - this.qrCodeWidgetSettingsForm.get('qrCodeTextFunction').setValidators([Validators.required]); + this.qrCodeWidgetSettingsForm.get('qrCodeTextFunction').setValidators([Validators.required, + (control: AbstractControl) => this.qrCodeTextFunctionComponent.validate(control as FormControl) + ]); } else { this.qrCodeWidgetSettingsForm.get('qrCodeTextPattern').setValidators([Validators.required]); this.qrCodeWidgetSettingsForm.get('qrCodeTextFunction').setValidators([]); diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index 9bfbcc71a9..4fc8387a93 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -191,6 +191,8 @@ [aliasController]="aliasController" [datakeySettingsSchema]="modelValue?.dataKeySettingsSchema" [dataKeySettingsDirective]="modelValue?.dataKeySettingsDirective" + [dashboard]="dashboard" + [widget]="widget" [callbacks]="widgetConfigCallbacks" [entityAliasId]="datasourceControl.get('entityAliasId').value" [formControl]="datasourceControl.get('dataKeys')"> @@ -285,6 +287,8 @@ [aliasController]="aliasController" [datakeySettingsSchema]="modelValue?.dataKeySettingsSchema" [dataKeySettingsDirective]="modelValue?.dataKeySettingsDirective" + [dashboard]="dashboard" + [widget]="widget" [callbacks]="widgetConfigCallbacks" [entityAliasId]="alarmSourceSettings.get('entityAliasId').value" formControlName="dataKeys"> @@ -480,6 +484,8 @@ fxLayout="column"> diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index b3325e0de0..52bfb548ea 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -25,7 +25,7 @@ import { datasourceTypeTranslationMap, defaultLegendConfig, GroupInfo, - JsonSchema, + JsonSchema, Widget, widgetType } from '@shared/models/widget.models'; import { @@ -65,7 +65,7 @@ import { MatDialog } from '@angular/material/dialog'; import { EntityService } from '@core/http/entity.service'; import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; import { WidgetActionsData } from './action/manage-widget-actions.component.models'; -import { DashboardState } from '@shared/models/dashboard.models'; +import { Dashboard, DashboardState } from '@shared/models/dashboard.models'; import { entityFields } from '@shared/models/entity.models'; import { Filter, Filters } from '@shared/models/query/query.models'; import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component'; @@ -126,17 +126,14 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont aliasController: IAliasController; @Input() - entityAliases: EntityAliases; + dashboard: Dashboard; @Input() - filters: Filters; + widget: Widget; @Input() functionsOnly: boolean; - @Input() - dashboardStates: {[id: string]: DashboardState }; - @Input() disabled: boolean; widgetType: widgetType; @@ -831,14 +828,14 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont data: { isAdd: true, allowedEntityTypes, - entityAliases: this.entityAliases, + entityAliases: this.dashboard.configuration.entityAliases, alias: singleEntityAlias } }).afterClosed().pipe( tap((entityAlias) => { if (entityAlias) { - this.entityAliases[entityAlias.id] = entityAlias; - this.aliasController.updateEntityAliases(this.entityAliases); + this.dashboard.configuration.entityAliases[entityAlias.id] = entityAlias; + this.aliasController.updateEntityAliases(this.dashboard.configuration.entityAliases); } }) ); @@ -852,14 +849,14 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { isAdd: true, - filters: this.filters, + filters: this.dashboard.configuration.filters, filter: singleFilter } }).afterClosed().pipe( tap((result) => { if (result) { - this.filters[result.id] = result; - this.aliasController.updateFilters(this.filters); + this.dashboard.configuration.filters[result.id] = result; + this.aliasController.updateFilters(this.dashboard.configuration.filters); } }) ); @@ -881,7 +878,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont } private fetchDashboardStates(query: string): Array { - const stateIds = Object.keys(this.dashboardStates); + const stateIds = Object.keys(this.dashboard.configuration.states); const result = query ? stateIds.filter(this.createFilterForDashboardState(query)) : stateIds; if (result && result.length) { return result; diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.ts index 7b5e3f1b0f..9226c4e1f1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.ts @@ -40,8 +40,9 @@ import { deepClone } from '@core/utils'; import { RuleChainType } from '@shared/models/rule-chain.models'; import { JsonFormComponent } from '@shared/components/json-form/json-form.component'; import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; -import { IWidgetSettingsComponent, WidgetSettings } from '@shared/models/widget.models'; +import { IWidgetSettingsComponent, Widget, WidgetSettings } from '@shared/models/widget.models'; import { widgetSettingsComponentsMap } from '@home/components/widget/lib/settings/widget-settings.module'; +import { Dashboard } from '@shared/models/dashboard.models'; @Component({ selector: 'tb-widget-settings', @@ -62,6 +63,12 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On @Input() disabled: boolean; + @Input() + dashboard: Dashboard; + + @Input() + widget: Widget; + settingsDirectiveValue: string; @Input() @@ -134,6 +141,8 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On this.changeSubscription = null; } if (this.definedSettingsComponent) { + this.definedSettingsComponent.dashboard = this.dashboard; + this.definedSettingsComponent.widget = this.widget; this.definedSettingsComponent.settings = this.widgetSettingsFormData.model; this.changeSubscription = this.definedSettingsComponent.settingsChanged.subscribe((settings) => { this.updateModel(settings); @@ -185,7 +194,8 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On const factory = this.cfr.resolveComponentFactory(componentType); this.definedSettingsComponentRef = this.definedSettingsContainer.createComponent(factory); this.definedSettingsComponent = this.definedSettingsComponentRef.instance; - this.definedSettingsComponent.settings = this.widgetSettingsFormData?.model; + this.definedSettingsComponent.dashboard = this.dashboard; + this.definedSettingsComponent.widget = this.widget; this.changeSubscription = this.definedSettingsComponent.settingsChanged.subscribe((settings) => { this.updateModel(settings); }); diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 45c8943753..0d94484f7a 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -33,6 +33,7 @@ import { AbstractControl, FormGroup } from '@angular/forms'; import {RuleChainType} from "@shared/models/rule-chain.models"; import {Observable} from "rxjs"; import {RuleNodeConfiguration} from "@shared/models/rule-node.models"; +import { Dashboard } from '@shared/models/dashboard.models'; export enum widgetType { timeseries = 'timeseries', @@ -587,6 +588,8 @@ export interface WidgetSize { } export interface IWidgetSettingsComponent { + dashboard: Dashboard; + widget: Widget; settings: WidgetSettings; settingsChanged: Observable; validate(); @@ -598,17 +601,21 @@ export interface IWidgetSettingsComponent { export abstract class WidgetSettingsComponent extends PageComponent implements IWidgetSettingsComponent, OnInit, AfterViewInit { + dashboard: Dashboard; + + widget: Widget; + settingsValue: WidgetSettings; private settingsSet = false; set settings(value: WidgetSettings) { - this.settingsValue = value; + this.settingsValue = value || this.defaultSettings(); if (!this.settingsSet) { this.settingsSet = true; - this.setupSettings(value); + this.setupSettings(this.settingsValue); } else { - this.updateSettings(value); + this.updateSettings(this.settingsValue); } } @@ -694,4 +701,8 @@ export abstract class WidgetSettingsComponent extends PageComponent implements protected abstract onSettingsSet(settings: WidgetSettings); + protected defaultSettings(): WidgetSettings { + return {}; + } + } From 472edc8409d5edc2f488dc7d8e627f807719e5c1 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Wed, 23 Mar 2022 12:07:52 +0200 Subject: [PATCH 018/276] Refactor controller exceptions handling --- .../org/thingsboard/server/controller/BaseController.java | 8 +++++++- .../server/dao/service/ConstraintValidator.java | 1 - 2 files changed, 7 insertions(+), 2 deletions(-) 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 f33767d9db..02810cc0d9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -285,7 +285,13 @@ public abstract class BaseController { @ExceptionHandler(Exception.class) public void handleControllerException(Exception e, HttpServletResponse response) { ThingsboardException thingsboardException = handleException(e); - handleThingsboardException(thingsboardException, response); + if (thingsboardException.getErrorCode() == ThingsboardErrorCode.GENERAL && thingsboardException.getCause() instanceof Exception + && StringUtils.equals(thingsboardException.getCause().getMessage(), thingsboardException.getMessage())) { + e = (Exception) thingsboardException.getCause(); + } else { + e = thingsboardException; + } + errorResponseHandler.handle(e, response); } @ExceptionHandler(ThingsboardException.class) diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java index 404eeb44cc..5faa605863 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java @@ -25,7 +25,6 @@ import org.thingsboard.server.dao.exception.DataValidationException; import javax.validation.ConstraintViolation; import javax.validation.Validation; -import javax.validation.ValidationException; import javax.validation.Validator; import java.util.List; import java.util.Set; From 6eb8a41f9a2607469b5adbd39f9fd1e046cc76ca Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Wed, 23 Mar 2022 19:38:44 +0200 Subject: [PATCH 019/276] Swagger docs for TwoFactorAuthConfigController and config classes --- .../TwoFactorAuthConfigController.java | 78 +++++++++++++++++-- .../mfa/config/TwoFactorAuthSettings.java | 16 +++- .../SmsTwoFactorAuthAccountConfig.java | 4 + .../TotpTwoFactorAuthAccountConfig.java | 5 +- .../account/TwoFactorAuthAccountConfig.java | 2 +- .../OtpBasedTwoFactorAuthProviderConfig.java | 3 +- .../SmsTwoFactorAuthProviderConfig.java | 5 ++ .../TotpTwoFactorAuthProviderConfig.java | 5 ++ 8 files changed, 104 insertions(+), 14 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java index 85991aab32..29df21bca4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java @@ -19,6 +19,8 @@ import com.google.zxing.BarcodeFormat; import com.google.zxing.client.j2se.MatrixToImageWriter; import com.google.zxing.common.BitMatrix; import com.google.zxing.qrcode.QRCodeWriter; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; @@ -44,6 +46,8 @@ import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; +import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; + @RestController @RequestMapping("/api/2fa") @TbCoreComponent @@ -54,6 +58,20 @@ public class TwoFactorAuthConfigController extends BaseController { private final TwoFactorAuthService twoFactorAuthService; + @ApiOperation(value = "Get 2FA account config (getTwoFaAccountConfig)", + notes = "Get user's account 2FA configuration. Returns empty result if user did not configured 2FA, " + + "or if a provider for previously set up account config is not now configured." + NEW_LINE + + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER + NEW_LINE + + "Response example for TOTP 2FA: " + NEW_LINE + + "{\n" + + " \"providerType\": \"TOTP\",\n" + + " \"authUrl\": \"otpauth://totp/ThingsBoard:tenant@thingsboard.org?issuer=ThingsBoard&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII\"\n" + + "}" + NEW_LINE + + "Response example for SMS 2FA: " + NEW_LINE + + "{\n" + + " \"providerType\": \"SMS\",\n" + + " \"phoneNumber\": \"+380505005050\"\n" + + "}") @GetMapping("/account/config") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") public TwoFactorAuthAccountConfig getTwoFaAccountConfig() throws ThingsboardException { @@ -61,9 +79,29 @@ public class TwoFactorAuthConfigController extends BaseController { return twoFactorAuthConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId()).orElse(null); } + @ApiOperation(value = "Generate 2FA account config (generateTwoFaAccountConfig)", + notes = "Generate new 2FA account config for specified provider type. " + + "This method is only useful for TOTP 2FA, as there is nothing to generate for other provider types. " + + "For TOTP, this will return a corresponding account config template " + + "with a generated OTP auth URL (with new random secret key for each API call) that can be then " + + "converted to a QR code to scan with an authenticator app. " + + "For other provider types, this method will return an empty config. " + NEW_LINE + + "Will throw an error (Bad Request) if the provider is not configured for usage. " + + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER + NEW_LINE + + "Example of a generated account config for TOTP 2FA: " + NEW_LINE + + "{\n" + + " \"providerType\": \"TOTP\",\n" + + " \"authUrl\": \"otpauth://totp/ThingsBoard:tenant@thingsboard.org?issuer=ThingsBoard&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII\"\n" + + "}" + NEW_LINE + + "For SMS provider type it will return something like: " + NEW_LINE + + "{\n" + + " \"providerType\": \"SMS\",\n" + + " \"phoneNumber\": null\n" + + "}") @PostMapping("/account/config/generate") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - public TwoFactorAuthAccountConfig generateTwoFaAccountConfig(@RequestParam TwoFactorAuthProviderType providerType) throws Exception { + public TwoFactorAuthAccountConfig generateTwoFaAccountConfig(@ApiParam(value = "2FA provider type to generate new account config for", defaultValue = "TOTP", required = true) + @RequestParam TwoFactorAuthProviderType providerType) throws Exception { SecurityUser user = getCurrentUser(); return twoFactorAuthService.generateNewAccountConfig(user, providerType); } @@ -83,16 +121,32 @@ public class TwoFactorAuthConfigController extends BaseController { } /* TMP */ + @ApiOperation(value = "Submit 2FA account config (submitTwoFaAccountConfig)", + notes = "Submit 2FA account config to prepare for a future verification. " + + "Basically, this method will send a verification code for a given account config, if this has " + + "sense for a chosen 2FA provider. This code is needed to then verify and save the account config." + NEW_LINE + + "Will throw an error (Bad Request) if submitted account config is not valid, " + + "or if the provider is not configured for usage. " + + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PostMapping("/account/config/submit") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - public void submitTwoFaAccountConfig(@Valid @RequestBody TwoFactorAuthAccountConfig accountConfig) throws Exception { + public void submitTwoFaAccountConfig(@ApiParam(value = "2FA account config value. For TOTP 2FA config, authUrl value must not be blank and must match specific pattern. " + + "For SMS 2FA, phoneNumber property must not be blank and must be of E.164 phone number format.", required = true) + @Valid @RequestBody TwoFactorAuthAccountConfig accountConfig) throws Exception { SecurityUser user = getCurrentUser(); twoFactorAuthService.prepareVerificationCode(user, accountConfig, false); } + @ApiOperation(value = "Verify and save 2FA account config (verifyAndSaveTwoFaAccountConfig)", + notes = "Checks the verification code for submitted config, and if it is correct, saves the provided account config. " + + "The config is stored in the user's additionalInfo. " + NEW_LINE + + "Will throw an error (Bad Request) if the provider is not configured for usage. " + + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PostMapping("/account/config") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - public void verifyAndSaveTwoFaAccountConfig(@Valid @RequestBody TwoFactorAuthAccountConfig accountConfig, + public void verifyAndSaveTwoFaAccountConfig(@ApiParam(value = "2FA account config to save. Validation rules are the same as in submitTwoFaAccountConfig API method", required = true) + @Valid @RequestBody TwoFactorAuthAccountConfig accountConfig, + @ApiParam(value = "6-digit code from an authenticator app in case of TOTP 2FA, or the one sent via an SMS message in case of SMS 2FA", required = true) @RequestParam String verificationCode) throws Exception { SecurityUser user = getCurrentUser(); boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, verificationCode, accountConfig, false); @@ -103,24 +157,34 @@ public class TwoFactorAuthConfigController extends BaseController { } } + @ApiOperation(value = "Delete 2FA account config (deleteTwoFaAccountConfig)", + notes = "Delete user's 2FA config. " + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) @DeleteMapping("/account/config") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - public void deleteTwoFactorAuthAccountConfig() throws ThingsboardException { + public void deleteTwoFaAccountConfig() throws ThingsboardException { SecurityUser user = getCurrentUser(); twoFactorAuthConfigManager.deleteTwoFaAccountConfig(user.getTenantId(), user.getId()); } + @ApiOperation(value = "Get 2FA settings (getTwoFaSettings)", + notes = "Get settings for 2FA. If 2FA is not configured, then an empty response will be returned." + + ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @GetMapping("/settings") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - public TwoFactorAuthSettings getTwoFactorAuthSettings() throws ThingsboardException { + public TwoFactorAuthSettings getTwoFaSettings() throws ThingsboardException { return twoFactorAuthConfigManager.getTwoFaSettings(getTenantId(), false).orElse(null); } + @ApiOperation(value = "Save 2FA settings (saveTwoFaSettings)", + notes = "Save settings for 2FA. If a user is sysadmin - the settings are saved as AdminSettings; " + + "if it is a tenant admin - as a tenant attribute." + + ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PostMapping("/settings") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - public void saveTwoFactorAuthSettings(@RequestBody TwoFactorAuthSettings twoFactorAuthSettings) throws ThingsboardException { - twoFactorAuthConfigManager.saveTwoFaSettings(getTenantId(), twoFactorAuthSettings); + public void saveTwoFaSettings(@ApiParam(value = "Settings value", required = true) + @RequestBody TwoFactorAuthSettings twoFaSettings) throws ThingsboardException { + twoFactorAuthConfigManager.saveTwoFaSettings(getTenantId(), twoFaSettings); } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java index 65e7ce2c04..a39cfd1e37 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.security.auth.mfa.config; +import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig; @@ -27,22 +28,29 @@ import java.util.List; import java.util.Optional; @Data +@ApiModel public class TwoFactorAuthSettings { + @ApiModelProperty(value = "Option for tenant admins to use 2FA settings configured by sysadmin. " + + "If this param is set to true, then the settings will not be validated for constraints " + + "(if it is a tenant admin; for sysadmin this param is ignored)") private boolean useSystemTwoFactorAuthSettings; + @ApiModelProperty(value = "The list of 2FA providers' configs. Users will only be allowed to use 2FA providers from this list.") @Valid private List providers; - @ApiModelProperty(example = "1:60 (1 request per minute)") + @ApiModelProperty(value = "Rate limit configuration for verification code sending. The format is standard: 'amountOfRequests:periodInSeconds'. " + + "The value of '1:60' would limit verification code sending requests to one per minute.", example = "1:60", required = false) @Pattern(regexp = "[1-9]\\d*:[1-9]\\d*", message = "verification code send rate limit configuration is invalid") private String verificationCodeSendRateLimit; - @ApiModelProperty(example = "3:900 (3 requests per 15 minutes)") + @ApiModelProperty(value = "Rate limit configuration for verification code checking.", example = "3:900", required = false) @Pattern(regexp = "[1-9]\\d*:[1-9]\\d*", message = "verification code check rate limit configuration is invalid") private String verificationCodeCheckRateLimit; - @ApiModelProperty(example = "10") + @ApiModelProperty(value = "Maximum number of verification failures before a user gets disabled.", example = "10", required = false) @Min(value = 0, message = "maximum number of verification failure before user lockout must be positive") private int maxVerificationFailuresBeforeUserLockout; - @ApiModelProperty(value = "in seconds", example = "3600 (60 minutes)") + @ApiModelProperty(value = "Total amount of time in seconds allotted for verification. " + + "Basically, this property sets a lifetime for pre-verification token. If not set, default value of 30 minutes is used.", example = "3600", required = false) @Min(value = 1, message = "total amount of time allotted for verification must be greater than 0") private Integer totalAllowedTimeForVerification; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java index c921c5aafe..ece6b780a4 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.service.security.auth.mfa.config.account; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; @@ -22,10 +24,12 @@ import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthPr import javax.validation.constraints.NotBlank; import javax.validation.constraints.Pattern; +@ApiModel @EqualsAndHashCode(callSuper = true) @Data public class SmsTwoFactorAuthAccountConfig extends OtpBasedTwoFactorAuthAccountConfig { + @ApiModelProperty(value = "Phone number to use for 2FA. Must no be blank and must be of E.164 number format.", required = true) @NotBlank(message = "phone number cannot be blank") @Pattern(regexp = "^\\+[1-9]\\d{1,14}$", message = "phone number is not of E.164 format") private String phoneNumber; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java index 7c92955043..c67b1ee345 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.security.auth.mfa.config.account; +import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; @@ -22,10 +23,12 @@ import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthPr import javax.validation.constraints.NotBlank; import javax.validation.constraints.Pattern; +@ApiModel @Data public class TotpTwoFactorAuthAccountConfig implements TwoFactorAuthAccountConfig { - @ApiModelProperty(example = "otpauth://totp/ThingsBoard:tenant@thingsboard.org?issuer=ThingsBoard&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII") + @ApiModelProperty(value = "OTP auth URL used to generate a QR code to scan with an authenticator app. Must not be blank and must follow specific pattern.", + example = "otpauth://totp/ThingsBoard:tenant@thingsboard.org?issuer=ThingsBoard&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII", required = true) @NotBlank(message = "OTP auth URL cannot be blank") @Pattern(regexp = "otpauth://totp/(\\S+?):(\\S+?)\\?issuer=(\\S+?)&secret=(\\w+?)", message = "OTP auth url is invalid") private String authUrl; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TwoFactorAuthAccountConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TwoFactorAuthAccountConfig.java index d1947a72be..31ee1e2807 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TwoFactorAuthAccountConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TwoFactorAuthAccountConfig.java @@ -27,7 +27,7 @@ import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthPr use = JsonTypeInfo.Id.NAME, property = "providerType") @JsonSubTypes({ - @Type(name = "TOTP", value = TotpTwoFactorAuthAccountConfig.class ), + @Type(name = "TOTP", value = TotpTwoFactorAuthAccountConfig.class), @Type(name = "SMS", value = SmsTwoFactorAuthAccountConfig.class) }) public interface TwoFactorAuthAccountConfig { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/OtpBasedTwoFactorAuthProviderConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/OtpBasedTwoFactorAuthProviderConfig.java index 597c3e1e46..6b4ef92cda 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/OtpBasedTwoFactorAuthProviderConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/OtpBasedTwoFactorAuthProviderConfig.java @@ -22,7 +22,8 @@ import javax.validation.constraints.Min; @Data public abstract class OtpBasedTwoFactorAuthProviderConfig implements TwoFactorAuthProviderConfig { - @ApiModelProperty(value = "in seconds", example = "60") + @ApiModelProperty(value = "Verification code lifetime in seconds. Verification codes with a lifetime bigger than this param " + + "will be considered incorrect", example = "60", required = true) @Min(value = 1, message = "verification code lifetime is required") private int verificationCodeLifetime; } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java index 8b34419041..a589905292 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.service.security.auth.mfa.config.provider; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; @@ -22,10 +24,13 @@ import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthPr import javax.validation.constraints.NotBlank; import javax.validation.constraints.Pattern; +@ApiModel(parent = OtpBasedTwoFactorAuthProviderConfig.class) @EqualsAndHashCode(callSuper = true) @Data public class SmsTwoFactorAuthProviderConfig extends OtpBasedTwoFactorAuthProviderConfig { + @ApiModelProperty(value = "SMS verification message template. Available template variables are ${verificationCode} and ${userEmail}. " + + "It must not be blank and must contain verification code variable.", required = true) @NotBlank(message = "verification message template is required") @Pattern(regexp = ".*\\$\\{verificationCode}.*", message = "template must contain verification code") private String smsVerificationMessageTemplate; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TotpTwoFactorAuthProviderConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TotpTwoFactorAuthProviderConfig.java index e1604bb618..8c0c324ae0 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TotpTwoFactorAuthProviderConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TotpTwoFactorAuthProviderConfig.java @@ -15,14 +15,19 @@ */ package org.thingsboard.server.service.security.auth.mfa.config.provider; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; import javax.validation.constraints.NotBlank; +@ApiModel @Data public class TotpTwoFactorAuthProviderConfig implements TwoFactorAuthProviderConfig { + @ApiModelProperty(value = "Issuer name that will be displayed in an authenticator app near a username. " + + "Must not be blank.", example = "ThingsBoard", required = true) @NotBlank(message = "issuer name must not be blank") private String issuerName; From 0915113a24c02262accfda8c2c63faae30be0019 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Wed, 23 Mar 2022 19:57:18 +0200 Subject: [PATCH 020/276] Swagger docs for TwoFactorAuthController --- .../controller/TwoFactorAuthController.java | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java index b93df6e751..5339c7a6a8 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.controller; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PostMapping; @@ -35,9 +37,8 @@ import org.thingsboard.server.service.security.system.SystemSecurityService; import javax.servlet.http.HttpServletRequest; -/* - * FIXME [viacheslav]: Swagger documentation - * */ +import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; + @RestController @RequestMapping("/api/auth/2fa") @TbCoreComponent @@ -50,16 +51,30 @@ public class TwoFactorAuthController extends BaseController { private final UserService userService; + @ApiOperation(value = "Request 2FA verification code (requestTwoFaVerificationCode)", + notes = "Request 2FA verification code." + NEW_LINE + + "To make a request to this endpoint, you need an access token with the scope of PRE_VERIFICATION_TOKEN, " + + "which is issued on username/password auth if 2FA is enabled." + NEW_LINE + + "The API method is rate limited (using rate limit config from TwoFactorAuthSettings). " + + "Will return a Bad Request error if provider is not configured for usage, " + + "and Too Many Requests error if rate limits are exceeded.") @PostMapping("/verification/send") @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") - public void sendTwoFaVerificationCode() throws Exception { + public void requestTwoFaVerificationCode() throws Exception { SecurityUser user = getCurrentUser(); twoFactorAuthService.prepareVerificationCode(user, true); } + @ApiOperation(value = "Check 2FA verification code (checkTwoFaVerificationCode)", + notes = "Checks 2FA verification code, and if it is correct the method returns a regular access and refresh token pair." + NEW_LINE + + "The API method is rate limited (using rate limit config from TwoFactorAuthSettings), and also will block a user " + + "after X unsuccessful verification attempts if such behavior is configured (in TwoFactorAuthSettings)." + NEW_LINE + + "Will return a Bad Request error if provider is not configured for usage, " + + "and Too Many Requests error if rate limits are exceeded.") @PostMapping("/verification/check") @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") - public JwtTokenPair checkTwoFaVerificationCode(@RequestParam String verificationCode, HttpServletRequest servletRequest) throws Exception { + public JwtTokenPair checkTwoFaVerificationCode(@ApiParam(value = "6-digit verification code", required = true) + @RequestParam String verificationCode, HttpServletRequest servletRequest) throws Exception { SecurityUser user = getCurrentUser(); boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, verificationCode, true); if (verificationSuccess) { From b7db4ed60415133ef5342412598d6e87d052314b Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Thu, 24 Mar 2022 14:00:17 +0200 Subject: [PATCH 021/276] Minor 2FA refactoring --- .../server/controller/TwoFactorAuthController.java | 14 ++++++++++++++ .../provider/SmsTwoFactorAuthProviderConfig.java | 3 ++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java index 5339c7a6a8..1575673dcf 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -19,6 +19,7 @@ import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -29,6 +30,9 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; +import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager; +import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; +import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.model.JwtTokenPair; import org.thingsboard.server.service.security.model.SecurityUser; @@ -46,6 +50,7 @@ import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; public class TwoFactorAuthController extends BaseController { private final TwoFactorAuthService twoFactorAuthService; + private final TwoFactorAuthConfigManager twoFactorAuthConfigManager; private final JwtTokenFactory tokenFactory; private final SystemSecurityService systemSecurityService; private final UserService userService; @@ -88,4 +93,13 @@ public class TwoFactorAuthController extends BaseController { } } + @ApiOperation(value = "Get currently used 2FA provider type (getCurrentlyUsedTwoFaProviderType)") + @GetMapping("/provider/type") + @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") + public TwoFactorAuthProviderType getCurrentlyUsedTwoFaProviderType() throws ThingsboardException { + SecurityUser user = getCurrentUser(); + return twoFactorAuthConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId()) + .map(TwoFactorAuthAccountConfig::getProviderType).orElse(null); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java index a589905292..88eca4cb2c 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java @@ -30,7 +30,8 @@ import javax.validation.constraints.Pattern; public class SmsTwoFactorAuthProviderConfig extends OtpBasedTwoFactorAuthProviderConfig { @ApiModelProperty(value = "SMS verification message template. Available template variables are ${verificationCode} and ${userEmail}. " + - "It must not be blank and must contain verification code variable.", required = true) + "It must not be blank and must contain verification code variable.", + example = "Here is your verification code: ${verificationCode}", required = true) @NotBlank(message = "verification message template is required") @Pattern(regexp = ".*\\$\\{verificationCode}.*", message = "template must contain verification code") private String smsVerificationMessageTemplate; From fd53d3aeb499c2d9b0c701c69d142ec95c4d2ed5 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Sun, 27 Mar 2022 23:41:30 +0300 Subject: [PATCH 022/276] added base tests for queue --- .../server/dao/tenant/TenantServiceImpl.java | 4 + .../dao/service/AbstractServiceTest.java | 4 + .../dao/service/BaseQueueServiceTest.java | 491 ++++++++++++++++++ .../dao/service/sql/QueueServiceSqlTest.java | 23 + 4 files changed, 522 insertions(+) create mode 100644 dao/src/test/java/org/thingsboard/server/dao/service/BaseQueueServiceTest.java create mode 100644 dao/src/test/java/org/thingsboard/server/dao/service/sql/QueueServiceSqlTest.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 2ae46df19a..b8d0f1f84b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -142,6 +142,10 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe if (tenant.getId() == null) { deviceProfileService.createDefaultDeviceProfile(savedTenant.getId()); apiUsageStateService.createDefaultApiUsageState(savedTenant.getId(), null); + TenantProfile tenantProfile = tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, savedTenant.getTenantProfileId()); + if(tenantProfile.isIsolatedTbRuleEngine()) { + queueService.createDefaultMainQueue(tenantProfile, savedTenant.getTenantId()); + } } return savedTenant; } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java index 34e2618ff6..dae497b52d 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java @@ -57,6 +57,7 @@ import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.ota.OtaPackageService; +import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; @@ -163,6 +164,9 @@ public abstract class AbstractServiceTest { @Autowired protected OtaPackageService otaPackageService; + @Autowired + protected QueueService queueService; + public class IdComparator implements Comparator { @Override public int compare(D o1, D o2) { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseQueueServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseQueueServiceTest.java new file mode 100644 index 0000000000..29490dea04 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseQueueServiceTest.java @@ -0,0 +1,491 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.service; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.queue.ProcessingStrategy; +import org.thingsboard.server.common.data.queue.ProcessingStrategyType; +import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.common.data.queue.SubmitStrategy; +import org.thingsboard.server.common.data.queue.SubmitStrategyType; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.tenant.TenantServiceImpl; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class BaseQueueServiceTest extends AbstractServiceTest { + + private IdComparator idComparator = new IdComparator<>(); + + private TenantId tenantId; + private TenantProfileId tenantProfileId; + + @Before + public void before() throws NoSuchFieldException, IllegalAccessException { + Field zkEnabled = TenantServiceImpl.class.getDeclaredField("zkEnabled"); + zkEnabled.setAccessible(true); + zkEnabled.set(tenantService, Boolean.TRUE); + + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setDefault(false); + tenantProfile.setName("Isolated TB Rule Engine"); + tenantProfile.setDescription("Isolated TB Rule Engine tenant profile"); + tenantProfile.setIsolatedTbCore(false); + tenantProfile.setIsolatedTbRuleEngine(true); + tenantProfile.setMaxNumberOfQueues(10); + tenantProfile.setMaxNumberOfPartitionsPerQueue(10); + + TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + Assert.assertNotNull(savedTenantProfile); + tenantProfileId = savedTenantProfile.getId(); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + tenant.setTenantProfileId(tenantProfileId); + Tenant savedTenant = tenantService.saveTenant(tenant); + Assert.assertNotNull(savedTenant); + tenantId = savedTenant.getId(); + } + + @After + public void after() { + tenantService.deleteTenant(tenantId); + tenantProfileService.deleteTenantProfile(TenantId.SYS_TENANT_ID, tenantProfileId); + } + + private ProcessingStrategy createTestProcessingStrategy() { + ProcessingStrategy processingStrategy = new ProcessingStrategy(); + processingStrategy.setType(ProcessingStrategyType.SKIP_ALL_FAILURES); + processingStrategy.setRetries(3); + processingStrategy.setFailurePercentage(0); + processingStrategy.setPauseBetweenRetries(3); + processingStrategy.setMaxPauseBetweenRetries(3); + return processingStrategy; + } + + private SubmitStrategy createTestSubmitStrategy() { + SubmitStrategy submitStrategy = new SubmitStrategy(); + submitStrategy.setType(SubmitStrategyType.BURST); + submitStrategy.setBatchSize(1000); + return submitStrategy; + } + + @Test + public void testSaveQueue() { + Queue queue = new Queue(); + queue.setTenantId(tenantId); + queue.setName("Test"); + queue.setTopic("tb_rule_engine.test"); + queue.setPollInterval(25); + queue.setPartitions(1); + queue.setPackProcessingTimeout(2000); + queue.setSubmitStrategy(createTestSubmitStrategy()); + queue.setProcessingStrategy(createTestProcessingStrategy()); + Queue savedQueue = queueService.saveQueue(queue); + + Assert.assertNotNull(savedQueue); + Assert.assertNotNull(savedQueue.getId()); + Assert.assertTrue(savedQueue.getCreatedTime() > 0); + Assert.assertEquals(queue.getTenantId(), savedQueue.getTenantId()); + Assert.assertEquals(queue.getName(), queue.getName()); + + savedQueue.setPollInterval(100); + + queueService.saveQueue(savedQueue); + Queue foundQueue = queueService.findQueueById(tenantId, savedQueue.getId()); + Assert.assertEquals(foundQueue.getPollInterval(), savedQueue.getPollInterval()); + + queueService.deleteQueue(tenantId, foundQueue.getId()); + } + + @Test(expected = DataValidationException.class) + public void testSaveQueueWithEmptyName() { + Queue queue = new Queue(); + queue.setTenantId(tenantId); + queue.setTopic("tb_rule_engine.test"); + queue.setPollInterval(25); + queue.setPartitions(1); + queue.setPackProcessingTimeout(2000); + queue.setSubmitStrategy(createTestSubmitStrategy()); + queue.setProcessingStrategy(createTestProcessingStrategy()); + queueService.saveQueue(queue); + } + + @Test(expected = DataValidationException.class) + public void testSaveQueueWithEmptyTopic() { + Queue queue = new Queue(); + queue.setTenantId(tenantId); + queue.setName("Test"); + queue.setPollInterval(25); + queue.setPartitions(1); + queue.setPackProcessingTimeout(2000); + queue.setSubmitStrategy(createTestSubmitStrategy()); + queue.setProcessingStrategy(createTestProcessingStrategy()); + queueService.saveQueue(queue); + } + + @Test(expected = DataValidationException.class) + public void testSaveQueueWithEmptyPoolInterval() { + Queue queue = new Queue(); + queue.setTenantId(tenantId); + queue.setName("Test"); + queue.setTopic("tb_rule_engine.test"); + queue.setPartitions(1); + queue.setPackProcessingTimeout(2000); + queue.setSubmitStrategy(createTestSubmitStrategy()); + queue.setProcessingStrategy(createTestProcessingStrategy()); + queueService.saveQueue(queue); + } + + @Test(expected = DataValidationException.class) + public void testSaveQueueWithEmptyPartitions() { + Queue queue = new Queue(); + queue.setTenantId(tenantId); + queue.setName("Test"); + queue.setTopic("tb_rule_engine.test"); + queue.setPollInterval(25); + queue.setPackProcessingTimeout(2000); + queue.setSubmitStrategy(createTestSubmitStrategy()); + queue.setProcessingStrategy(createTestProcessingStrategy()); + queueService.saveQueue(queue); + } + + @Test(expected = DataValidationException.class) + public void testSaveQueueWithEmptyPackProcessingTimeout() { + Queue queue = new Queue(); + queue.setTenantId(tenantId); + queue.setName("Test"); + queue.setTopic("tb_rule_engine.test"); + queue.setPollInterval(25); + queue.setPartitions(1); + queue.setSubmitStrategy(createTestSubmitStrategy()); + queue.setProcessingStrategy(createTestProcessingStrategy()); + queueService.saveQueue(queue); + } + + @Test(expected = DataValidationException.class) + public void testSaveQueueWithEmptySubmitStrategy() { + Queue queue = new Queue(); + queue.setTenantId(tenantId); + queue.setName("Test"); + queue.setTopic("tb_rule_engine.test"); + queue.setPollInterval(25); + queue.setPartitions(1); + queue.setPackProcessingTimeout(2000); + queue.setProcessingStrategy(createTestProcessingStrategy()); + queueService.saveQueue(queue); + } + + @Test(expected = DataValidationException.class) + public void testSaveQueueWithEmptyProcessingStrategy() { + Queue queue = new Queue(); + queue.setTenantId(tenantId); + queue.setName("Test"); + queue.setTopic("tb_rule_engine.test"); + queue.setPollInterval(25); + queue.setPartitions(1); + queue.setPackProcessingTimeout(2000); + queue.setSubmitStrategy(createTestSubmitStrategy()); + queueService.saveQueue(queue); + } + + @Test(expected = DataValidationException.class) + public void testSaveQueueWithEmptySubmitStrategyType() { + Queue queue = new Queue(); + queue.setTenantId(tenantId); + queue.setName("Test"); + queue.setTopic("tb_rule_engine.test"); + queue.setPollInterval(25); + queue.setPartitions(1); + queue.setPackProcessingTimeout(2000); + queue.setSubmitStrategy(createTestSubmitStrategy()); + queue.getSubmitStrategy().setType(null); + queue.setProcessingStrategy(createTestProcessingStrategy()); + queueService.saveQueue(queue); + } + + @Test(expected = DataValidationException.class) + public void testSaveQueueWithEmptySubmitStrategyBatchSize() { + Queue queue = new Queue(); + queue.setTenantId(tenantId); + queue.setName("Test"); + queue.setTopic("tb_rule_engine.test"); + queue.setPollInterval(25); + queue.setPartitions(1); + queue.setPackProcessingTimeout(2000); + queue.setSubmitStrategy(createTestSubmitStrategy()); + queue.getSubmitStrategy().setType(SubmitStrategyType.BATCH); + queue.getSubmitStrategy().setBatchSize(0); + queue.setProcessingStrategy(createTestProcessingStrategy()); + queueService.saveQueue(queue); + } + + @Test(expected = DataValidationException.class) + public void testSaveQueueWithEmptyProcessingStrategyType() { + Queue queue = new Queue(); + queue.setTenantId(tenantId); + queue.setName("Test"); + queue.setTopic("tb_rule_engine.test"); + queue.setPollInterval(25); + queue.setPartitions(1); + queue.setPackProcessingTimeout(2000); + queue.setSubmitStrategy(createTestSubmitStrategy()); + queue.setProcessingStrategy(createTestProcessingStrategy()); + queue.getProcessingStrategy().setType(null); + queueService.saveQueue(queue); + } + + @Test(expected = DataValidationException.class) + public void testSaveQueueWithNegativeProcessingStrategyRetries() { + Queue queue = new Queue(); + queue.setTenantId(tenantId); + queue.setName("Test"); + queue.setTopic("tb_rule_engine.test"); + queue.setPollInterval(25); + queue.setPartitions(1); + queue.setPackProcessingTimeout(2000); + queue.setSubmitStrategy(createTestSubmitStrategy()); + queue.setProcessingStrategy(createTestProcessingStrategy()); + queue.getProcessingStrategy().setRetries(-1); + queueService.saveQueue(queue); + } + + @Test(expected = DataValidationException.class) + public void testSaveQueueWithNegativeProcessingStrategyFailurePercentage() { + Queue queue = new Queue(); + queue.setTenantId(tenantId); + queue.setName("Test"); + queue.setTopic("tb_rule_engine.test"); + queue.setPollInterval(25); + queue.setPartitions(1); + queue.setPackProcessingTimeout(2000); + queue.setSubmitStrategy(createTestSubmitStrategy()); + queue.setProcessingStrategy(createTestProcessingStrategy()); + queue.getProcessingStrategy().setFailurePercentage(-1); + queueService.saveQueue(queue); + } + + @Test(expected = DataValidationException.class) + public void testSaveQueueWithNegativeProcessingStrategyPauseBetweenRetries() { + Queue queue = new Queue(); + queue.setTenantId(tenantId); + queue.setName("Test"); + queue.setTopic("tb_rule_engine.test"); + queue.setPollInterval(25); + queue.setPartitions(1); + queue.setPackProcessingTimeout(2000); + queue.setSubmitStrategy(createTestSubmitStrategy()); + queue.setProcessingStrategy(createTestProcessingStrategy()); + queue.getProcessingStrategy().setPauseBetweenRetries(-1); + queueService.saveQueue(queue); + } + + @Test(expected = DataValidationException.class) + public void testSaveQueueWithProcessingStrategyPauseBetweenRetriesBiggerThenMaxPauseBetweenRetries() { + Queue queue = new Queue(); + queue.setTenantId(tenantId); + queue.setName("Test"); + queue.setTopic("tb_rule_engine.test"); + queue.setPollInterval(25); + queue.setPartitions(1); + queue.setPackProcessingTimeout(2000); + queue.setSubmitStrategy(createTestSubmitStrategy()); + queue.setProcessingStrategy(createTestProcessingStrategy()); + queue.getProcessingStrategy().setPauseBetweenRetries(100); + queueService.saveQueue(queue); + } + + @Test(expected = DataValidationException.class) + public void testSaveQueueWithNotIsolatedTenant() { + Tenant tenant = new Tenant(); + tenant.setTitle("Not isolated tenant"); + Tenant savedTenant = tenantService.saveTenant(tenant); + Assert.assertNotNull(savedTenant); + + Queue queue = new Queue(); + queue.setTenantId(savedTenant.getId()); + queue.setName("Test"); + queue.setTopic("tb_rule_engine.test"); + queue.setPollInterval(25); + queue.setPartitions(1); + queue.setPackProcessingTimeout(2000); + queue.setSubmitStrategy(createTestSubmitStrategy()); + queue.setProcessingStrategy(createTestProcessingStrategy()); + try { + queueService.saveQueue(queue); + } finally { + tenantService.deleteTenant(savedTenant.getId()); + } + } + + @Test(expected = DataValidationException.class) + public void testSaveQueueWithExceededLimitPerTenant() { + for (int i = 1; i <= 10; i++) { + //main queue created automatically + Queue queue = new Queue(); + queue.setTenantId(tenantId); + queue.setName("Test" + i); + queue.setTopic("tb_rule_engine.test" + i); + queue.setPollInterval(25); + queue.setPartitions(1); + queue.setPackProcessingTimeout(2000); + queue.setSubmitStrategy(createTestSubmitStrategy()); + queue.setProcessingStrategy(createTestProcessingStrategy()); + queueService.saveQueue(queue); + } + } + + @Test + public void testUpdateQueue() { + Queue queue = new Queue(); + queue.setTenantId(tenantId); + queue.setName("Test"); + queue.setTopic("tb_rule_engine.test"); + queue.setPollInterval(25); + queue.setPartitions(1); + queue.setPackProcessingTimeout(2000); + queue.setSubmitStrategy(createTestSubmitStrategy()); + queue.setProcessingStrategy(createTestProcessingStrategy()); + Queue savedQueue = queueService.saveQueue(queue); + + Assert.assertNotNull(savedQueue); + + queue.setPollInterval(1000); + + queueService.saveQueue(savedQueue); + + Queue foundQueue = queueService.findQueueById(tenantId, savedQueue.getId()); + + Assert.assertEquals(savedQueue, foundQueue); + } + + + @Test + public void testFindQueueById() { + Queue queue = new Queue(); + queue.setTenantId(tenantId); + queue.setName("Test"); + queue.setTopic("tb_rule_engine.test"); + queue.setPollInterval(25); + queue.setPartitions(1); + queue.setPackProcessingTimeout(2000); + queue.setSubmitStrategy(createTestSubmitStrategy()); + queue.setProcessingStrategy(createTestProcessingStrategy()); + Queue savedQueue = queueService.saveQueue(queue); + Queue foundQueue = queueService.findQueueById(tenantId, savedQueue.getId()); + Assert.assertNotNull(foundQueue); + Assert.assertEquals(savedQueue, foundQueue); + } + + @Test + public void testDeleteQueue() { + Queue queue = new Queue(); + queue.setTenantId(tenantId); + queue.setName("Test"); + queue.setTopic("tb_rule_engine.test"); + queue.setPollInterval(25); + queue.setPartitions(1); + queue.setPackProcessingTimeout(2000); + queue.setSubmitStrategy(createTestSubmitStrategy()); + queue.setProcessingStrategy(createTestProcessingStrategy()); + Queue savedQueue = queueService.saveQueue(queue); + Queue foundQueue = queueService.findQueueById(tenantId, savedQueue.getId()); + Assert.assertNotNull(foundQueue); + queueService.deleteQueue(tenantId, savedQueue.getId()); + foundQueue = queueService.findQueueById(tenantId, savedQueue.getId()); + Assert.assertNull(foundQueue); + } + + @Test + public void testFindQueueByTenantIdAndName() { + Queue queue = new Queue(); + queue.setTenantId(tenantId); + queue.setName("Test"); + queue.setTopic("tb_rule_engine.test"); + queue.setPollInterval(25); + queue.setPartitions(1); + queue.setPackProcessingTimeout(2000); + queue.setSubmitStrategy(createTestSubmitStrategy()); + queue.setProcessingStrategy(createTestProcessingStrategy()); + Queue savedQueue = queueService.saveQueue(queue); + Queue foundQueue = queueService.findQueueByTenantIdAndName(tenantId, savedQueue.getName()); + + Assert.assertNotNull(foundQueue); + Assert.assertEquals(savedQueue, foundQueue); + } + + @Test + public void testFindQueuesByTenantId() { + List queues = new ArrayList<>(); + for (int i = 1; i < 10; i++) { + Queue queue = new Queue(); + queue.setTenantId(tenantId); + queue.setName("Test" + i); + queue.setTopic("tb_rule_engine.test" + i); + queue.setPollInterval(25); + queue.setPartitions(1); + queue.setPackProcessingTimeout(2000); + queue.setSubmitStrategy(createTestSubmitStrategy()); + queue.setProcessingStrategy(createTestProcessingStrategy()); + + queues.add(queueService.saveQueue(queue)); + } + + List loadedQueues = new ArrayList<>(); + PageLink pageLink = new PageLink(3); + PageData pageData = null; + do { + pageData = queueService.findQueuesByTenantId(tenantId, pageLink); + loadedQueues.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + for (int i = 0; i < loadedQueues.size(); i++) { + Queue queue = loadedQueues.get(i); + if (queue.getName().equals("Main")) { + loadedQueues.remove(queue); + break; + } + } + + Collections.sort(queues, idComparator); + Collections.sort(loadedQueues, idComparator); + + Assert.assertEquals(queues, loadedQueues); + + queueService.deleteQueuesByTenantId(tenantId); + + pageLink = new PageLink(33); + pageData = queueService.findQueuesByTenantId(tenantId, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertTrue(pageData.getData().isEmpty()); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/QueueServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/QueueServiceSqlTest.java new file mode 100644 index 0000000000..320bb4fcaf --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/QueueServiceSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.service.sql; + +import org.thingsboard.server.dao.service.BaseQueueServiceTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class QueueServiceSqlTest extends BaseQueueServiceTest { +} From 3764ebfa1c59f88db38b739adb2846b3b11aa6a3 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 28 Mar 2022 22:38:31 +0300 Subject: [PATCH 023/276] implemented deleteTopic for queues --- .../server/queue/TbQueueAdmin.java | 2 +- .../common/msg/queue/TopicPartitionInfo.java | 2 +- .../queue/RuleEngineTbQueueAdminFactory.java | 114 ++++++++++++++++++ .../azure/servicebus/TbServiceBusAdmin.java | 25 ++++ .../server/queue/kafka/TbKafkaAdmin.java | 17 +++ .../InMemoryTbTransportQueueFactory.java | 3 + .../server/queue/pubsub/TbPubSubAdmin.java | 31 +++++ .../queue/rabbitmq/TbRabbitMqAdmin.java | 9 ++ .../server/queue/sqs/TbAwsSqsAdmin.java | 35 ++++-- .../server/dao/queue/BaseQueueService.java | 32 +++-- 10 files changed, 243 insertions(+), 27 deletions(-) create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/RuleEngineTbQueueAdminFactory.java diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueAdmin.java b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueAdmin.java index 37c99dfccb..1e5873ab67 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueAdmin.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueAdmin.java @@ -21,5 +21,5 @@ public interface TbQueueAdmin { void destroy(); - default void deleteTopic(String topic) { } + void deleteTopic(String topic); } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java index baf1ea0334..ae6a5146e0 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java @@ -41,7 +41,7 @@ public class TopicPartitionInfo { this.partition = partition; this.myPartition = myPartition; String tmp = topic; - if (tenantId != null) { + if (tenantId != null && !tenantId.isNullUid()) { tmp += "." + tenantId.getId().toString(); } if (partition != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/RuleEngineTbQueueAdminFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/RuleEngineTbQueueAdminFactory.java new file mode 100644 index 0000000000..1dd6c0045d --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/RuleEngineTbQueueAdminFactory.java @@ -0,0 +1,114 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.queue; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.thingsboard.server.queue.azure.servicebus.TbServiceBusAdmin; +import org.thingsboard.server.queue.azure.servicebus.TbServiceBusQueueConfigs; +import org.thingsboard.server.queue.azure.servicebus.TbServiceBusSettings; +import org.thingsboard.server.queue.kafka.TbKafkaAdmin; +import org.thingsboard.server.queue.kafka.TbKafkaSettings; +import org.thingsboard.server.queue.kafka.TbKafkaTopicConfigs; +import org.thingsboard.server.queue.pubsub.TbPubSubAdmin; +import org.thingsboard.server.queue.pubsub.TbPubSubSettings; +import org.thingsboard.server.queue.pubsub.TbPubSubSubscriptionSettings; +import org.thingsboard.server.queue.rabbitmq.TbRabbitMqAdmin; +import org.thingsboard.server.queue.rabbitmq.TbRabbitMqQueueArguments; +import org.thingsboard.server.queue.rabbitmq.TbRabbitMqSettings; +import org.thingsboard.server.queue.sqs.TbAwsSqsAdmin; +import org.thingsboard.server.queue.sqs.TbAwsSqsQueueAttributes; +import org.thingsboard.server.queue.sqs.TbAwsSqsSettings; + +@Configuration +public class RuleEngineTbQueueAdminFactory { + + @Autowired(required = false) + private TbKafkaTopicConfigs kafkaTopicConfigs; + @Autowired(required = false) + private TbKafkaSettings kafkaSettings; + + @Autowired(required = false) + private TbAwsSqsQueueAttributes awsSqsQueueAttributes; + @Autowired(required = false) + private TbAwsSqsSettings awsSqsSettings; + + @Autowired(required = false) + private TbPubSubSubscriptionSettings pubSubSubscriptionSettings; + @Autowired(required = false) + private TbPubSubSettings pubSubSettings; + + @Autowired(required = false) + private TbRabbitMqQueueArguments rabbitMqQueueArguments; + @Autowired(required = false) + private TbRabbitMqSettings rabbitMqSettings; + + @Autowired(required = false) + private TbServiceBusQueueConfigs serviceBusQueueConfigs; + @Autowired(required = false) + private TbServiceBusSettings serviceBusSettings; + + @ConditionalOnExpression("'${queue.type:null}'=='kafka'") + @Bean + public TbQueueAdmin createKafkaAdmin() { + return new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getRuleEngineConfigs()); + } + + @ConditionalOnExpression("'${queue.type:null}'=='aws-sqs'") + @Bean + public TbQueueAdmin createAwsSqsAdmin() { + return new TbAwsSqsAdmin(awsSqsSettings, awsSqsQueueAttributes.getRuleEngineAttributes()); + } + + @ConditionalOnExpression("'${queue.type:null}'=='pubsub'") + @Bean + public TbQueueAdmin createPubSubAdmin() { + return new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getRuleEngineSettings()); + } + + @ConditionalOnExpression("'${queue.type:null}'=='rabbitmq'") + @Bean + public TbQueueAdmin createRabbitMqAdmin() { + return new TbRabbitMqAdmin(rabbitMqSettings, rabbitMqQueueArguments.getRuleEngineArgs()); + } + + @ConditionalOnExpression("'${queue.type:null}'=='service-bus'") + @Bean + public TbQueueAdmin createServiceBusAdmin() { + return new TbServiceBusAdmin(serviceBusSettings, serviceBusQueueConfigs.getRuleEngineConfigs()); + } + + @ConditionalOnExpression("'${queue.type:null}'=='in-memory'") + @Bean + public TbQueueAdmin createInMemoryAdmin() { + return new TbQueueAdmin() { + + @Override + public void createTopicIfNotExists(String topic) { + } + + @Override + public void deleteTopic(String topic) { + } + + @Override + public void destroy() { + } + }; + } +} \ No newline at end of file diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusAdmin.java b/common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusAdmin.java index 71ec4b6811..c8f76a8eeb 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusAdmin.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusAdmin.java @@ -82,6 +82,31 @@ public class TbServiceBusAdmin implements TbQueueAdmin { } } + @Override + public void deleteTopic(String topic) { + if (queues.contains(topic)) { + doDelete(topic); + } else { + try { + if (client.getQueue(topic) != null) { + doDelete(topic); + } else { + log.warn("Azure Service Bus Queue [{}] is not exist.", topic); + } + } catch (ServiceBusException | InterruptedException e) { + log.error("Failed to delete Azure Service Bus queue [{}]", topic, e); + } + } + } + + private void doDelete(String topic) { + try { + client.deleteTopic(topic); + } catch (ServiceBusException | InterruptedException e) { + log.error("Failed to delete Azure Service Bus queue [{}]", topic, e); + } + } + private void setQueueConfigs(QueueDescription queueDescription) { queueConfigs.forEach((confKey, confValue) -> { switch (confKey) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java index 1034263690..d5a9a34dc7 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java @@ -84,6 +84,23 @@ public class TbKafkaAdmin implements TbQueueAdmin { } + @Override + public void deleteTopic(String topic) { + if (topics.contains(topic)) { + client.deleteTopics(Collections.singletonList(topic)); + } else { + try { + if (client.listTopics().names().get().contains(topic)) { + client.deleteTopics(Collections.singletonList(topic)); + } else { + log.warn("Kafka topic [{}] does not exist.", topic); + } + } catch (InterruptedException | ExecutionException e) { + log.error("Failed to delete kafka topic [{}].", topic, e); + } + } + } + @Override public void destroy() { if (client != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java index cb60272ace..252b70459e 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java @@ -73,6 +73,9 @@ public class InMemoryTbTransportQueueFactory implements TbTransportQueueFactory @Override public void destroy() {} + + @Override + public void deleteTopic(String topic) {} }); templateBuilder.requestTemplate(producerTemplate); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubAdmin.java b/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubAdmin.java index 7fb7cc9463..06d082feb1 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubAdmin.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubAdmin.java @@ -136,6 +136,37 @@ public class TbPubSubAdmin implements TbQueueAdmin { createSubscriptionIfNotExists(partition, topicName); } + @Override + public void deleteTopic(String topic) { + TopicName topicName = TopicName.newBuilder() + .setTopic(topic) + .setProject(pubSubSettings.getProjectId()) + .build(); + + ProjectSubscriptionName subscriptionName = + ProjectSubscriptionName.of(pubSubSettings.getProjectId(), topic); + + if (topicSet.contains(topicName.toString())) { + topicAdminClient.deleteTopic(topicName); + } else { + if (topicAdminClient.getTopic(topicName) != null) { + topicAdminClient.deleteTopic(topicName); + } else { + log.warn("PubSub topic [{}] does not exist.", topic); + } + } + + if (subscriptionSet.contains(subscriptionName.toString())) { + subscriptionAdminClient.deleteSubscription(subscriptionName); + } else { + if (subscriptionAdminClient.getSubscription(subscriptionName) != null) { + subscriptionAdminClient.deleteSubscription(subscriptionName); + } else { + log.warn("PubSub subscription [{}] does not exist.", topic); + } + } + } + private void createSubscriptionIfNotExists(String partition, TopicName topicName) { ProjectSubscriptionName subscriptionName = ProjectSubscriptionName.of(pubSubSettings.getProjectId(), partition); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqAdmin.java b/common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqAdmin.java index d2e5172a6f..3c7c18ad93 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqAdmin.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqAdmin.java @@ -58,6 +58,15 @@ public class TbRabbitMqAdmin implements TbQueueAdmin { } } + @Override + public void deleteTopic(String topic) { + try { + channel.queueDelete(topic); + } catch (IOException e) { + log.error("Failed to delete RabbitMq queue [{}].", topic); + } + } + @Override public void destroy() { if (channel != null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsAdmin.java b/common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsAdmin.java index 39ffc65e4a..3bd141f4db 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsAdmin.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsAdmin.java @@ -23,18 +23,20 @@ import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; import com.amazonaws.services.sqs.AmazonSQS; import com.amazonaws.services.sqs.AmazonSQSClientBuilder; import com.amazonaws.services.sqs.model.CreateQueueRequest; +import com.amazonaws.services.sqs.model.GetQueueUrlResult; +import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.queue.TbQueueAdmin; import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; import java.util.stream.Collectors; +@Slf4j public class TbAwsSqsAdmin implements TbQueueAdmin { private final Map attributes; private final AmazonSQS sqsClient; - private final Set queues; + private final Map queues; public TbAwsSqsAdmin(TbAwsSqsSettings sqsSettings, Map attributes) { this.attributes = attributes; @@ -57,18 +59,37 @@ public class TbAwsSqsAdmin implements TbQueueAdmin { .getQueueUrls() .stream() .map(this::getQueueNameFromUrl) - .collect(Collectors.toCollection(ConcurrentHashMap::newKeySet)); + .collect(Collectors.toMap(this::convertTopicToQueueName, Function.identity())); } @Override public void createTopicIfNotExists(String topic) { - String queueName = topic.replaceAll("\\.", "_") + ".fifo"; - if (queues.contains(queueName)) { + String queueName = convertTopicToQueueName(topic); + if (queues.containsKey(queueName)) { return; } final CreateQueueRequest createQueueRequest = new CreateQueueRequest(queueName).withAttributes(attributes); String queueUrl = sqsClient.createQueue(createQueueRequest).getQueueUrl(); - queues.add(getQueueNameFromUrl(queueUrl)); + queues.put(getQueueNameFromUrl(queueUrl), queueUrl); + } + + private String convertTopicToQueueName(String topic) { + return topic.replaceAll("\\.", "_") + ".fifo"; + } + + @Override + public void deleteTopic(String topic) { + String queueName = convertTopicToQueueName(topic); + if (queues.containsKey(queueName)) { + sqsClient.deleteQueue(queues.get(queueName)); + } else { + GetQueueUrlResult queueUrl = sqsClient.getQueueUrl(queueName); + if (queueUrl != null) { + sqsClient.deleteQueue(queueUrl.getQueueUrl()); + } else { + log.warn("Aws SQS queue [{}] does not exist!", queueName); + } + } } private String getQueueNameFromUrl(String queueUrl) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java index cad7f9cb48..75978ea371 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java @@ -38,7 +38,6 @@ import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; -import org.thingsboard.server.dao.tenant.TenantDao; import org.thingsboard.server.queue.TbQueueAdmin; import java.util.List; @@ -51,9 +50,6 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ @Autowired private QueueDao queueDao; - @Autowired - private TenantDao tenantDao; - @Autowired private TbTenantProfileCache tenantProfileCache; @@ -101,21 +97,21 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ int oldPartitions = oldQueue.getPartitions(); int currentPartitions = queue.getPartitions(); - //TODO: 3.2 remove if partitions can't be deleted. -// if (currentPartitions != oldPartitions && tbQueueAdmin != null) { + //TODO: remove if partitions can't be deleted. + if (currentPartitions != oldPartitions && tbQueueAdmin != null) { // queueClusterService.onQueueDelete(queue, null); -// if (currentPartitions > oldPartitions) { -// log.info("Added [{}] new partitions to [{}] queue", currentPartitions - oldPartitions, queue.getName()); -// for (int i = oldPartitions; i < currentPartitions; i++) { -// tbQueueAdmin.createTopicIfNotExists(new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName()); -// } -// } else { -// log.info("Removed [{}] partitions from [{}] queue", oldPartitions - currentPartitions, queue.getName()); -// for (int i = currentPartitions; i < oldPartitions; i++) { -// tbQueueAdmin.deleteTopic(new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName()); -// } -// } -// } + if (currentPartitions > oldPartitions) { + log.info("Added [{}] new partitions to [{}] queue", currentPartitions - oldPartitions, queue.getName()); + for (int i = oldPartitions; i < currentPartitions; i++) { + tbQueueAdmin.createTopicIfNotExists(new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName()); + } + } else { + log.info("Removed [{}] partitions from [{}] queue", oldPartitions - currentPartitions, queue.getName()); + for (int i = currentPartitions; i < oldPartitions; i++) { + tbQueueAdmin.deleteTopic(new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName()); + } + } + } return updatedQueue; } From 95f41810ac347e782baf7e00d3ad90dc3be27e22 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 29 Mar 2022 11:32:27 +0300 Subject: [PATCH 024/276] Store 2FA account config is UserCredentials' additionalInfo --- .../DefaultTwoFactorAuthConfigManager.java | 42 ++++++++++++------- .../server/dao/user/UserService.java | 14 +++---- .../common/data/security/UserCredentials.java | 9 +++- .../dao/model/sql/UserCredentialsEntity.java | 11 +++++ .../main/resources/sql/schema-entities.sql | 3 +- 5 files changed, 53 insertions(+), 26 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFactorAuthConfigManager.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFactorAuthConfigManager.java index bd486a6af0..ebdd03d734 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFactorAuthConfigManager.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFactorAuthConfigManager.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.security.auth.mfa.config; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; @@ -22,13 +23,13 @@ import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.DataConstants; -import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.settings.AdminSettingsDao; @@ -41,6 +42,7 @@ import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthPr import java.util.Collections; import java.util.Optional; import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; @Service @RequiredArgsConstructor @@ -62,9 +64,8 @@ public class DefaultTwoFactorAuthConfigManager implements TwoFactorAuthConfigMan @Override public Optional getTwoFaAccountConfig(TenantId tenantId, UserId userId) { - User user = userService.findUserById(tenantId, userId); - return Optional.ofNullable(user.getAdditionalInfo()) - .flatMap(additionalInfo -> Optional.ofNullable(additionalInfo.get(TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY)).filter(jsonNode -> !jsonNode.isNull())) + return Optional.ofNullable(getAccountInfo(tenantId, userId).get(TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY)) + .filter(JsonNode::isObject) .map(jsonNode -> JacksonUtil.treeToValue(jsonNode, TwoFactorAuthAccountConfig.class)) .filter(twoFactorAuthAccountConfig -> { return getTwoFaProviderConfig(tenantId, twoFactorAuthAccountConfig.getProviderType()).isPresent(); @@ -76,24 +77,33 @@ public class DefaultTwoFactorAuthConfigManager implements TwoFactorAuthConfigMan getTwoFaProviderConfig(tenantId, accountConfig.getProviderType()) .orElseThrow(() -> new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS)); - User user = userService.findUserById(tenantId, userId); - ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(user.getAdditionalInfo()) - .orElseGet(JacksonUtil::newObjectNode); - additionalInfo.set(TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY, JacksonUtil.valueToTree(accountConfig)); - user.setAdditionalInfo(additionalInfo); - - userService.saveUser(user); + updateAccountInfo(tenantId, userId, accountInfo -> { + accountInfo.set(TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY, JacksonUtil.valueToTree(accountConfig)); + }); } @Override public void deleteTwoFaAccountConfig(TenantId tenantId, UserId userId) { - User user = userService.findUserById(tenantId, userId); - ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(user.getAdditionalInfo()) + updateAccountInfo(tenantId, userId, accountInfo -> { + accountInfo.remove(TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY); + }); + } + + private ObjectNode getAccountInfo(TenantId tenantId, UserId userId) { + return (ObjectNode) Optional.ofNullable(userService.findUserCredentialsByUserId(tenantId, userId).getAdditionalInfo()) + .filter(JsonNode::isObject) .orElseGet(JacksonUtil::newObjectNode); - additionalInfo.remove(TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY); - user.setAdditionalInfo(additionalInfo); + } - userService.saveUser(user); + // FIXME [viacheslav]: upgrade script for credentials' additional info + private void updateAccountInfo(TenantId tenantId, UserId userId, Consumer updater) { + UserCredentials credentials = userService.findUserCredentialsByUserId(tenantId, userId); + ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(credentials.getAdditionalInfo()) + .filter(JsonNode::isObject) + .orElseGet(JacksonUtil::newObjectNode); + updater.accept(additionalInfo); + credentials.setAdditionalInfo(additionalInfo); + userService.saveUserCredentials(tenantId, credentials); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java index d5d29efa40..93da1ecd6c 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java @@ -51,24 +51,24 @@ public interface UserService { UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials); - void deleteUser(TenantId tenantId, UserId userId); + void deleteUser(TenantId tenantId, UserId userId); PageData findUsersByTenantId(TenantId tenantId, PageLink pageLink); PageData findTenantAdmins(TenantId tenantId, PageLink pageLink); - void deleteTenantAdmins(TenantId tenantId); + void deleteTenantAdmins(TenantId tenantId); PageData findCustomerUsers(TenantId tenantId, CustomerId customerId, PageLink pageLink); - void deleteCustomerUsers(TenantId tenantId, CustomerId customerId); + void deleteCustomerUsers(TenantId tenantId, CustomerId customerId); - void setUserCredentialsEnabled(TenantId tenantId, UserId userId, boolean enabled); + void setUserCredentialsEnabled(TenantId tenantId, UserId userId, boolean enabled); - void resetFailedLoginAttempts(TenantId tenantId, UserId userId); + void resetFailedLoginAttempts(TenantId tenantId, UserId userId); - int increaseFailedLoginAttempts(TenantId tenantId, UserId userId); + int increaseFailedLoginAttempts(TenantId tenantId, UserId userId); - void setLastLoginTs(TenantId tenantId, UserId userId); + void setLastLoginTs(TenantId tenantId, UserId userId); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java index 3816d27131..9f8dab575b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java @@ -16,12 +16,12 @@ package org.thingsboard.server.common.data.security; import lombok.EqualsAndHashCode; -import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; import org.thingsboard.server.common.data.id.UserCredentialsId; import org.thingsboard.server.common.data.id.UserId; @EqualsAndHashCode(callSuper = true) -public class UserCredentials extends BaseData { +public class UserCredentials extends SearchTextBasedWithAdditionalInfo { private static final long serialVersionUID = -2108436378880529163L; @@ -87,6 +87,11 @@ public class UserCredentials extends BaseData { public void setResetToken(String resetToken) { this.resetToken = resetToken; } + + @Override + public String getSearchText() { + return null; + } @Override public String toString() { diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserCredentialsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserCredentialsEntity.java index 4379af14e7..211977122a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserCredentialsEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserCredentialsEntity.java @@ -15,14 +15,18 @@ */ package org.thingsboard.server.dao.model.sql; +import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; import org.thingsboard.server.common.data.id.UserCredentialsId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.model.BaseEntity; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Column; import javax.persistence.Entity; @@ -31,6 +35,7 @@ import java.util.UUID; @Data @EqualsAndHashCode(callSuper = true) +@TypeDef(name = "json", typeClass = JsonStringType.class) @Entity @Table(name = ModelConstants.USER_CREDENTIALS_COLUMN_FAMILY_NAME) public final class UserCredentialsEntity extends BaseSqlEntity implements BaseEntity { @@ -50,6 +55,10 @@ public final class UserCredentialsEntity extends BaseSqlEntity @Column(name = ModelConstants.USER_CREDENTIALS_RESET_TOKEN_PROPERTY, unique = true) private String resetToken; + @Type(type = "json") + @Column(name = ModelConstants.ADDITIONAL_INFO_PROPERTY) + private JsonNode additionalInfo; + public UserCredentialsEntity() { super(); } @@ -66,6 +75,7 @@ public final class UserCredentialsEntity extends BaseSqlEntity this.password = userCredentials.getPassword(); this.activateToken = userCredentials.getActivateToken(); this.resetToken = userCredentials.getResetToken(); + this.additionalInfo = userCredentials.getAdditionalInfo(); } @Override @@ -79,6 +89,7 @@ public final class UserCredentialsEntity extends BaseSqlEntity userCredentials.setPassword(password); userCredentials.setActivateToken(activateToken); userCredentials.setResetToken(resetToken); + userCredentials.setAdditionalInfo(additionalInfo); return userCredentials; } diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 4e2596485b..34bee652e2 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -370,7 +370,8 @@ CREATE TABLE IF NOT EXISTS user_credentials ( enabled boolean, password varchar(255), reset_token varchar(255) UNIQUE, - user_id uuid UNIQUE + user_id uuid UNIQUE, + additional_info varchar ); CREATE TABLE IF NOT EXISTS widget_type ( From 922436d38b6d2b57cb5e7de73ee9434e7a84d8fe Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 29 Mar 2022 16:30:57 +0300 Subject: [PATCH 025/276] Tests for rate limits with different refill strategy --- .../common/msg/tools/RateLimitsTest.java | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 common/message/src/test/java/org/thingsboard/server/common/msg/tools/RateLimitsTest.java diff --git a/common/message/src/test/java/org/thingsboard/server/common/msg/tools/RateLimitsTest.java b/common/message/src/test/java/org/thingsboard/server/common/msg/tools/RateLimitsTest.java new file mode 100644 index 0000000000..d4cc408558 --- /dev/null +++ b/common/message/src/test/java/org/thingsboard/server/common/msg/tools/RateLimitsTest.java @@ -0,0 +1,87 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.common.msg.tools; + +import org.junit.Test; + +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public class RateLimitsTest { + + @Test + public void testRateLimits_greedyRefill() { + for (int period = 1; period <= 5; period++) { + for (int capacity = 1; capacity <= 5; capacity++) { + testRateLimitWithGreedyRefill(capacity, period); + } + } + } + + private void testRateLimitWithGreedyRefill(int capacity, int period) { + String rateLimitConfig = capacity + ":" + period; + TbRateLimits rateLimits = new TbRateLimits(rateLimitConfig); + + rateLimits.tryConsume(capacity); + assertThat(rateLimits.tryConsume()).as("new token is available").isFalse(); + + int expectedRefillTime = (int) (((double) period / capacity) * 1000); + int gap = 100; + + for (int i = 0; i < capacity; i++) { + await("token refill for rate limit " + rateLimitConfig) + .atLeast(expectedRefillTime - gap, TimeUnit.MILLISECONDS) + .atMost(expectedRefillTime + gap, TimeUnit.MILLISECONDS) + .untilAsserted(() -> { + assertThat(rateLimits.tryConsume()).as("token is available").isTrue(); + }); + assertThat(rateLimits.tryConsume()).as("new token is available").isFalse(); + } + } + + @Test + public void testRateLimits_intervalRefill() { + for (int period = 1; period <= 3; period++) { + for (int capacity = 1; capacity <= 3; capacity++) { + testRateLimitWithIntervalRefill(capacity, period); + } + } + } + + private void testRateLimitWithIntervalRefill(int capacity, int period) { + String rateLimitConfig = capacity + ":" + period; + TbRateLimits rateLimits = new TbRateLimits(rateLimitConfig, true); + + rateLimits.tryConsume(capacity); + assertThat(rateLimits.tryConsume()).as("new token is available").isFalse(); + + int expectedRefillTime = period * 1000; + int gap = 100; + + await("tokens refill for rate limit " + rateLimitConfig) + .atLeast(expectedRefillTime - gap, TimeUnit.MILLISECONDS) + .atMost(expectedRefillTime + gap, TimeUnit.MILLISECONDS) + .untilAsserted(() -> { + for (int i = 0; i < capacity; i++) { + assertThat(rateLimits.tryConsume()).as("token is available").isTrue(); + } + assertThat(rateLimits.tryConsume()).as("new token is available").isFalse(); + }); + } + +} From 190430ffc45da3971fcc652795d546b71adcec13 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 29 Mar 2022 19:18:00 +0300 Subject: [PATCH 026/276] Store 2FA account config in UserAuthSettings table --- .../TwoFactorAuthConfigController.java | 8 +- .../controller/TwoFactorAuthController.java | 4 +- .../auth/mfa/DefaultTwoFactorAuthService.java | 8 +- .../auth/mfa/TwoFactorAuthService.java | 4 +- .../DefaultTwoFactorAuthConfigManager.java | 62 ++++++-------- .../config/TwoFactorAuthConfigManager.java | 3 +- .../mfa/provider/TwoFactorAuthProvider.java | 5 +- .../impl/OtpBasedTwoFactorAuthProvider.java | 4 +- .../impl/SmsTwoFactorAuthProvider.java | 6 +- .../impl/TotpTwoFactorAuthProvider.java | 6 +- .../system/DefaultSystemSecurityService.java | 2 +- .../system/SystemSecurityService.java | 2 +- .../controller/TwoFactorAuthConfigTest.java | 16 ++-- .../server/controller/TwoFactorAuthTest.java | 14 ++-- .../common/data/id/UserAuthSettingsId.java | 26 ++++++ .../data/security/UserAuthSettings.java | 34 ++++++++ .../model/mfa}/TwoFactorAuthSettings.java | 6 +- .../OtpBasedTwoFactorAuthAccountConfig.java | 2 +- .../SmsTwoFactorAuthAccountConfig.java | 4 +- .../TotpTwoFactorAuthAccountConfig.java | 4 +- .../account/TwoFactorAuthAccountConfig.java | 4 +- .../OtpBasedTwoFactorAuthProviderConfig.java | 2 +- .../SmsTwoFactorAuthProviderConfig.java | 3 +- .../TotpTwoFactorAuthProviderConfig.java | 3 +- .../provider/TwoFactorAuthProviderConfig.java | 3 +- .../provider/TwoFactorAuthProviderType.java | 2 +- .../server/dao/model/ModelConstants.java | 7 ++ .../dao/model/sql/UserAuthSettingsEntity.java | 80 +++++++++++++++++++ .../dao/sql/user/JpaUserAuthSettingsDao.java | 56 +++++++++++++ .../sql/user/UserAuthSettingsRepository.java | 33 ++++++++ .../server/dao/user/UserAuthSettingsDao.java | 28 +++++++ .../server/dao/user/UserServiceImpl.java | 5 +- .../main/resources/sql/schema-entities.sql | 8 ++ 33 files changed, 356 insertions(+), 98 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/id/UserAuthSettingsId.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/security/UserAuthSettings.java rename {application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config => common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa}/TwoFactorAuthSettings.java (92%) rename {application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config => common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa}/account/OtpBasedTwoFactorAuthAccountConfig.java (91%) rename {application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config => common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa}/account/SmsTwoFactorAuthAccountConfig.java (89%) rename {application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config => common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa}/account/TotpTwoFactorAuthAccountConfig.java (90%) rename {application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config => common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa}/account/TwoFactorAuthAccountConfig.java (88%) rename {application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config => common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa}/provider/OtpBasedTwoFactorAuthProviderConfig.java (93%) rename {application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config => common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa}/provider/SmsTwoFactorAuthProviderConfig.java (91%) rename {application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config => common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa}/provider/TotpTwoFactorAuthProviderConfig.java (88%) rename {application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config => common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa}/provider/TwoFactorAuthProviderConfig.java (88%) rename {application/src/main/java/org/thingsboard/server/service/security/auth => common/data/src/main/java/org/thingsboard/server/common/data/security/model}/mfa/provider/TwoFactorAuthProviderType.java (90%) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/sql/UserAuthSettingsEntity.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserAuthSettingsDao.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/user/UserAuthSettingsRepository.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/user/UserAuthSettingsDao.java diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java index 29df21bca4..d4cc690604 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java @@ -36,10 +36,10 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager; -import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; -import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig; -import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; -import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; +import org.thingsboard.server.common.data.security.model.mfa.TwoFactorAuthSettings; +import org.thingsboard.server.common.data.security.model.mfa.account.TotpTwoFactorAuthAccountConfig; +import org.thingsboard.server.common.data.security.model.mfa.account.TwoFactorAuthAccountConfig; +import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType; import org.thingsboard.server.service.security.model.SecurityUser; import javax.servlet.ServletOutputStream; diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java index 1575673dcf..2a74d565b5 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -31,8 +31,8 @@ import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager; -import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; -import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; +import org.thingsboard.server.common.data.security.model.mfa.account.TwoFactorAuthAccountConfig; +import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.model.JwtTokenPair; import org.thingsboard.server.service.security.model.SecurityUser; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java index ee9e252831..9b2b0b90bd 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java @@ -25,15 +25,15 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType; import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager; -import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; -import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; -import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig; +import org.thingsboard.server.common.data.security.model.mfa.TwoFactorAuthSettings; +import org.thingsboard.server.common.data.security.model.mfa.account.TwoFactorAuthAccountConfig; +import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderConfig; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProvider; -import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.system.SystemSecurityService; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java index e07ac2b2fa..d84236cfb5 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java @@ -17,8 +17,8 @@ package org.thingsboard.server.service.security.auth.mfa; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; -import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; +import org.thingsboard.server.common.data.security.model.mfa.account.TwoFactorAuthAccountConfig; +import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType; import org.thingsboard.server.service.security.model.SecurityUser; public interface TwoFactorAuthService { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFactorAuthConfigManager.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFactorAuthConfigManager.java index ebdd03d734..a74b8374af 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFactorAuthConfigManager.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFactorAuthConfigManager.java @@ -15,8 +15,6 @@ */ package org.thingsboard.server.service.security.auth.mfa.config; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import org.springframework.stereotype.Service; @@ -29,31 +27,30 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.JsonDataEntry; -import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.common.data.security.UserAuthSettings; +import org.thingsboard.server.common.data.security.model.mfa.TwoFactorAuthSettings; +import org.thingsboard.server.common.data.security.model.mfa.account.TwoFactorAuthAccountConfig; +import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderConfig; +import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.settings.AdminSettingsDao; import org.thingsboard.server.dao.settings.AdminSettingsService; -import org.thingsboard.server.dao.user.UserService; -import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; -import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig; -import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; +import org.thingsboard.server.dao.user.UserAuthSettingsDao; import java.util.Collections; import java.util.Optional; import java.util.concurrent.ExecutionException; -import java.util.function.Consumer; @Service @RequiredArgsConstructor public class DefaultTwoFactorAuthConfigManager implements TwoFactorAuthConfigManager { - private final UserService userService; + private final UserAuthSettingsDao userAuthSettingsDao; private final AdminSettingsService adminSettingsService; private final AdminSettingsDao adminSettingsDao; private final AttributesService attributesService; - protected static final String TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY = "twoFaConfig"; protected static final String TWO_FACTOR_AUTH_SETTINGS_KEY = "twoFaSettings"; @@ -64,12 +61,9 @@ public class DefaultTwoFactorAuthConfigManager implements TwoFactorAuthConfigMan @Override public Optional getTwoFaAccountConfig(TenantId tenantId, UserId userId) { - return Optional.ofNullable(getAccountInfo(tenantId, userId).get(TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY)) - .filter(JsonNode::isObject) - .map(jsonNode -> JacksonUtil.treeToValue(jsonNode, TwoFactorAuthAccountConfig.class)) - .filter(twoFactorAuthAccountConfig -> { - return getTwoFaProviderConfig(tenantId, twoFactorAuthAccountConfig.getProviderType()).isPresent(); - }); + return Optional.ofNullable(userAuthSettingsDao.findByUserId(userId)) + .flatMap(userAuthSettings -> Optional.ofNullable(userAuthSettings.getTwoFaAccountConfig())) + .filter(twoFaAccountConfig -> getTwoFaProviderConfig(tenantId, twoFaAccountConfig.getProviderType()).isPresent()); } @Override @@ -77,33 +71,23 @@ public class DefaultTwoFactorAuthConfigManager implements TwoFactorAuthConfigMan getTwoFaProviderConfig(tenantId, accountConfig.getProviderType()) .orElseThrow(() -> new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS)); - updateAccountInfo(tenantId, userId, accountInfo -> { - accountInfo.set(TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY, JacksonUtil.valueToTree(accountConfig)); - }); + UserAuthSettings userAuthSettings = Optional.ofNullable(userAuthSettingsDao.findByUserId(userId)) + .orElseGet(() -> { + UserAuthSettings newUserAuthSettings = new UserAuthSettings(); + newUserAuthSettings.setUserId(userId); + return newUserAuthSettings; + }); + userAuthSettings.setTwoFaAccountConfig(accountConfig); + userAuthSettingsDao.save(tenantId, userAuthSettings); } @Override public void deleteTwoFaAccountConfig(TenantId tenantId, UserId userId) { - updateAccountInfo(tenantId, userId, accountInfo -> { - accountInfo.remove(TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY); - }); - } - - private ObjectNode getAccountInfo(TenantId tenantId, UserId userId) { - return (ObjectNode) Optional.ofNullable(userService.findUserCredentialsByUserId(tenantId, userId).getAdditionalInfo()) - .filter(JsonNode::isObject) - .orElseGet(JacksonUtil::newObjectNode); - } - - // FIXME [viacheslav]: upgrade script for credentials' additional info - private void updateAccountInfo(TenantId tenantId, UserId userId, Consumer updater) { - UserCredentials credentials = userService.findUserCredentialsByUserId(tenantId, userId); - ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(credentials.getAdditionalInfo()) - .filter(JsonNode::isObject) - .orElseGet(JacksonUtil::newObjectNode); - updater.accept(additionalInfo); - credentials.setAdditionalInfo(additionalInfo); - userService.saveUserCredentials(tenantId, credentials); + Optional.ofNullable(userAuthSettingsDao.findByUserId(userId)) + .ifPresent(userAuthSettings -> { + userAuthSettings.setTwoFaAccountConfig(null); + userAuthSettingsDao.save(tenantId, userAuthSettings); + }); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthConfigManager.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthConfigManager.java index 96189bf584..d1c5999e7e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthConfigManager.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthConfigManager.java @@ -18,7 +18,8 @@ package org.thingsboard.server.service.security.auth.mfa.config; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; +import org.thingsboard.server.common.data.security.model.mfa.TwoFactorAuthSettings; +import org.thingsboard.server.common.data.security.model.mfa.account.TwoFactorAuthAccountConfig; import java.util.Optional; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java index 030482ac6d..6961aeeaec 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java @@ -17,8 +17,9 @@ package org.thingsboard.server.service.security.auth.mfa.provider; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; -import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig; +import org.thingsboard.server.common.data.security.model.mfa.account.TwoFactorAuthAccountConfig; +import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderConfig; +import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType; import org.thingsboard.server.service.security.model.SecurityUser; public interface TwoFactorAuthProvider { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java index edeaf7ba3d..ce03f38230 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java @@ -21,8 +21,8 @@ import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.service.security.auth.mfa.config.account.OtpBasedTwoFactorAuthAccountConfig; -import org.thingsboard.server.service.security.auth.mfa.config.provider.OtpBasedTwoFactorAuthProviderConfig; +import org.thingsboard.server.common.data.security.model.mfa.account.OtpBasedTwoFactorAuthAccountConfig; +import org.thingsboard.server.common.data.security.model.mfa.provider.OtpBasedTwoFactorAuthProviderConfig; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProvider; import org.thingsboard.server.service.security.model.SecurityUser; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFactorAuthProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFactorAuthProvider.java index d6d99d03e8..9044762585 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFactorAuthProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFactorAuthProvider.java @@ -21,10 +21,10 @@ import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.security.model.mfa.account.SmsTwoFactorAuthAccountConfig; +import org.thingsboard.server.common.data.security.model.mfa.provider.SmsTwoFactorAuthProviderConfig; +import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.security.auth.mfa.config.account.SmsTwoFactorAuthAccountConfig; -import org.thingsboard.server.service.security.auth.mfa.config.provider.SmsTwoFactorAuthProviderConfig; -import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; import org.thingsboard.server.service.security.model.SecurityUser; import java.util.Map; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFactorAuthProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFactorAuthProvider.java index 83767ee3d8..9a30ea1fef 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFactorAuthProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFactorAuthProvider.java @@ -24,11 +24,11 @@ import org.jboss.aerogear.security.otp.api.Base32; import org.springframework.stereotype.Service; import org.springframework.web.util.UriComponentsBuilder; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig; -import org.thingsboard.server.service.security.auth.mfa.config.provider.TotpTwoFactorAuthProviderConfig; +import org.thingsboard.server.common.data.security.model.mfa.account.TotpTwoFactorAuthAccountConfig; +import org.thingsboard.server.common.data.security.model.mfa.provider.TotpTwoFactorAuthProviderConfig; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProvider; -import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; import org.thingsboard.server.service.security.model.SecurityUser; @Service diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java index c164ff04b4..82607cf70b 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java @@ -54,7 +54,7 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserServiceImpl; -import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; +import org.thingsboard.server.common.data.security.model.mfa.TwoFactorAuthSettings; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.exception.UserPasswordExpiredException; import org.thingsboard.server.service.security.model.SecurityUser; diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java index 4c14e45ae6..90241f723b 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java @@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; +import org.thingsboard.server.common.data.security.model.mfa.TwoFactorAuthSettings; import org.thingsboard.server.service.security.model.SecurityUser; import javax.servlet.http.HttpServletRequest; diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java index 38ff2e0702..6e3f11160f 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java @@ -31,14 +31,14 @@ import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager; -import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; -import org.thingsboard.server.service.security.auth.mfa.config.account.SmsTwoFactorAuthAccountConfig; -import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig; -import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; -import org.thingsboard.server.service.security.auth.mfa.config.provider.SmsTwoFactorAuthProviderConfig; -import org.thingsboard.server.service.security.auth.mfa.config.provider.TotpTwoFactorAuthProviderConfig; -import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig; -import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; +import org.thingsboard.server.common.data.security.model.mfa.TwoFactorAuthSettings; +import org.thingsboard.server.common.data.security.model.mfa.account.SmsTwoFactorAuthAccountConfig; +import org.thingsboard.server.common.data.security.model.mfa.account.TotpTwoFactorAuthAccountConfig; +import org.thingsboard.server.common.data.security.model.mfa.account.TwoFactorAuthAccountConfig; +import org.thingsboard.server.common.data.security.model.mfa.provider.SmsTwoFactorAuthProviderConfig; +import org.thingsboard.server.common.data.security.model.mfa.provider.TotpTwoFactorAuthProviderConfig; +import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderConfig; +import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType; import org.thingsboard.server.service.security.auth.mfa.provider.impl.OtpBasedTwoFactorAuthProvider; import org.thingsboard.server.service.security.auth.mfa.provider.impl.TotpTwoFactorAuthProvider; diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java index 09d1208df3..41ddd05c1b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java @@ -40,13 +40,13 @@ import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager; -import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; -import org.thingsboard.server.service.security.auth.mfa.config.account.SmsTwoFactorAuthAccountConfig; -import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig; -import org.thingsboard.server.service.security.auth.mfa.config.provider.SmsTwoFactorAuthProviderConfig; -import org.thingsboard.server.service.security.auth.mfa.config.provider.TotpTwoFactorAuthProviderConfig; -import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig; -import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; +import org.thingsboard.server.common.data.security.model.mfa.TwoFactorAuthSettings; +import org.thingsboard.server.common.data.security.model.mfa.account.SmsTwoFactorAuthAccountConfig; +import org.thingsboard.server.common.data.security.model.mfa.account.TotpTwoFactorAuthAccountConfig; +import org.thingsboard.server.common.data.security.model.mfa.provider.SmsTwoFactorAuthProviderConfig; +import org.thingsboard.server.common.data.security.model.mfa.provider.TotpTwoFactorAuthProviderConfig; +import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderConfig; +import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType; import org.thingsboard.server.service.security.auth.rest.LoginRequest; import org.thingsboard.server.service.security.model.JwtTokenPair; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/UserAuthSettingsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/UserAuthSettingsId.java new file mode 100644 index 0000000000..ae89bad915 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/UserAuthSettingsId.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.common.data.id; + +import java.util.UUID; + +public class UserAuthSettingsId extends UUIDBased { + + public UserAuthSettingsId(UUID id) { + super(id); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/UserAuthSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/UserAuthSettings.java new file mode 100644 index 0000000000..769f861435 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/UserAuthSettings.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.common.data.security; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.id.UserAuthSettingsId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.model.mfa.account.TwoFactorAuthAccountConfig; + +@Data +@EqualsAndHashCode(callSuper = true) +public class UserAuthSettings extends BaseData { + + private static final long serialVersionUID = 2628320657987010348L; + + private UserId userId; + private TwoFactorAuthAccountConfig twoFaAccountConfig; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/TwoFactorAuthSettings.java similarity index 92% rename from application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java rename to common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/TwoFactorAuthSettings.java index a39cfd1e37..49d5b64b4b 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/TwoFactorAuthSettings.java @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.security.auth.mfa.config; +package org.thingsboard.server.common.data.security.model.mfa; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; -import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig; -import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; +import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType; +import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderConfig; import javax.validation.Valid; import javax.validation.constraints.Min; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/OtpBasedTwoFactorAuthAccountConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/OtpBasedTwoFactorAuthAccountConfig.java similarity index 91% rename from application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/OtpBasedTwoFactorAuthAccountConfig.java rename to common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/OtpBasedTwoFactorAuthAccountConfig.java index ef090c63b4..c58287556b 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/OtpBasedTwoFactorAuthAccountConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/OtpBasedTwoFactorAuthAccountConfig.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.security.auth.mfa.config.account; +package org.thingsboard.server.common.data.security.model.mfa.account; import lombok.Data; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/SmsTwoFactorAuthAccountConfig.java similarity index 89% rename from application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java rename to common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/SmsTwoFactorAuthAccountConfig.java index ece6b780a4..2863f3f42e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/SmsTwoFactorAuthAccountConfig.java @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.security.auth.mfa.config.account; +package org.thingsboard.server.common.data.security.model.mfa.account; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; -import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; +import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Pattern; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TotpTwoFactorAuthAccountConfig.java similarity index 90% rename from application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java rename to common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TotpTwoFactorAuthAccountConfig.java index c67b1ee345..cc1171a713 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TotpTwoFactorAuthAccountConfig.java @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.security.auth.mfa.config.account; +package org.thingsboard.server.common.data.security.model.mfa.account; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; -import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; +import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Pattern; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TwoFactorAuthAccountConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFactorAuthAccountConfig.java similarity index 88% rename from application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TwoFactorAuthAccountConfig.java rename to common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFactorAuthAccountConfig.java index 31ee1e2807..fc1c9bc636 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TwoFactorAuthAccountConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFactorAuthAccountConfig.java @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.security.auth.mfa.config.account; +package org.thingsboard.server.common.data.security.model.mfa.account; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; -import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; +import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType; @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/OtpBasedTwoFactorAuthProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/OtpBasedTwoFactorAuthProviderConfig.java similarity index 93% rename from application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/OtpBasedTwoFactorAuthProviderConfig.java rename to common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/OtpBasedTwoFactorAuthProviderConfig.java index 6b4ef92cda..655816fcc6 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/OtpBasedTwoFactorAuthProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/OtpBasedTwoFactorAuthProviderConfig.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.security.auth.mfa.config.provider; +package org.thingsboard.server.common.data.security.model.mfa.provider; import io.swagger.annotations.ApiModelProperty; import lombok.Data; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/SmsTwoFactorAuthProviderConfig.java similarity index 91% rename from application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java rename to common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/SmsTwoFactorAuthProviderConfig.java index 88eca4cb2c..4096fe36f4 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/SmsTwoFactorAuthProviderConfig.java @@ -13,13 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.security.auth.mfa.config.provider; +package org.thingsboard.server.common.data.security.model.mfa.provider; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; -import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Pattern; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TotpTwoFactorAuthProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TotpTwoFactorAuthProviderConfig.java similarity index 88% rename from application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TotpTwoFactorAuthProviderConfig.java rename to common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TotpTwoFactorAuthProviderConfig.java index 8c0c324ae0..df44a662ec 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TotpTwoFactorAuthProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TotpTwoFactorAuthProviderConfig.java @@ -13,12 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.security.auth.mfa.config.provider; +package org.thingsboard.server.common.data.security.model.mfa.provider; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; -import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; import javax.validation.constraints.NotBlank; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TwoFactorAuthProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFactorAuthProviderConfig.java similarity index 88% rename from application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TwoFactorAuthProviderConfig.java rename to common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFactorAuthProviderConfig.java index f912f43144..24458af562 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TwoFactorAuthProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFactorAuthProviderConfig.java @@ -13,14 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.security.auth.mfa.config.provider; +package org.thingsboard.server.common.data.security.model.mfa.provider; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; -import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProviderType.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFactorAuthProviderType.java similarity index 90% rename from application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProviderType.java rename to common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFactorAuthProviderType.java index 9a4a3672a7..04e4401395 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProviderType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFactorAuthProviderType.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.security.auth.mfa.provider; +package org.thingsboard.server.common.data.security.model.mfa.provider; public enum TwoFactorAuthProviderType { TOTP, diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 8f692d5a12..d054b2b051 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -559,6 +559,13 @@ public class ModelConstants { public static final String EDGE_EVENT_BY_ID_VIEW_NAME = "edge_event_by_id"; + /** + * User auth settings constants. + * */ + public static final String USER_AUTH_SETTINGS_COLUMN_FAMILY_NAME = "user_auth_settings"; + public static final String USER_AUTH_SETTINGS_USER_ID_PROPERTY = USER_ID_PROPERTY; + public static final String USER_AUTH_SETTINGS_TWO_FA_ACCOUNT_CONFIG_PROPERTY = "mfa_account_config"; + /** * Cassandra attributes and timeseries constants. */ diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserAuthSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserAuthSettingsEntity.java new file mode 100644 index 0000000000..59c24c3405 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserAuthSettingsEntity.java @@ -0,0 +1,80 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.UserAuthSettingsId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.UserAuthSettings; +import org.thingsboard.server.common.data.security.model.mfa.account.TwoFactorAuthAccountConfig; +import org.thingsboard.server.dao.model.BaseEntity; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import java.util.UUID; + +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +@TypeDef(name = "json", typeClass = JsonStringType.class) +@Entity +@Table(name = ModelConstants.USER_AUTH_SETTINGS_COLUMN_FAMILY_NAME) // FIXME [viacheslav]: add to upgrade script +public class UserAuthSettingsEntity extends BaseSqlEntity implements BaseEntity { + + @Column(name = ModelConstants.USER_AUTH_SETTINGS_USER_ID_PROPERTY, nullable = false, unique = true) + private UUID userId; + @Type(type = "json") + @Column(name = ModelConstants.USER_AUTH_SETTINGS_TWO_FA_ACCOUNT_CONFIG_PROPERTY) + private JsonNode twoFaAccountConfig; + + public UserAuthSettingsEntity(UserAuthSettings userAuthSettings) { + if (userAuthSettings.getId() != null) { + this.setId(userAuthSettings.getId().getId()); + } + this.setCreatedTime(userAuthSettings.getCreatedTime()); + if (userAuthSettings.getUserId() != null) { + this.userId = userAuthSettings.getUserId().getId(); + } + if (userAuthSettings.getTwoFaAccountConfig() != null) { + this.twoFaAccountConfig = JacksonUtil.valueToTree(userAuthSettings.getTwoFaAccountConfig()); + } + } + + @Override + public UserAuthSettings toData() { + UserAuthSettings userAuthSettings = new UserAuthSettings(); + userAuthSettings.setId(new UserAuthSettingsId(id)); + userAuthSettings.setCreatedTime(createdTime); + if (userId != null) { + userAuthSettings.setUserId(new UserId(userId)); + } + if (twoFaAccountConfig != null) { + userAuthSettings.setTwoFaAccountConfig(JacksonUtil.treeToValue(twoFaAccountConfig, TwoFactorAuthAccountConfig.class)); + } + return userAuthSettings; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserAuthSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserAuthSettingsDao.java new file mode 100644 index 0000000000..55cc195f6b --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserAuthSettingsDao.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.sql.user; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.UserAuthSettings; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.UserAuthSettingsEntity; +import org.thingsboard.server.dao.sql.JpaAbstractDao; +import org.thingsboard.server.dao.user.UserAuthSettingsDao; + +import java.util.UUID; + +@Component +@RequiredArgsConstructor +public class JpaUserAuthSettingsDao extends JpaAbstractDao implements UserAuthSettingsDao { + + private final UserAuthSettingsRepository repository; + + @Override + public UserAuthSettings findByUserId(UserId userId) { + return DaoUtil.getData(repository.findByUserId(userId.getId())); + } + + @Override + public void removeByUserId(UserId userId) { + repository.deleteByUserId(userId.getId()); + } + + @Override + protected Class getEntityClass() { + return UserAuthSettingsEntity.class; + } + + @Override + protected JpaRepository getRepository() { + return repository; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserAuthSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserAuthSettingsRepository.java new file mode 100644 index 0000000000..38642a0161 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserAuthSettingsRepository.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.sql.user; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.dao.model.sql.UserAuthSettingsEntity; + +import java.util.UUID; + +@Repository +public interface UserAuthSettingsRepository extends JpaRepository { + + UserAuthSettingsEntity findByUserId(UUID userId); + + @Transactional + void deleteByUserId(UUID userId); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserAuthSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserAuthSettingsDao.java new file mode 100644 index 0000000000..50bc21d6f2 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserAuthSettingsDao.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.user; + +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.UserAuthSettings; +import org.thingsboard.server.dao.Dao; + +public interface UserAuthSettingsDao extends Dao { + + UserAuthSettings findByUserId(UserId userId); + + void removeByUserId(UserId userId); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index 218f604709..24b5dc837e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.ListenableFuture; -import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Value; @@ -69,17 +68,20 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic private final UserDao userDao; private final UserCredentialsDao userCredentialsDao; + private final UserAuthSettingsDao userAuthSettingsDao; private final DataValidator userValidator; private final DataValidator userCredentialsValidator; private final ApplicationEventPublisher eventPublisher; public UserServiceImpl(UserDao userDao, UserCredentialsDao userCredentialsDao, + UserAuthSettingsDao userAuthSettingsDao, DataValidator userValidator, DataValidator userCredentialsValidator, ApplicationEventPublisher eventPublisher) { this.userDao = userDao; this.userCredentialsDao = userCredentialsDao; + this.userAuthSettingsDao = userAuthSettingsDao; this.userValidator = userValidator; this.userCredentialsValidator = userCredentialsValidator; this.eventPublisher = eventPublisher; @@ -216,6 +218,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic validateId(userId, INCORRECT_USER_ID + userId); UserCredentials userCredentials = userCredentialsDao.findByUserId(tenantId, userId.getId()); userCredentialsDao.removeById(tenantId, userCredentials.getUuidId()); + userAuthSettingsDao.removeByUserId(userId); deleteEntityRelations(tenantId, userId); userDao.removeById(tenantId, userId.getId()); eventPublisher.publishEvent(new UserAuthDataChangedEvent(userId)); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 34bee652e2..da4bcc5bec 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -694,3 +694,11 @@ BEGIN deleted := ttl_deleted_count; END $$; + + +CREATE TABLE IF NOT EXISTS user_auth_settings ( + id uuid NOT NULL CONSTRAINT user_auth_settings_pkey PRIMARY KEY, + created_time bigint NOT NULL, + user_id uuid UNIQUE NOT NULL CONSTRAINT fk_user_auth_settings_user_id REFERENCES tb_user(id), + mfa_account_config varchar +); From e5eb484c2f6c0309e5a3e0f0fa8b0d466bbf1607 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 29 Mar 2022 21:42:46 +0300 Subject: [PATCH 027/276] added TbQueueClusterService and NorificationsTopicService --- .../queue/DefaultTbClusterService.java | 115 ++++++++++++++++-- .../DefaultSubscriptionManagerService.java | 22 ++-- .../DefaultTbCoreToTransportService.java | 13 +- .../server/cluster/TbClusterService.java | 10 +- .../server/queue/TbQueueClusterService.java | 24 ++++ .../queue/discovery/HashPartitionService.java | 14 --- .../discovery/NotificationsTopicService.java | 54 ++++++++ .../queue/discovery/PartitionService.java | 9 -- .../provider/AwsSqsMonolithQueueFactory.java | 22 ++-- .../provider/AwsSqsTbCoreQueueFactory.java | 10 +- .../AwsSqsTbRuleEngineQueueFactory.java | 10 +- .../InMemoryMonolithQueueFactory.java | 12 +- .../provider/KafkaMonolithQueueFactory.java | 12 +- .../provider/KafkaTbCoreQueueFactory.java | 11 +- .../KafkaTbRuleEngineQueueFactory.java | 10 +- .../provider/PubSubMonolithQueueFactory.java | 12 +- .../provider/PubSubTbCoreQueueFactory.java | 10 +- .../PubSubTbRuleEngineQueueFactory.java | 10 +- .../RabbitMqMonolithQueueFactory.java | 12 +- .../provider/RabbitMqTbCoreQueueFactory.java | 10 +- .../RabbitMqTbRuleEngineQueueFactory.java | 10 +- .../ServiceBusMonolithQueueFactory.java | 12 +- .../ServiceBusTbCoreQueueFactory.java | 10 +- .../ServiceBusTbRuleEngineQueueFactory.java | 10 +- .../service/DefaultTransportService.java | 6 +- .../server/dao/queue/BaseQueueService.java | 13 +- 26 files changed, 316 insertions(+), 147 deletions(-) create mode 100644 common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueClusterService.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/discovery/NotificationsTopicService.java diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 4344507dd9..ebe99d99e0 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -18,7 +18,9 @@ package org.thingsboard.server.service.queue; import com.google.protobuf.ByteString; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.thingsboard.rule.engine.api.msg.DeviceEdgeUpdateMsg; @@ -27,6 +29,7 @@ import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.Device; @@ -60,6 +63,7 @@ import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.MultipleTbQueueCallbackWrapper; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.discovery.NotificationsTopicService; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.service.gateway_device.GatewayNotificationsService; @@ -88,8 +92,12 @@ public class DefaultTbClusterService implements TbClusterService { private final AtomicInteger toRuleEngineNfs = new AtomicInteger(0); private final AtomicInteger toTransportNfs = new AtomicInteger(0); + @Autowired + @Lazy + private PartitionService partitionService; + private final TbQueueProducerProvider producerProvider; - private final PartitionService partitionService; + private final NotificationsTopicService notificationsTopicService; private final DataDecodingEncodingService encodingService; private final TbDeviceProfileCache deviceProfileCache; private final OtaPackageStateService otaPackageStateService; @@ -120,7 +128,7 @@ public class DefaultTbClusterService implements TbClusterService { @Override public void pushNotificationToCore(String serviceId, FromDeviceRpcResponse response, TbQueueCallback callback) { - TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_CORE, serviceId); + TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, serviceId); log.trace("PUSHING msg: {} to:{}", response, tpi); FromDeviceRPCResponseProto.Builder builder = FromDeviceRPCResponseProto.newBuilder() .setRequestIdMSB(response.getId().getMostSignificantBits()) @@ -185,7 +193,7 @@ public class DefaultTbClusterService implements TbClusterService { @Override public void pushNotificationToRuleEngine(String serviceId, FromDeviceRpcResponse response, TbQueueCallback callback) { - TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceId); + TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceId); log.trace("PUSHING msg: {} to:{}", response, tpi); FromDeviceRPCResponseProto.Builder builder = FromDeviceRPCResponseProto.newBuilder() .setRequestIdMSB(response.getId().getMostSignificantBits()) @@ -199,14 +207,14 @@ public class DefaultTbClusterService implements TbClusterService { @Override public void pushNotificationToTransport(String serviceId, ToTransportMsg response, TbQueueCallback callback) { - if (serviceId == null || serviceId.isEmpty()){ + if (serviceId == null || serviceId.isEmpty()) { log.trace("pushNotificationToTransport: skipping message without serviceId [{}], (ToTransportMsg) response [{}]", serviceId, response); if (callback != null) { callback.onSuccess(null); //callback that message already sent, no useful payload expected } return; } - TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_TRANSPORT, serviceId); + TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, serviceId); log.trace("PUSHING msg: {} to:{}", response, tpi); producerProvider.getTransportNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), response), callback); toTransportNfs.incrementAndGet(); @@ -314,7 +322,7 @@ public class DefaultTbClusterService implements TbClusterService { Set tbTransportServices = partitionService.getAllServiceIds(ServiceType.TB_TRANSPORT); TbQueueCallback proxyCallback = callback != null ? new MultipleTbQueueCallbackWrapper(tbTransportServices.size(), callback) : null; for (String transportServiceId : tbTransportServices) { - TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_TRANSPORT, transportServiceId); + TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, transportServiceId); toTransportNfProducer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), transportMsg), proxyCallback); toTransportNfs.incrementAndGet(); } @@ -328,7 +336,7 @@ public class DefaultTbClusterService implements TbClusterService { TbQueueProducer> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer(); Set tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE); for (String serviceId : tbCoreServices) { - TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_CORE, serviceId); + TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, serviceId); ToCoreNotificationMsg toCoreMsg = ToCoreNotificationMsg.newBuilder().setEdgeEventUpdateMsg(ByteString.copyFrom(msgBytes)).build(); toCoreNfProducer.send(tpi, new TbProtoQueueMsg<>(msg.getEdgeId().getId(), toCoreMsg), null); toCoreNfs.incrementAndGet(); @@ -349,7 +357,7 @@ public class DefaultTbClusterService implements TbClusterService { TbQueueProducer> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer(); Set tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE); for (String serviceId : tbCoreServices) { - TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_CORE, serviceId); + TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, serviceId); ToCoreNotificationMsg toCoreMsg = ToCoreNotificationMsg.newBuilder().setComponentLifecycleMsg(ByteString.copyFrom(msgBytes)).build(); toCoreNfProducer.send(tpi, new TbProtoQueueMsg<>(msg.getEntityId().getId(), toCoreMsg), null); toCoreNfs.incrementAndGet(); @@ -358,7 +366,7 @@ public class DefaultTbClusterService implements TbClusterService { tbRuleEngineServices.removeAll(tbCoreServices); } for (String serviceId : tbRuleEngineServices) { - TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceId); + TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceId); ToRuleEngineNotificationMsg toRuleEngineMsg = ToRuleEngineNotificationMsg.newBuilder().setComponentLifecycleMsg(ByteString.copyFrom(msgBytes)).build(); toRuleEngineProducer.send(tpi, new TbProtoQueueMsg<>(msg.getEntityId().getId(), toRuleEngineMsg), null); toRuleEngineNfs.incrementAndGet(); @@ -473,4 +481,93 @@ public class DefaultTbClusterService implements TbClusterService { break; } } + + @Override + public void onQueueChange(Queue queue, TbQueueCallback callback) { + log.trace("[{}][{}] Processing queue change [{}] event", queue.getTenantId(), queue.getId(), queue.getName()); + +// TransportProtos.QueueUpdateMsg queueUpdateMsg = TransportProtos.QueueUpdateMsg.newBuilder() +// .setTenantIdMSB(queue.getTenantId().getId().getMostSignificantBits()) +// .setTenantIdLSB(queue.getTenantId().getId().getLeastSignificantBits()) +// .setQueueName(queue.getName()) +// .setQueueTopic(queue.getTopic()) +// .setPartitions(queue.getPartitions()) +// .build(); +// +// if ("Main".equals(queue.getName())) { +// Set tbTransportServices = partitionService.getAllServices(ServiceType.TB_TRANSPORT); +// ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setQueueUpdateMsg(queueUpdateMsg).build(); +// for (TransportProtos.ServiceInfo transportService : tbTransportServices) { +// TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, transportService.getServiceId()); +// producerProvider.getTransportNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), transportMsg), callback); +// toTransportNfs.incrementAndGet(); +// } +// +// Set tbCoreServices = partitionService.getAllServices(ServiceType.TB_CORE); +// ToCoreNotificationMsg coreMsg = ToCoreNotificationMsg.newBuilder().setQueueUpdateMsg(queueUpdateMsg).build(); +// for (TransportProtos.ServiceInfo coreService : tbCoreServices) { +// TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, coreService.getServiceId()); +// producerProvider.getTbCoreNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), coreMsg), callback); +// toCoreNfs.incrementAndGet(); +// } +// } else { +// Set tbRuleEngineServices = partitionService.getAllServices(ServiceType.TB_RULE_ENGINE); +// ToRuleEngineNotificationMsg ruleEngineMsg = ToRuleEngineNotificationMsg.newBuilder().setQueueUpdateMsg(queueUpdateMsg).build(); +// for (TransportProtos.ServiceInfo ruleEngineService : tbRuleEngineServices) { +// TenantId tenantId = new TenantId(new UUID(ruleEngineService.getTenantIdMSB(), ruleEngineService.getTenantIdLSB())); +// if (tenantId.equals(queue.getTenantId())) { +// TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, ruleEngineService.getServiceId()); +// producerProvider.getRuleEngineNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), ruleEngineMsg), callback); +// toRuleEngineNfs.incrementAndGet(); +// } +// } +// } + } + + @Override + public void onQueueDelete(Queue queue, TbQueueCallback callback) { + log.trace("[{}][{}] Processing queue delete [{}] event", queue.getTenantId(), queue.getId(), queue.getName()); + +// TransportProtos.QueueDeleteMsg queueDeleteMsg = TransportProtos.QueueDeleteMsg.newBuilder() +// .setTenantIdMSB(queue.getTenantId().getId().getMostSignificantBits()) +// .setTenantIdLSB(queue.getTenantId().getId().getLeastSignificantBits()) +// .setQueueName(queue.getName()) +// .build(); +// +// if ("Main".equals(queue.getName())) { +// Set tbTransportServices = partitionService.getAllServices(ServiceType.TB_TRANSPORT); +// ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setQueueDeleteMsg(queueDeleteMsg).build(); +// for (TransportProtos.ServiceInfo transportService : tbTransportServices) { +// TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, transportService.getServiceId()); +// producerProvider.getTransportNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), transportMsg), callback); +// toTransportNfs.incrementAndGet(); +// } +// +// Set tbCoreServices = partitionService.getAllServices(ServiceType.TB_CORE); +// ToCoreNotificationMsg coreMsg = ToCoreNotificationMsg.newBuilder().setQueueDeleteMsg(queueDeleteMsg).build(); +// for (TransportProtos.ServiceInfo coreService : tbCoreServices) { +// TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, coreService.getServiceId()); +// producerProvider.getTbCoreNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), coreMsg), callback); +// toCoreNfs.incrementAndGet(); +// } +// } else { +// //TODO: 3.2 should be changed with smth like TransportApiRequestTemplate and get responses from all RE's +// Set tbRuleEngineServices = partitionService.getAllServices(ServiceType.TB_RULE_ENGINE); +// ToRuleEngineNotificationMsg ruleEngineMsg = ToRuleEngineNotificationMsg.newBuilder().setQueueDeleteMsg(queueDeleteMsg).build(); +// for (TransportProtos.ServiceInfo ruleEngineService : tbRuleEngineServices) { +// TenantId tenantId = new TenantId(new UUID(ruleEngineService.getTenantIdMSB(), ruleEngineService.getTenantIdLSB())); +// if (tenantId.equals(queue.getTenantId())) { +// TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, ruleEngineService.getServiceId()); +// log.info("Send notification about deleting queue [{}] to Rule Engine [{}]", queue.getName(), tpi.getFullTopicName()); +// producerProvider.getRuleEngineNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), ruleEngineMsg), callback); +// toRuleEngineNfs.incrementAndGet(); +// } +// } +// try { +// Thread.sleep(3000); +// } catch (InterruptedException e) { +// e.printStackTrace(); +// } +// } + } } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java index 83398e29ed..b158a51b77 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java @@ -50,6 +50,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.TbSubscriptionUpdate import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.discovery.NotificationsTopicService; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbApplicationEventListener; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; @@ -88,6 +89,9 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene @Autowired private TimeseriesService tsService; + @Autowired + private NotificationsTopicService notificationsTopicService; + @Autowired private PartitionService partitionService; @@ -264,7 +268,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene updateDeviceInactivityTimeout(tenantId, entityId, attributes); } else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) { clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onUpdate(tenantId, - new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, new ArrayList<>(attributes)) + new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, new ArrayList<>(attributes)) , null); } } @@ -377,7 +381,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(s.getSubscriptionId(), subscriptionUpdate); localSubscriptionService.onSubscriptionUpdate(s.getSessionId(), update, TbCallback.EMPTY); } else { - TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_CORE, s.getServiceId()); + TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, s.getServiceId()); toCoreNotificationsProducer.send(tpi, toProto(s, subscriptionUpdate, ignoreEmptyUpdates), null); } } @@ -400,7 +404,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene AlarmSubscriptionUpdate update = new AlarmSubscriptionUpdate(s.getSubscriptionId(), alarm, deleted); localSubscriptionService.onSubscriptionUpdate(s.getSessionId(), update, TbCallback.EMPTY); } else { - TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_CORE, s.getServiceId()); + TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, s.getServiceId()); toCoreNotificationsProducer.send(tpi, toProto(s, alarm, deleted), null); } } @@ -441,7 +445,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene } }); if (!missedUpdates.isEmpty()) { - TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_CORE, subscription.getServiceId()); + TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, subscription.getServiceId()); toCoreNotificationsProducer.send(tpi, toProto(subscription, missedUpdates), null); } }, @@ -464,7 +468,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene DonAsynchron.withCallback(tsService.findLatest(subscription.getTenantId(), subscription.getEntityId(), subscription.getKeyStates().keySet()), missedUpdates -> { if (missedUpdates != null && !missedUpdates.isEmpty()) { - TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_CORE, subscription.getServiceId()); + TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, subscription.getServiceId()); toCoreNotificationsProducer.send(tpi, toProto(subscription, missedUpdates), null); } }, @@ -483,7 +487,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene DonAsynchron.withCallback(tsService.findAll(subscription.getTenantId(), subscription.getEntityId(), queries), missedUpdates -> { if (missedUpdates != null && !missedUpdates.isEmpty()) { - TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_CORE, subscription.getServiceId()); + TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, subscription.getServiceId()); toCoreNotificationsProducer.send(tpi, toProto(subscription, missedUpdates), null); } }, @@ -533,7 +537,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene }); ToCoreNotificationMsg toCoreMsg = ToCoreNotificationMsg.newBuilder().setToLocalSubscriptionServiceMsg( - LocalSubscriptionServiceMsgProto.newBuilder().setSubUpdate(builder.build()).build()) + LocalSubscriptionServiceMsgProto.newBuilder().setSubUpdate(builder.build()).build()) .build(); return new TbProtoQueueMsg<>(subscription.getEntityId().getId(), toCoreMsg); } @@ -547,8 +551,8 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene builder.setDeleted(deleted); ToCoreNotificationMsg toCoreMsg = ToCoreNotificationMsg.newBuilder().setToLocalSubscriptionServiceMsg( - LocalSubscriptionServiceMsgProto.newBuilder() - .setAlarmSubUpdate(builder.build()).build()) + LocalSubscriptionServiceMsgProto.newBuilder() + .setAlarmSubUpdate(builder.build()).build()) .build(); return new TbProtoQueueMsg<>(subscription.getEntityId().getId(), toCoreMsg); } diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTbCoreToTransportService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTbCoreToTransportService.java index 883cfd645c..0228743076 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTbCoreToTransportService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTbCoreToTransportService.java @@ -16,7 +16,6 @@ package org.thingsboard.server.service.transport; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; @@ -25,7 +24,7 @@ import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.NotificationsTopicService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -39,11 +38,11 @@ import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; @TbCoreComponent public class DefaultTbCoreToTransportService implements TbCoreToTransportService { - private final PartitionService partitionService; + private final NotificationsTopicService notificationsTopicService; private final TbQueueProducer> tbTransportProducer; - public DefaultTbCoreToTransportService(PartitionService partitionService, TbQueueProducerProvider tbQueueProducerProvider) { - this.partitionService = partitionService; + public DefaultTbCoreToTransportService(NotificationsTopicService notificationsTopicService, TbQueueProducerProvider tbQueueProducerProvider) { + this.notificationsTopicService = notificationsTopicService; this.tbTransportProducer = tbQueueProducerProvider.getTransportNotificationsMsgProducer(); } @@ -54,14 +53,14 @@ public class DefaultTbCoreToTransportService implements TbCoreToTransportService @Override public void process(String nodeId, ToTransportMsg msg, Runnable onSuccess, Consumer onFailure) { - if (nodeId == null || nodeId.isEmpty()){ + if (nodeId == null || nodeId.isEmpty()) { log.trace("process: skipping message without nodeId [{}], (ToTransportMsg) msg [{}]", nodeId, msg); if (onSuccess != null) { onSuccess.run(); } return; } - TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_TRANSPORT, nodeId); + TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, nodeId); UUID sessionId = new UUID(msg.getSessionIdMSB(), msg.getSessionIdLSB()); log.trace("[{}][{}] Pushing session data to topic: {}", tpi.getFullTopicName(), sessionId, msg); TbProtoQueueMsg queueMsg = new TbProtoQueueMsg<>(NULL_UUID, msg); diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java index 7fb3a83425..22953f259a 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java @@ -15,31 +15,31 @@ */ package org.thingsboard.server.cluster; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; -import org.thingsboard.server.common.data.edge.EdgeEventType; -import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; -import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueClusterService; import java.util.UUID; -public interface TbClusterService { +public interface TbClusterService extends TbQueueClusterService { void pushMsgToCore(TopicPartitionInfo tpi, UUID msgKey, ToCoreMsg msg, TbQueueCallback callback); diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueClusterService.java b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueClusterService.java new file mode 100644 index 0000000000..d37760211f --- /dev/null +++ b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueClusterService.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.queue; + +import org.thingsboard.server.common.data.queue.Queue; + +public interface TbQueueClusterService { + void onQueueChange(Queue queue, TbQueueCallback callback); + + void onQueueDelete(Queue queue, TbQueueCallback callback); +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index 4882a96f72..96df34186c 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -213,20 +213,6 @@ public class HashPartitionService implements PartitionService { return result; } - @Override - public TopicPartitionInfo getNotificationsTopic(ServiceType serviceType, String serviceId) { - switch (serviceType) { - case TB_CORE: - return tbCoreNotificationTopics.computeIfAbsent(serviceId, - id -> buildNotificationsTopicPartitionInfo(serviceType, serviceId)); - case TB_RULE_ENGINE: - return tbRuleEngineNotificationTopics.computeIfAbsent(serviceId, - id -> buildNotificationsTopicPartitionInfo(serviceType, serviceId)); - default: - return buildNotificationsTopicPartitionInfo(serviceType, serviceId); - } - } - @Override public int resolvePartitionIndex(UUID entityId, int partitions) { int hash = hashFunction.newHasher() diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/NotificationsTopicService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/NotificationsTopicService.java new file mode 100644 index 0000000000..2c259a3ce0 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/NotificationsTopicService.java @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.queue.discovery; + +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; + +import java.util.HashMap; +import java.util.Map; + +@Service +public class NotificationsTopicService { + + private Map tbCoreNotificationTopics = new HashMap<>(); + private Map tbRuleEngineNotificationTopics = new HashMap<>(); + + /** + * Each Service should start a consumer for messages that target individual service instance based on serviceId. + * This topic is likely to have single partition, and is always assigned to the service. + * @param serviceType + * @param serviceId + * @return + */ + public TopicPartitionInfo getNotificationsTopic(ServiceType serviceType, String serviceId) { + switch (serviceType) { + case TB_CORE: + return tbCoreNotificationTopics.computeIfAbsent(serviceId, + id -> buildNotificationsTopicPartitionInfo(serviceType, serviceId)); + case TB_RULE_ENGINE: + return tbRuleEngineNotificationTopics.computeIfAbsent(serviceId, + id -> buildNotificationsTopicPartitionInfo(serviceType, serviceId)); + default: + return buildNotificationsTopicPartitionInfo(serviceType, serviceId); + } + } + + private TopicPartitionInfo buildNotificationsTopicPartitionInfo(ServiceType serviceType, String serviceId) { + return new TopicPartitionInfo(serviceType.name().toLowerCase() + ".notifications." + serviceId, null, null, false); + } +} \ No newline at end of file diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java index c011e6545a..c222b252a5 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java @@ -49,15 +49,6 @@ public interface PartitionService { */ Set getAllServiceIds(ServiceType serviceType); - /** - * Each Service should start a consumer for messages that target individual service instance based on serviceId. - * This topic is likely to have single partition, and is always assigned to the service. - * @param serviceType - * @param serviceId - * @return - */ - TopicPartitionInfo getNotificationsTopic(ServiceType serviceType, String serviceId); - int resolvePartitionIndex(UUID entityId, int partitions); int countTransportsByType(String type); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java index 9979e03608..873719c832 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java @@ -22,7 +22,15 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.js.JsInvokeProtos.RemoteJsRequest; import org.thingsboard.server.gen.js.JsInvokeProtos.RemoteJsResponse; -import org.thingsboard.server.gen.transport.TransportProtos.*; +import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; @@ -30,7 +38,7 @@ import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.NotificationsTopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; @@ -51,7 +59,7 @@ import java.nio.charset.StandardCharsets; @ConditionalOnExpression("'${queue.type:null}'=='aws-sqs' && '${service.type:null}'=='monolith'") public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngineQueueFactory { - private final PartitionService partitionService; + private final NotificationsTopicService notificationsTopicService; private final TbQueueCoreSettings coreSettings; private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueRuleEngineSettings ruleEngineSettings; @@ -66,7 +74,7 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng private final TbQueueAdmin transportApiAdmin; private final TbQueueAdmin notificationAdmin; - public AwsSqsMonolithQueueFactory(PartitionService partitionService, TbQueueCoreSettings coreSettings, + public AwsSqsMonolithQueueFactory(NotificationsTopicService notificationsTopicService, TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, TbServiceInfoProvider serviceInfoProvider, TbQueueTransportApiSettings transportApiSettings, @@ -74,7 +82,7 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng TbAwsSqsSettings sqsSettings, TbAwsSqsQueueAttributes sqsQueueAttributes, TbQueueRemoteJsInvokeSettings jsInvokeSettings) { - this.partitionService = partitionService; + this.notificationsTopicService = notificationsTopicService; this.coreSettings = coreSettings; this.serviceInfoProvider = serviceInfoProvider; this.ruleEngineSettings = ruleEngineSettings; @@ -124,7 +132,7 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng @Override public TbQueueConsumer> createToRuleEngineNotificationsMsgConsumer() { return new TbAwsSqsConsumerTemplate<>(notificationAdmin, sqsSettings, - partitionService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName(), + notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); } @@ -137,7 +145,7 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng @Override public TbQueueConsumer> createToCoreNotificationsMsgConsumer() { return new TbAwsSqsConsumerTemplate<>(notificationAdmin, sqsSettings, - partitionService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName(), + notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java index a09e832892..81736c2b55 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueFactory.java @@ -37,7 +37,7 @@ import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.NotificationsTopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; @@ -61,7 +61,7 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { private final TbQueueRuleEngineSettings ruleEngineSettings; private final TbQueueCoreSettings coreSettings; private final TbQueueTransportApiSettings transportApiSettings; - private final PartitionService partitionService; + private final NotificationsTopicService notificationsTopicService; private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; @@ -76,7 +76,7 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { TbQueueCoreSettings coreSettings, TbQueueTransportApiSettings transportApiSettings, TbQueueRuleEngineSettings ruleEngineSettings, - PartitionService partitionService, + NotificationsTopicService notificationsTopicService, TbServiceInfoProvider serviceInfoProvider, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbAwsSqsQueueAttributes sqsQueueAttributes, @@ -85,7 +85,7 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { this.coreSettings = coreSettings; this.transportApiSettings = transportApiSettings; this.ruleEngineSettings = ruleEngineSettings; - this.partitionService = partitionService; + this.notificationsTopicService = notificationsTopicService; this.serviceInfoProvider = serviceInfoProvider; this.jsInvokeSettings = jsInvokeSettings; this.transportNotificationSettings = transportNotificationSettings; @@ -131,7 +131,7 @@ public class AwsSqsTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueConsumer> createToCoreNotificationsMsgConsumer() { return new TbAwsSqsConsumerTemplate<>(notificationAdmin, sqsSettings, - partitionService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName(), + notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java index ac4898e48d..9c223f08f5 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java @@ -32,7 +32,7 @@ import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.NotificationsTopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; @@ -52,7 +52,7 @@ import java.nio.charset.StandardCharsets; @ConditionalOnExpression("'${queue.type:null}'=='aws-sqs' && '${service.type:null}'=='tb-rule-engine'") public class AwsSqsTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { - private final PartitionService partitionService; + private final NotificationsTopicService notificationsTopicService; private final TbQueueCoreSettings coreSettings; private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueRuleEngineSettings ruleEngineSettings; @@ -65,14 +65,14 @@ public class AwsSqsTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory private final TbQueueAdmin jsExecutorAdmin; private final TbQueueAdmin notificationAdmin; - public AwsSqsTbRuleEngineQueueFactory(PartitionService partitionService, TbQueueCoreSettings coreSettings, + public AwsSqsTbRuleEngineQueueFactory(NotificationsTopicService notificationsTopicService, TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, TbServiceInfoProvider serviceInfoProvider, TbAwsSqsSettings sqsSettings, TbAwsSqsQueueAttributes sqsQueueAttributes, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbQueueTransportNotificationSettings transportNotificationSettings) { - this.partitionService = partitionService; + this.notificationsTopicService = notificationsTopicService; this.coreSettings = coreSettings; this.serviceInfoProvider = serviceInfoProvider; this.ruleEngineSettings = ruleEngineSettings; @@ -120,7 +120,7 @@ public class AwsSqsTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory @Override public TbQueueConsumer> createToRuleEngineNotificationsMsgConsumer() { return new TbAwsSqsConsumerTemplate<>(notificationAdmin, sqsSettings, - partitionService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName(), + notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName(), msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToRuleEngineNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java index 0548603cb7..e3c81df1dd 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java @@ -27,7 +27,7 @@ import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.NotificationsTopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.memory.InMemoryStorage; import org.thingsboard.server.queue.memory.InMemoryTbQueueConsumer; @@ -43,7 +43,7 @@ import org.thingsboard.server.queue.settings.TbRuleEngineQueueConfiguration; @ConditionalOnExpression("'${queue.type:null}'=='in-memory' && '${service.type:null}'=='monolith'") public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngineQueueFactory { - private final PartitionService partitionService; + private final NotificationsTopicService notificationsTopicService; private final TbQueueCoreSettings coreSettings; private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueRuleEngineSettings ruleEngineSettings; @@ -51,12 +51,12 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE private final TbQueueTransportNotificationSettings transportNotificationSettings; private final InMemoryStorage storage; - public InMemoryMonolithQueueFactory(PartitionService partitionService, TbQueueCoreSettings coreSettings, + public InMemoryMonolithQueueFactory(NotificationsTopicService notificationsTopicService, TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, TbServiceInfoProvider serviceInfoProvider, TbQueueTransportApiSettings transportApiSettings, TbQueueTransportNotificationSettings transportNotificationSettings) { - this.partitionService = partitionService; + this.notificationsTopicService = notificationsTopicService; this.coreSettings = coreSettings; this.serviceInfoProvider = serviceInfoProvider; this.ruleEngineSettings = ruleEngineSettings; @@ -97,7 +97,7 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE @Override public TbQueueConsumer> createToRuleEngineNotificationsMsgConsumer() { - return new InMemoryTbQueueConsumer<>(partitionService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName()); + return new InMemoryTbQueueConsumer<>(notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName()); } @Override @@ -107,7 +107,7 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE @Override public TbQueueConsumer> createToCoreNotificationsMsgConsumer() { - return new InMemoryTbQueueConsumer<>(partitionService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName()); + return new InMemoryTbQueueConsumer<>(notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName()); } @Override diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index cf75a77135..ce171497de 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -37,7 +37,7 @@ import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.NotificationsTopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.kafka.TbKafkaAdmin; import org.thingsboard.server.queue.kafka.TbKafkaConsumerStatsService; @@ -60,7 +60,7 @@ import java.util.concurrent.atomic.AtomicLong; @ConditionalOnExpression("'${queue.type:null}'=='kafka' && '${service.type:null}'=='monolith'") public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngineQueueFactory { - private final PartitionService partitionService; + private final NotificationsTopicService notificationsTopicService; private final TbKafkaSettings kafkaSettings; private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueCoreSettings coreSettings; @@ -78,7 +78,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi private final TbQueueAdmin fwUpdatesAdmin; private final AtomicLong consumerCount = new AtomicLong(); - public KafkaMonolithQueueFactory(PartitionService partitionService, TbKafkaSettings kafkaSettings, + public KafkaMonolithQueueFactory(NotificationsTopicService notificationsTopicService, TbKafkaSettings kafkaSettings, TbServiceInfoProvider serviceInfoProvider, TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, @@ -87,7 +87,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbKafkaConsumerStatsService consumerStatsService, TbKafkaTopicConfigs kafkaTopicConfigs) { - this.partitionService = partitionService; + this.notificationsTopicService = notificationsTopicService; this.kafkaSettings = kafkaSettings; this.serviceInfoProvider = serviceInfoProvider; this.coreSettings = coreSettings; @@ -173,7 +173,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi public TbQueueConsumer> createToRuleEngineNotificationsMsgConsumer() { TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); - consumerBuilder.topic(partitionService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName()); + consumerBuilder.topic(notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName()); consumerBuilder.clientId("monolith-rule-engine-notifications-consumer-" + serviceInfoProvider.getServiceId()); consumerBuilder.groupId("monolith-rule-engine-notifications-consumer-" + serviceInfoProvider.getServiceId()); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); @@ -199,7 +199,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi public TbQueueConsumer> createToCoreNotificationsMsgConsumer() { TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); - consumerBuilder.topic(partitionService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName()); + consumerBuilder.topic(notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName()); consumerBuilder.clientId("monolith-core-notifications-consumer-" + serviceInfoProvider.getServiceId()); consumerBuilder.groupId("monolith-core-notifications-consumer-" + serviceInfoProvider.getServiceId()); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java index 4f12f797f0..58dec0792a 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java @@ -37,7 +37,7 @@ import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.NotificationsTopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.kafka.TbKafkaAdmin; import org.thingsboard.server.queue.kafka.TbKafkaConsumerStatsService; @@ -58,7 +58,7 @@ import java.nio.charset.StandardCharsets; @ConditionalOnExpression("'${queue.type:null}'=='kafka' && '${service.type:null}'=='tb-core'") public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { - private final PartitionService partitionService; + private final NotificationsTopicService notificationsTopicService; private final TbKafkaSettings kafkaSettings; private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueCoreSettings coreSettings; @@ -75,7 +75,8 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { private final TbQueueAdmin notificationAdmin; private final TbQueueAdmin fwUpdatesAdmin; - public KafkaTbCoreQueueFactory(PartitionService partitionService, TbKafkaSettings kafkaSettings, + public KafkaTbCoreQueueFactory(NotificationsTopicService notificationsTopicService, + TbKafkaSettings kafkaSettings, TbServiceInfoProvider serviceInfoProvider, TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, @@ -84,7 +85,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { TbKafkaConsumerStatsService consumerStatsService, TbQueueTransportNotificationSettings transportNotificationSettings, TbKafkaTopicConfigs kafkaTopicConfigs) { - this.partitionService = partitionService; + this.notificationsTopicService = notificationsTopicService; this.kafkaSettings = kafkaSettings; this.serviceInfoProvider = serviceInfoProvider; this.coreSettings = coreSettings; @@ -169,7 +170,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { public TbQueueConsumer> createToCoreNotificationsMsgConsumer() { TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); - consumerBuilder.topic(partitionService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName()); + consumerBuilder.topic(notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName()); consumerBuilder.clientId("tb-core-notifications-consumer-" + serviceInfoProvider.getServiceId()); consumerBuilder.groupId("tb-core-notifications-node-" + serviceInfoProvider.getServiceId()); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java index 2ed638ac35..645cbee906 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java @@ -35,7 +35,7 @@ import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.NotificationsTopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.kafka.TbKafkaAdmin; import org.thingsboard.server.queue.kafka.TbKafkaConsumerStatsService; @@ -57,7 +57,7 @@ import java.util.concurrent.atomic.AtomicLong; @ConditionalOnExpression("'${queue.type:null}'=='kafka' && '${service.type:null}'=='tb-rule-engine'") public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { - private final PartitionService partitionService; + private final NotificationsTopicService notificationsTopicService; private final TbKafkaSettings kafkaSettings; private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueCoreSettings coreSettings; @@ -73,7 +73,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { private final TbQueueAdmin fwUpdatesAdmin; private final AtomicLong consumerCount = new AtomicLong(); - public KafkaTbRuleEngineQueueFactory(PartitionService partitionService, TbKafkaSettings kafkaSettings, + public KafkaTbRuleEngineQueueFactory(NotificationsTopicService notificationsTopicService, TbKafkaSettings kafkaSettings, TbServiceInfoProvider serviceInfoProvider, TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, @@ -81,7 +81,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { TbKafkaConsumerStatsService consumerStatsService, TbQueueTransportNotificationSettings transportNotificationSettings, TbKafkaTopicConfigs kafkaTopicConfigs) { - this.partitionService = partitionService; + this.notificationsTopicService = notificationsTopicService; this.kafkaSettings = kafkaSettings; this.serviceInfoProvider = serviceInfoProvider; this.coreSettings = coreSettings; @@ -175,7 +175,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { public TbQueueConsumer> createToRuleEngineNotificationsMsgConsumer() { TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); - consumerBuilder.topic(partitionService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName()); + consumerBuilder.topic(notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName()); consumerBuilder.clientId("tb-rule-engine-notifications-consumer-" + serviceInfoProvider.getServiceId()); consumerBuilder.groupId("tb-rule-engine-notifications-node-" + serviceInfoProvider.getServiceId()); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java index 3b31531583..3388f0d0f3 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java @@ -38,7 +38,7 @@ import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.NotificationsTopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.pubsub.TbPubSubAdmin; import org.thingsboard.server.queue.pubsub.TbPubSubConsumerTemplate; @@ -64,7 +64,7 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng private final TbQueueRuleEngineSettings ruleEngineSettings; private final TbQueueTransportApiSettings transportApiSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; - private final PartitionService partitionService; + private final NotificationsTopicService notificationsTopicService; private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; @@ -79,7 +79,7 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng TbQueueRuleEngineSettings ruleEngineSettings, TbQueueTransportApiSettings transportApiSettings, TbQueueTransportNotificationSettings transportNotificationSettings, - PartitionService partitionService, + NotificationsTopicService notificationsTopicService, TbServiceInfoProvider serviceInfoProvider, TbPubSubSubscriptionSettings pubSubSubscriptionSettings, TbQueueRemoteJsInvokeSettings jsInvokeSettings) { @@ -88,7 +88,7 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng this.ruleEngineSettings = ruleEngineSettings; this.transportApiSettings = transportApiSettings; this.transportNotificationSettings = transportNotificationSettings; - this.partitionService = partitionService; + this.notificationsTopicService = notificationsTopicService; this.serviceInfoProvider = serviceInfoProvider; this.coreAdmin = new TbPubSubAdmin(pubSubSettings, pubSubSubscriptionSettings.getCoreSettings()); @@ -134,7 +134,7 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng @Override public TbQueueConsumer> createToRuleEngineNotificationsMsgConsumer() { return new TbPubSubConsumerTemplate<>(notificationAdmin, pubSubSettings, - partitionService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName(), + notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); } @@ -147,7 +147,7 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng @Override public TbQueueConsumer> createToCoreNotificationsMsgConsumer() { return new TbPubSubConsumerTemplate<>(notificationAdmin, pubSubSettings, - partitionService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName(), + notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java index 3b1c3c81c9..18a6668581 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbCoreQueueFactory.java @@ -37,7 +37,7 @@ import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.NotificationsTopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.pubsub.TbPubSubAdmin; import org.thingsboard.server.queue.pubsub.TbPubSubConsumerTemplate; @@ -60,7 +60,7 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { private final TbPubSubSettings pubSubSettings; private final TbQueueCoreSettings coreSettings; private final TbQueueTransportApiSettings transportApiSettings; - private final PartitionService partitionService; + private final NotificationsTopicService notificationsTopicService; private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; @@ -75,7 +75,7 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { public PubSubTbCoreQueueFactory(TbPubSubSettings pubSubSettings, TbQueueCoreSettings coreSettings, TbQueueTransportApiSettings transportApiSettings, - PartitionService partitionService, + NotificationsTopicService notificationsTopicService, TbServiceInfoProvider serviceInfoProvider, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbQueueTransportNotificationSettings transportNotificationSettings, @@ -84,7 +84,7 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { this.pubSubSettings = pubSubSettings; this.coreSettings = coreSettings; this.transportApiSettings = transportApiSettings; - this.partitionService = partitionService; + this.notificationsTopicService = notificationsTopicService; this.serviceInfoProvider = serviceInfoProvider; this.jsInvokeSettings = jsInvokeSettings; this.transportNotificationSettings = transportNotificationSettings; @@ -131,7 +131,7 @@ public class PubSubTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueConsumer> createToCoreNotificationsMsgConsumer() { return new TbPubSubConsumerTemplate<>(notificationAdmin, pubSubSettings, - partitionService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName(), + notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java index d5acbeb4c8..8a3b88846f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java @@ -35,7 +35,7 @@ import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.NotificationsTopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.pubsub.TbPubSubAdmin; import org.thingsboard.server.queue.pubsub.TbPubSubConsumerTemplate; @@ -58,7 +58,7 @@ public class PubSubTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory private final TbPubSubSettings pubSubSettings; private final TbQueueCoreSettings coreSettings; private final TbQueueRuleEngineSettings ruleEngineSettings; - private final PartitionService partitionService; + private final NotificationsTopicService notificationsTopicService; private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; @@ -71,7 +71,7 @@ public class PubSubTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory public PubSubTbRuleEngineQueueFactory(TbPubSubSettings pubSubSettings, TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, - PartitionService partitionService, + NotificationsTopicService notificationsTopicService, TbServiceInfoProvider serviceInfoProvider, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbQueueTransportNotificationSettings transportNotificationSettings, @@ -79,7 +79,7 @@ public class PubSubTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory this.pubSubSettings = pubSubSettings; this.coreSettings = coreSettings; this.ruleEngineSettings = ruleEngineSettings; - this.partitionService = partitionService; + this.notificationsTopicService = notificationsTopicService; this.serviceInfoProvider = serviceInfoProvider; this.jsInvokeSettings = jsInvokeSettings; this.transportNotificationSettings = transportNotificationSettings; @@ -124,7 +124,7 @@ public class PubSubTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory @Override public TbQueueConsumer> createToRuleEngineNotificationsMsgConsumer() { return new TbPubSubConsumerTemplate<>(notificationAdmin, pubSubSettings, - partitionService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName(), + notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java index 80ae1d24a9..a22f6811e3 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java @@ -38,7 +38,7 @@ import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.NotificationsTopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.rabbitmq.TbRabbitMqAdmin; import org.thingsboard.server.queue.rabbitmq.TbRabbitMqConsumerTemplate; @@ -59,7 +59,7 @@ import java.nio.charset.StandardCharsets; @ConditionalOnExpression("'${queue.type:null}'=='rabbitmq' && '${service.type:null}'=='monolith'") public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngineQueueFactory { - private final PartitionService partitionService; + private final NotificationsTopicService notificationsTopicService; private final TbQueueCoreSettings coreSettings; private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueRuleEngineSettings ruleEngineSettings; @@ -74,7 +74,7 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE private final TbQueueAdmin transportApiAdmin; private final TbQueueAdmin notificationAdmin; - public RabbitMqMonolithQueueFactory(PartitionService partitionService, TbQueueCoreSettings coreSettings, + public RabbitMqMonolithQueueFactory(NotificationsTopicService notificationsTopicService, TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, TbServiceInfoProvider serviceInfoProvider, TbQueueTransportApiSettings transportApiSettings, @@ -82,7 +82,7 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE TbRabbitMqSettings rabbitMqSettings, TbRabbitMqQueueArguments queueArguments, TbQueueRemoteJsInvokeSettings jsInvokeSettings) { - this.partitionService = partitionService; + this.notificationsTopicService = notificationsTopicService; this.coreSettings = coreSettings; this.serviceInfoProvider = serviceInfoProvider; this.ruleEngineSettings = ruleEngineSettings; @@ -132,7 +132,7 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE @Override public TbQueueConsumer> createToRuleEngineNotificationsMsgConsumer() { return new TbRabbitMqConsumerTemplate<>(notificationAdmin, rabbitMqSettings, - partitionService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName(), + notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); } @@ -145,7 +145,7 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE @Override public TbQueueConsumer> createToCoreNotificationsMsgConsumer() { return new TbRabbitMqConsumerTemplate<>(notificationAdmin, rabbitMqSettings, - partitionService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName(), + notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java index e2abcde93c..e728be6085 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbCoreQueueFactory.java @@ -37,7 +37,7 @@ import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.NotificationsTopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.rabbitmq.TbRabbitMqAdmin; import org.thingsboard.server.queue.rabbitmq.TbRabbitMqConsumerTemplate; @@ -61,7 +61,7 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { private final TbQueueRuleEngineSettings ruleEngineSettings; private final TbQueueCoreSettings coreSettings; private final TbQueueTransportApiSettings transportApiSettings; - private final PartitionService partitionService; + private final NotificationsTopicService notificationsTopicService; private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; @@ -76,7 +76,7 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { TbQueueCoreSettings coreSettings, TbQueueTransportApiSettings transportApiSettings, TbQueueRuleEngineSettings ruleEngineSettings, - PartitionService partitionService, + NotificationsTopicService notificationsTopicService, TbServiceInfoProvider serviceInfoProvider, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbQueueTransportNotificationSettings transportNotificationSettings, @@ -85,7 +85,7 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { this.coreSettings = coreSettings; this.transportApiSettings = transportApiSettings; this.ruleEngineSettings = ruleEngineSettings; - this.partitionService = partitionService; + this.notificationsTopicService = notificationsTopicService; this.serviceInfoProvider = serviceInfoProvider; this.jsInvokeSettings = jsInvokeSettings; this.transportNotificationSettings = transportNotificationSettings; @@ -131,7 +131,7 @@ public class RabbitMqTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueConsumer> createToCoreNotificationsMsgConsumer() { return new TbRabbitMqConsumerTemplate<>(notificationAdmin, rabbitMqSettings, - partitionService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName(), + notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java index c17ce6a058..21e152787d 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java @@ -35,7 +35,7 @@ import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.NotificationsTopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.rabbitmq.TbRabbitMqAdmin; import org.thingsboard.server.queue.rabbitmq.TbRabbitMqConsumerTemplate; @@ -55,7 +55,7 @@ import java.nio.charset.StandardCharsets; @ConditionalOnExpression("'${queue.type:null}'=='rabbitmq' && '${service.type:null}'=='tb-rule-engine'") public class RabbitMqTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { - private final PartitionService partitionService; + private final NotificationsTopicService notificationsTopicService; private final TbQueueCoreSettings coreSettings; private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueRuleEngineSettings ruleEngineSettings; @@ -68,14 +68,14 @@ public class RabbitMqTbRuleEngineQueueFactory implements TbRuleEngineQueueFactor private final TbQueueAdmin jsExecutorAdmin; private final TbQueueAdmin notificationAdmin; - public RabbitMqTbRuleEngineQueueFactory(PartitionService partitionService, TbQueueCoreSettings coreSettings, + public RabbitMqTbRuleEngineQueueFactory(NotificationsTopicService notificationsTopicService, TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, TbServiceInfoProvider serviceInfoProvider, TbRabbitMqSettings rabbitMqSettings, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbQueueTransportNotificationSettings transportNotificationSettings, TbRabbitMqQueueArguments queueArguments) { - this.partitionService = partitionService; + this.notificationsTopicService = notificationsTopicService; this.coreSettings = coreSettings; this.serviceInfoProvider = serviceInfoProvider; this.ruleEngineSettings = ruleEngineSettings; @@ -123,7 +123,7 @@ public class RabbitMqTbRuleEngineQueueFactory implements TbRuleEngineQueueFactor @Override public TbQueueConsumer> createToRuleEngineNotificationsMsgConsumer() { return new TbRabbitMqConsumerTemplate<>(notificationAdmin, rabbitMqSettings, - partitionService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName(), + notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java index 86dafd18d3..da219d9084 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java @@ -42,7 +42,7 @@ import org.thingsboard.server.queue.azure.servicebus.TbServiceBusSettings; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.NotificationsTopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; @@ -58,7 +58,7 @@ import java.nio.charset.StandardCharsets; @ConditionalOnExpression("'${queue.type:null}'=='service-bus' && '${service.type:null}'=='monolith'") public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngineQueueFactory { - private final PartitionService partitionService; + private final NotificationsTopicService notificationsTopicService; private final TbQueueCoreSettings coreSettings; private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueRuleEngineSettings ruleEngineSettings; @@ -73,7 +73,7 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul private final TbQueueAdmin transportApiAdmin; private final TbQueueAdmin notificationAdmin; - public ServiceBusMonolithQueueFactory(PartitionService partitionService, TbQueueCoreSettings coreSettings, + public ServiceBusMonolithQueueFactory(NotificationsTopicService notificationsTopicService, TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, TbServiceInfoProvider serviceInfoProvider, TbQueueTransportApiSettings transportApiSettings, @@ -81,7 +81,7 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul TbServiceBusSettings serviceBusSettings, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbServiceBusQueueConfigs serviceBusQueueConfigs) { - this.partitionService = partitionService; + this.notificationsTopicService = notificationsTopicService; this.coreSettings = coreSettings; this.serviceInfoProvider = serviceInfoProvider; this.ruleEngineSettings = ruleEngineSettings; @@ -131,7 +131,7 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul @Override public TbQueueConsumer> createToRuleEngineNotificationsMsgConsumer() { return new TbServiceBusConsumerTemplate<>(notificationAdmin, serviceBusSettings, - partitionService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName(), + notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); } @@ -144,7 +144,7 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul @Override public TbQueueConsumer> createToCoreNotificationsMsgConsumer() { return new TbServiceBusConsumerTemplate<>(notificationAdmin, serviceBusSettings, - partitionService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName(), + notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java index 815f4c8b38..e1eb41b2b2 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbCoreQueueFactory.java @@ -42,7 +42,7 @@ import org.thingsboard.server.queue.azure.servicebus.TbServiceBusSettings; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.NotificationsTopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; @@ -61,7 +61,7 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { private final TbQueueRuleEngineSettings ruleEngineSettings; private final TbQueueCoreSettings coreSettings; private final TbQueueTransportApiSettings transportApiSettings; - private final PartitionService partitionService; + private final NotificationsTopicService notificationsTopicService; private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; @@ -76,7 +76,7 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { TbQueueCoreSettings coreSettings, TbQueueTransportApiSettings transportApiSettings, TbQueueRuleEngineSettings ruleEngineSettings, - PartitionService partitionService, + NotificationsTopicService notificationsTopicService, TbServiceInfoProvider serviceInfoProvider, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbQueueTransportNotificationSettings transportNotificationSettings, @@ -85,7 +85,7 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { this.coreSettings = coreSettings; this.transportApiSettings = transportApiSettings; this.ruleEngineSettings = ruleEngineSettings; - this.partitionService = partitionService; + this.notificationsTopicService = notificationsTopicService; this.serviceInfoProvider = serviceInfoProvider; this.jsInvokeSettings = jsInvokeSettings; this.transportNotificationSettings = transportNotificationSettings; @@ -131,7 +131,7 @@ public class ServiceBusTbCoreQueueFactory implements TbCoreQueueFactory { @Override public TbQueueConsumer> createToCoreNotificationsMsgConsumer() { return new TbServiceBusConsumerTemplate<>(notificationAdmin, serviceBusSettings, - partitionService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName(), + notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java index daa6db1a6e..a6ef991f57 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java @@ -40,7 +40,7 @@ import org.thingsboard.server.queue.azure.servicebus.TbServiceBusSettings; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.NotificationsTopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.settings.TbQueueCoreSettings; import org.thingsboard.server.queue.settings.TbQueueRemoteJsInvokeSettings; @@ -55,7 +55,7 @@ import java.nio.charset.StandardCharsets; @ConditionalOnExpression("'${queue.type:null}'=='service-bus' && '${service.type:null}'=='tb-rule-engine'") public class ServiceBusTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { - private final PartitionService partitionService; + private final NotificationsTopicService notificationsTopicService; private final TbQueueCoreSettings coreSettings; private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueRuleEngineSettings ruleEngineSettings; @@ -68,14 +68,14 @@ public class ServiceBusTbRuleEngineQueueFactory implements TbRuleEngineQueueFact private final TbQueueAdmin jsExecutorAdmin; private final TbQueueAdmin notificationAdmin; - public ServiceBusTbRuleEngineQueueFactory(PartitionService partitionService, TbQueueCoreSettings coreSettings, + public ServiceBusTbRuleEngineQueueFactory(NotificationsTopicService notificationsTopicService, TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, TbServiceInfoProvider serviceInfoProvider, TbServiceBusSettings serviceBusSettings, TbQueueRemoteJsInvokeSettings jsInvokeSettings, TbQueueTransportNotificationSettings transportNotificationSettings, TbServiceBusQueueConfigs serviceBusQueueConfigs) { - this.partitionService = partitionService; + this.notificationsTopicService = notificationsTopicService; this.coreSettings = coreSettings; this.serviceInfoProvider = serviceInfoProvider; this.ruleEngineSettings = ruleEngineSettings; @@ -123,7 +123,7 @@ public class ServiceBusTbRuleEngineQueueFactory implements TbRuleEngineQueueFact @Override public TbQueueConsumer> createToRuleEngineNotificationsMsgConsumer() { return new TbServiceBusConsumerTemplate<>(notificationAdmin, serviceBusSettings, - partitionService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName(), + notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index 1ca7d80394..77738a1e9d 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -85,6 +85,7 @@ import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.AsyncCallbackTemplate; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.discovery.NotificationsTopicService; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; @@ -157,6 +158,7 @@ public class DefaultTransportService implements TransportService { private final TbTransportQueueFactory queueProvider; private final TbQueueProducerProvider producerProvider; private final PartitionService partitionService; + private final NotificationsTopicService notificationsTopicService; private final TbServiceInfoProvider serviceInfoProvider; private final StatsFactory statsFactory; private final TransportDeviceProfileCache deviceProfileCache; @@ -190,6 +192,7 @@ public class DefaultTransportService implements TransportService { TbTransportQueueFactory queueProvider, TbQueueProducerProvider producerProvider, PartitionService partitionService, + NotificationsTopicService notificationsTopicService, StatsFactory statsFactory, TransportDeviceProfileCache deviceProfileCache, TransportTenantProfileCache tenantProfileCache, @@ -200,6 +203,7 @@ public class DefaultTransportService implements TransportService { this.queueProvider = queueProvider; this.producerProvider = producerProvider; this.partitionService = partitionService; + this.notificationsTopicService = notificationsTopicService; this.statsFactory = statsFactory; this.deviceProfileCache = deviceProfileCache; this.tenantProfileCache = tenantProfileCache; @@ -224,7 +228,7 @@ public class DefaultTransportService implements TransportService { ruleEngineMsgProducer = producerProvider.getRuleEngineMsgProducer(); tbCoreMsgProducer = producerProvider.getTbCoreMsgProducer(); transportNotificationsConsumer = queueProvider.createTransportNotificationsConsumer(); - TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_TRANSPORT, serviceInfoProvider.getServiceId()); + TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, serviceInfoProvider.getServiceId()); transportNotificationsConsumer.subscribe(Collections.singleton(tpi)); transportApiRequestTemplate.init(); mainConsumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("transport-consumer")); diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java index 75978ea371..7049d8ae1c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java @@ -39,6 +39,7 @@ import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.queue.TbQueueAdmin; +import org.thingsboard.server.queue.TbQueueClusterService; import java.util.List; @@ -56,8 +57,8 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ @Autowired(required = false) private TbQueueAdmin tbQueueAdmin; -// @Autowired(required = false) -// private TbQueueClusterService queueClusterService; + @Autowired(required = false) + private TbQueueClusterService queueClusterService; // @Autowired // private QueueStatsService queueStatsService; @@ -99,7 +100,7 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ //TODO: remove if partitions can't be deleted. if (currentPartitions != oldPartitions && tbQueueAdmin != null) { -// queueClusterService.onQueueDelete(queue, null); + queueClusterService.onQueueDelete(queue, null); if (currentPartitions > oldPartitions) { log.info("Added [{}] new partitions to [{}] queue", currentPartitions - oldPartitions, queue.getName()); for (int i = oldPartitions; i < currentPartitions; i++) { @@ -121,9 +122,9 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ public void deleteQueue(TenantId tenantId, QueueId queueId) { log.trace("Executing deleteQueue, queueId: [{}]", queueId); Queue queue = findQueueById(tenantId, queueId); -// if (queueClusterService != null) { -// queueClusterService.onQueueDelete(queue, null); -// } + if (queueClusterService != null) { + queueClusterService.onQueueDelete(queue, null); + } // queueStatsService.deleteQueueStatsByQueueId(tenantId, queueId); boolean result = queueDao.removeById(tenantId, queueId.getId()); if (result && tbQueueAdmin != null) { From b58be41083444b68afd45c86a17e60100ad06999 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Wed, 30 Mar 2022 10:44:21 +0300 Subject: [PATCH 028/276] Tests for JwtTokenFactory --- .../security/auth/JwtTokenFactoryTest.java | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java new file mode 100644 index 0000000000..f865c9b5e0 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java @@ -0,0 +1,165 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.security.auth; + +import io.jsonwebtoken.Claims; +import org.junit.BeforeClass; +import org.junit.Test; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.security.model.JwtToken; +import org.thingsboard.server.config.JwtSettings; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.model.UserPrincipal; +import org.thingsboard.server.service.security.model.token.AccessJwtToken; +import org.thingsboard.server.service.security.model.token.JwtTokenFactory; +import org.thingsboard.server.service.security.model.token.RawAccessJwtToken; + +import java.util.Calendar; +import java.util.Date; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +public class JwtTokenFactoryTest { + + private static JwtTokenFactory tokenFactory; + private static JwtSettings jwtSettings; + + @BeforeClass + public static void beforeAll() { + jwtSettings = new JwtSettings(); + jwtSettings.setTokenIssuer("tb"); + jwtSettings.setTokenSigningKey("abewafaf"); + jwtSettings.setTokenExpirationTime((int) TimeUnit.HOURS.toSeconds(2)); + jwtSettings.setRefreshTokenExpTime((int) TimeUnit.DAYS.toSeconds(7)); + + tokenFactory = new JwtTokenFactory(jwtSettings); + } + + @Test + public void testCreateAndParseAccessJwtToken() { + SecurityUser securityUser = new SecurityUser(); + securityUser.setId(new UserId(UUID.randomUUID())); + securityUser.setEmail("tenant@thingsboard.org"); + securityUser.setAuthority(Authority.TENANT_ADMIN); + securityUser.setTenantId(new TenantId(UUID.randomUUID())); + securityUser.setEnabled(true); + securityUser.setFirstName("A"); + securityUser.setLastName("B"); + securityUser.setUserPrincipal(new UserPrincipal(UserPrincipal.Type.USER_NAME, securityUser.getEmail())); + securityUser.setCustomerId(new CustomerId(UUID.randomUUID())); + + testCreateAndParseAccessJwtToken(securityUser); + + securityUser = new SecurityUser(securityUser, true, new UserPrincipal(UserPrincipal.Type.PUBLIC_ID, securityUser.getEmail())); + securityUser.setFirstName(null); + securityUser.setLastName(null); + securityUser.setCustomerId(null); + + testCreateAndParseAccessJwtToken(securityUser); + } + + public void testCreateAndParseAccessJwtToken(SecurityUser securityUser) { + AccessJwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); + checkExpirationTime(accessToken, jwtSettings.getTokenExpirationTime()); + + SecurityUser parsedSecurityUser = tokenFactory.parseAccessJwtToken(new RawAccessJwtToken(accessToken.getToken())); + assertThat(parsedSecurityUser.getId()).isEqualTo(securityUser.getId()); + assertThat(parsedSecurityUser.getEmail()).isEqualTo(securityUser.getEmail()); + assertThat(parsedSecurityUser.getUserPrincipal()).matches(userPrincipal -> { + return userPrincipal.getType().equals(securityUser.getUserPrincipal().getType()) + && userPrincipal.getValue().equals(securityUser.getUserPrincipal().getValue()); + }); + assertThat(parsedSecurityUser.getAuthorities()).isEqualTo(securityUser.getAuthorities()); + assertThat(parsedSecurityUser.isEnabled()).isEqualTo(securityUser.isEnabled()); + assertThat(parsedSecurityUser.getTenantId()).isEqualTo(securityUser.getTenantId()); + assertThat(parsedSecurityUser.getCustomerId()).isEqualTo(securityUser.getCustomerId()); + assertThat(parsedSecurityUser.getFirstName()).isEqualTo(securityUser.getFirstName()); + assertThat(parsedSecurityUser.getLastName()).isEqualTo(securityUser.getLastName()); + } + + @Test + public void testCreateAndParseRefreshJwtToken() { + SecurityUser securityUser = new SecurityUser(); + securityUser.setId(new UserId(UUID.randomUUID())); + securityUser.setEmail("tenant@thingsboard.org"); + securityUser.setAuthority(Authority.TENANT_ADMIN); + securityUser.setUserPrincipal(new UserPrincipal(UserPrincipal.Type.USER_NAME, securityUser.getEmail())); + securityUser.setEnabled(true); + securityUser.setTenantId(new TenantId(UUID.randomUUID())); + securityUser.setCustomerId(new CustomerId(UUID.randomUUID())); + + JwtToken refreshToken = tokenFactory.createRefreshToken(securityUser); + checkExpirationTime(refreshToken, jwtSettings.getRefreshTokenExpTime()); + + SecurityUser parsedSecurityUser = tokenFactory.parseRefreshToken(new RawAccessJwtToken(refreshToken.getToken())); + assertThat(parsedSecurityUser.getId()).isEqualTo(securityUser.getId()); + assertThat(parsedSecurityUser.getUserPrincipal()).matches(userPrincipal -> { + return userPrincipal.getType().equals(securityUser.getUserPrincipal().getType()) + && userPrincipal.getValue().equals(securityUser.getUserPrincipal().getValue()); + }); + assertThat(parsedSecurityUser.getAuthority()).isNull(); + } + + @Test + public void testCreateAndParsePreVerificationJwtToken() { + SecurityUser securityUser = new SecurityUser(); + securityUser.setId(new UserId(UUID.randomUUID())); + securityUser.setEmail("tenant@thingsboard.org"); + securityUser.setAuthority(Authority.TENANT_ADMIN); + securityUser.setUserPrincipal(new UserPrincipal(UserPrincipal.Type.USER_NAME, securityUser.getEmail())); + securityUser.setEnabled(true); + securityUser.setTenantId(new TenantId(UUID.randomUUID())); + securityUser.setCustomerId(new CustomerId(UUID.randomUUID())); + + int tokenLifetime = (int) TimeUnit.MINUTES.toSeconds(30); + JwtToken preVerificationToken = tokenFactory.createPreVerificationToken(securityUser, tokenLifetime); + checkExpirationTime(preVerificationToken, tokenLifetime); + + SecurityUser parsedSecurityUser = tokenFactory.parseAccessJwtToken(new RawAccessJwtToken(preVerificationToken.getToken())); + assertThat(parsedSecurityUser.getId()).isEqualTo(securityUser.getId()); + assertThat(parsedSecurityUser.getAuthority()).isEqualTo(Authority.PRE_VERIFICATION_TOKEN); + assertThat(parsedSecurityUser.getTenantId()).isEqualTo(securityUser.getTenantId()); + assertThat(parsedSecurityUser.getCustomerId()).isEqualTo(securityUser.getCustomerId()); + assertThat(parsedSecurityUser.getUserPrincipal()).matches(userPrincipal -> { + return userPrincipal.getType() == UserPrincipal.Type.USER_NAME + && userPrincipal.getValue().equals(securityUser.getUserPrincipal().getValue()); + }); + } + + private void checkExpirationTime(JwtToken jwtToken, int tokenLifetime) { + Claims claims = tokenFactory.parseTokenClaims(jwtToken).getBody(); + assertThat(claims.getExpiration()).matches(actualExpirationTime -> { + Calendar expirationTime = Calendar.getInstance(); + expirationTime.setTime(new Date()); + expirationTime.add(Calendar.SECOND, tokenLifetime); + if (actualExpirationTime.equals(expirationTime.getTime())) { + return true; + } else if (actualExpirationTime.before(expirationTime.getTime())) { + int gap = 2; + expirationTime.add(Calendar.SECOND, -gap); + return actualExpirationTime.after(expirationTime.getTime()); + } else { + return false; + } + }); + } + +} From de16eca0a5fa7a401b39886ecd31f93de2f80ada Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Wed, 30 Mar 2022 10:48:56 +0300 Subject: [PATCH 029/276] Fix RateLimitsTest --- .../server/common/msg/tools/RateLimitsTest.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/common/message/src/test/java/org/thingsboard/server/common/msg/tools/RateLimitsTest.java b/common/message/src/test/java/org/thingsboard/server/common/msg/tools/RateLimitsTest.java index d4cc408558..3878d64ec9 100644 --- a/common/message/src/test/java/org/thingsboard/server/common/msg/tools/RateLimitsTest.java +++ b/common/message/src/test/java/org/thingsboard/server/common/msg/tools/RateLimitsTest.java @@ -26,11 +26,9 @@ public class RateLimitsTest { @Test public void testRateLimits_greedyRefill() { - for (int period = 1; period <= 5; period++) { - for (int capacity = 1; capacity <= 5; capacity++) { - testRateLimitWithGreedyRefill(capacity, period); - } - } + testRateLimitWithGreedyRefill(3, 10); + testRateLimitWithGreedyRefill(3, 3); + testRateLimitWithGreedyRefill(4, 2); } private void testRateLimitWithGreedyRefill(int capacity, int period) { @@ -56,11 +54,9 @@ public class RateLimitsTest { @Test public void testRateLimits_intervalRefill() { - for (int period = 1; period <= 3; period++) { - for (int capacity = 1; capacity <= 3; capacity++) { - testRateLimitWithIntervalRefill(capacity, period); - } - } + testRateLimitWithIntervalRefill(10, 5); + testRateLimitWithIntervalRefill(3, 3); + testRateLimitWithIntervalRefill(4, 2); } private void testRateLimitWithIntervalRefill(int capacity, int period) { From 8eb68ff7a6bd154985eac32a9e60418a061b3245 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Wed, 30 Mar 2022 10:50:57 +0300 Subject: [PATCH 030/276] Revert changes to UserCredentials --- .../server/common/data/security/UserCredentials.java | 9 ++------- .../server/dao/model/sql/UserCredentialsEntity.java | 11 ----------- dao/src/main/resources/sql/schema-entities.sql | 3 +-- 3 files changed, 3 insertions(+), 20 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java index 9f8dab575b..3816d27131 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java @@ -16,12 +16,12 @@ package org.thingsboard.server.common.data.security; import lombok.EqualsAndHashCode; -import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; +import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.id.UserCredentialsId; import org.thingsboard.server.common.data.id.UserId; @EqualsAndHashCode(callSuper = true) -public class UserCredentials extends SearchTextBasedWithAdditionalInfo { +public class UserCredentials extends BaseData { private static final long serialVersionUID = -2108436378880529163L; @@ -87,11 +87,6 @@ public class UserCredentials extends SearchTextBasedWithAdditionalInfo implements BaseEntity { @@ -55,10 +50,6 @@ public final class UserCredentialsEntity extends BaseSqlEntity @Column(name = ModelConstants.USER_CREDENTIALS_RESET_TOKEN_PROPERTY, unique = true) private String resetToken; - @Type(type = "json") - @Column(name = ModelConstants.ADDITIONAL_INFO_PROPERTY) - private JsonNode additionalInfo; - public UserCredentialsEntity() { super(); } @@ -75,7 +66,6 @@ public final class UserCredentialsEntity extends BaseSqlEntity this.password = userCredentials.getPassword(); this.activateToken = userCredentials.getActivateToken(); this.resetToken = userCredentials.getResetToken(); - this.additionalInfo = userCredentials.getAdditionalInfo(); } @Override @@ -89,7 +79,6 @@ public final class UserCredentialsEntity extends BaseSqlEntity userCredentials.setPassword(password); userCredentials.setActivateToken(activateToken); userCredentials.setResetToken(resetToken); - userCredentials.setAdditionalInfo(additionalInfo); return userCredentials; } diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index da4bcc5bec..8ee0854ae9 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -370,8 +370,7 @@ CREATE TABLE IF NOT EXISTS user_credentials ( enabled boolean, password varchar(255), reset_token varchar(255) UNIQUE, - user_id uuid UNIQUE, - additional_info varchar + user_id uuid UNIQUE ); CREATE TABLE IF NOT EXISTS widget_type ( From 061ffcba53ceca03ca27b293cc6290808d04e49a Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 31 Mar 2022 18:24:28 +0200 Subject: [PATCH 031/276] drop queue table in tests --- dao/src/test/resources/sql/hsql/drop-all-tables.sql | 1 + dao/src/test/resources/sql/psql/drop-all-tables.sql | 1 + dao/src/test/resources/sql/timescale/drop-all-tables.sql | 1 + 3 files changed, 3 insertions(+) diff --git a/dao/src/test/resources/sql/hsql/drop-all-tables.sql b/dao/src/test/resources/sql/hsql/drop-all-tables.sql index 095a38533e..3ad3dac3b3 100644 --- a/dao/src/test/resources/sql/hsql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/hsql/drop-all-tables.sql @@ -38,4 +38,5 @@ DROP TABLE IF EXISTS ota_package; DROP TABLE IF EXISTS edge; DROP TABLE IF EXISTS edge_event; DROP TABLE IF EXISTS rpc; +DROP TABLE IF EXISTS queue; DROP FUNCTION IF EXISTS to_uuid; diff --git a/dao/src/test/resources/sql/psql/drop-all-tables.sql b/dao/src/test/resources/sql/psql/drop-all-tables.sql index 3d1d38a0e7..3a6d92a903 100644 --- a/dao/src/test/resources/sql/psql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/psql/drop-all-tables.sql @@ -39,3 +39,4 @@ DROP TABLE IF EXISTS firmware; DROP TABLE IF EXISTS edge; DROP TABLE IF EXISTS edge_event; DROP TABLE IF EXISTS rpc; +DROP TABLE IF EXISTS queue; \ No newline at end of file diff --git a/dao/src/test/resources/sql/timescale/drop-all-tables.sql b/dao/src/test/resources/sql/timescale/drop-all-tables.sql index f113ec5277..735192374d 100644 --- a/dao/src/test/resources/sql/timescale/drop-all-tables.sql +++ b/dao/src/test/resources/sql/timescale/drop-all-tables.sql @@ -34,3 +34,4 @@ DROP TABLE IF EXISTS oauth2_client_registration_template; DROP TABLE IF EXISTS api_usage_state; DROP TABLE IF EXISTS resource; DROP TABLE IF EXISTS firmware; +DROP TABLE IF EXISTS queue; From 4f0f95912ea843319a2b4d9c558db192e4b35add Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 31 Mar 2022 19:42:57 +0200 Subject: [PATCH 032/276] BaseQueueServiceTest is abstract --- .../thingsboard/server/dao/service/BaseQueueServiceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseQueueServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseQueueServiceTest.java index 29490dea04..173f596287 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseQueueServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseQueueServiceTest.java @@ -38,7 +38,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -public class BaseQueueServiceTest extends AbstractServiceTest { +public abstract class BaseQueueServiceTest extends AbstractServiceTest { private IdComparator idComparator = new IdComparator<>(); From b86a1546e8a9e77e86f41e166324a0f1fd63dbeb Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Fri, 1 Apr 2022 13:33:11 +0200 Subject: [PATCH 033/276] created queue routing service --- .../queue/DefaultQueueRoutingInfoService.java | 54 ++++++++++++++++ .../transport/DefaultTransportApiService.java | 36 +++++++++++ common/cluster-api/src/main/proto/queue.proto | 37 +++++++++++ .../queue/discovery/HashPartitionService.java | 2 - .../queue/discovery/QueueRoutingInfo.java | 54 ++++++++++++++++ .../discovery/QueueRoutingInfoService.java | 29 +++++++++ .../common/transport/TransportService.java | 7 ++ .../service/DefaultTransportService.java | 36 +++++++++++ .../TransportQueueRoutingInfoService.java | 64 +++++++++++++++++++ 9 files changed, 317 insertions(+), 2 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/queue/DefaultQueueRoutingInfoService.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueRoutingInfo.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueRoutingInfoService.java create mode 100644 common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportQueueRoutingInfoService.java diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultQueueRoutingInfoService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultQueueRoutingInfoService.java new file mode 100644 index 0000000000..df22d42e62 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultQueueRoutingInfoService.java @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.queue; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.queue.QueueService; +import org.thingsboard.server.queue.discovery.QueueRoutingInfo; +import org.thingsboard.server.queue.discovery.QueueRoutingInfoService; + +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Service +@ConditionalOnExpression("'${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core' || '${service.type:null}'=='tb-rule-engine'") +public class DefaultQueueRoutingInfoService implements QueueRoutingInfoService { + + private final QueueService queueService; + + public DefaultQueueRoutingInfoService(QueueService queueService) { + this.queueService = queueService; + } + + @Override + public List getAllQueuesRoutingInfo() { + return queueService.findAllQueues().stream().map(QueueRoutingInfo::new).collect(Collectors.toList()); + } + + @Override + public List getMainQueuesRoutingInfo() { + return queueService.findAllMainQueues().stream().map(QueueRoutingInfo::new).collect(Collectors.toList()); + } + + @Override + public List getQueuesRoutingInfo(TenantId tenantId) { + return queueService.findQueuesByTenantId(tenantId).stream().map(QueueRoutingInfo::new).collect(Collectors.toList()); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 5fda933234..0cba1ec325 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -57,6 +57,7 @@ import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.ota.OtaPackageUtil; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; @@ -72,6 +73,7 @@ import org.thingsboard.server.dao.device.provision.ProvisionFailedException; import org.thingsboard.server.dao.device.provision.ProvisionRequest; import org.thingsboard.server.dao.device.provision.ProvisionResponse; import org.thingsboard.server.dao.ota.OtaPackageService; +import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.gen.transport.TransportProtos; @@ -99,6 +101,7 @@ import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.resource.TbResourceService; +import java.util.List; import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -134,6 +137,7 @@ public class DefaultTransportApiService implements TransportApiService { private final TbResourceService resourceService; private final OtaPackageService otaPackageService; private final OtaPackageDataCache otaPackageDataCache; + private final QueueService queueService; private final ConcurrentMap deviceCreationLocks = new ConcurrentHashMap<>(); @@ -176,6 +180,12 @@ public class DefaultTransportApiService implements TransportApiService { result = handle(transportApiRequestMsg.getDeviceCredentialsRequestMsg()); } else if (transportApiRequestMsg.hasOtaPackageRequestMsg()) { result = handle(transportApiRequestMsg.getOtaPackageRequestMsg()); + } else if (transportApiRequestMsg.hasGetAllMainQueueRoutingInfoRequestMsg()) { + return Futures.transform(handle(transportApiRequestMsg.getGetAllMainQueueRoutingInfoRequestMsg()), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); + } else if (transportApiRequestMsg.hasGetTenantQueueRoutingInfoRequestMsg()) { + return Futures.transform(handle(transportApiRequestMsg.getGetTenantQueueRoutingInfoRequestMsg()), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); + } else if (transportApiRequestMsg.hasGetAllQueueRoutingInfoRequestMsg()) { + return Futures.transform(handle(transportApiRequestMsg.getGetAllQueueRoutingInfoRequestMsg()), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); } return Futures.transform(Optional.ofNullable(result).orElseGet(this::getEmptyTransportApiResponseFuture), @@ -636,6 +646,32 @@ public class DefaultTransportApiService implements TransportApiService { } } + private ListenableFuture handle(TransportProtos.GetAllMainQueueRoutingInfoRequestMsg requestMsg) { + return queuesToTransportApiResponseMsg(queueService.findAllMainQueues()); + } + + private ListenableFuture handle(TransportProtos.GetAllQueueRoutingInfoRequestMsg requestMsg) { + return queuesToTransportApiResponseMsg(queueService.findAllQueues()); + } + + private ListenableFuture handle(TransportProtos.GetTenantQueueRoutingInfoRequestMsg requestMsg) { + TenantId tenantId = new TenantId(new UUID(requestMsg.getTenantIdMSB(), requestMsg.getTenantIdLSB())); + return queuesToTransportApiResponseMsg(queueService.findQueuesByTenantId(tenantId)); + } + + private ListenableFuture queuesToTransportApiResponseMsg(List queues) { + return Futures.immediateFuture(TransportApiResponseMsg.newBuilder() + .addAllGetQueueRoutingInfoResponseMsgs(queues.stream() + .map(queue -> TransportProtos.GetQueueRoutingInfoResponseMsg.newBuilder() + .setTenantIdMSB(queue.getTenantId().getId().getMostSignificantBits()) + .setTenantIdLSB(queue.getTenantId().getId().getLeastSignificantBits()) + .setQueueName(queue.getName()) + .setQueueTopic(queue.getTopic()) + .setPartitions(queue.getPartitions()) + .build()).collect(Collectors.toList())).build()); + } + + private Long checkLong(Long l) { return l != null ? l : 0; } diff --git a/common/cluster-api/src/main/proto/queue.proto b/common/cluster-api/src/main/proto/queue.proto index a9d517f4dd..1084cb2e44 100644 --- a/common/cluster-api/src/main/proto/queue.proto +++ b/common/cluster-api/src/main/proto/queue.proto @@ -197,6 +197,39 @@ message GetEntityProfileRequestMsg { int64 entityIdLSB = 3; } +message GetAllMainQueueRoutingInfoRequestMsg { +} + +message GetAllQueueRoutingInfoRequestMsg { +} + +message GetTenantQueueRoutingInfoRequestMsg { + int64 tenantIdMSB = 1; + int64 tenantIdLSB = 2; +} + +message GetQueueRoutingInfoResponseMsg { + int64 tenantIdMSB = 1; + int64 tenantIdLSB = 2; + string queueName = 3; + string queueTopic = 4; + int32 partitions = 5; +} + +message QueueUpdateMsg { + int64 tenantIdMSB = 1; + int64 tenantIdLSB = 2; + string queueName = 3; + string queueTopic = 4; + int32 partitions = 5; +} + +message QueueDeleteMsg { + int64 tenantIdMSB = 1; + int64 tenantIdLSB = 2; + string queueName = 3; +} + message LwM2MRegistrationRequestMsg { string tenantId = 1; string endpoint = 2; @@ -673,6 +706,9 @@ message TransportApiRequestMsg { GetSnmpDevicesRequestMsg snmpDevicesRequestMsg = 11; GetDeviceRequestMsg deviceRequestMsg = 12; GetDeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 13; + GetAllMainQueueRoutingInfoRequestMsg GetAllMainQueueRoutingInfoRequestMsg = 14; + GetTenantQueueRoutingInfoRequestMsg getTenantQueueRoutingInfoRequestMsg = 15; + GetAllQueueRoutingInfoRequestMsg getAllQueueRoutingInfoRequestMsg = 16; } /* Response from ThingsBoard Core Service to Transport Service */ @@ -687,6 +723,7 @@ message TransportApiResponseMsg { GetOtaPackageResponseMsg otaPackageResponseMsg = 8; GetDeviceResponseMsg deviceResponseMsg = 9; GetDeviceCredentialsResponseMsg deviceCredentialsResponseMsg = 10; + repeated GetQueueRoutingInfoResponseMsg getQueueRoutingInfoResponseMsgs = 11; } /* Messages that are handled by ThingsBoard Core Service */ diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index 96df34186c..4e27a01760 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -72,8 +72,6 @@ public class HashPartitionService implements PartitionService { private ConcurrentMap> myPartitions = new ConcurrentHashMap<>(); private ConcurrentMap tpiCache = new ConcurrentHashMap<>(); - private Map tbCoreNotificationTopics = new HashMap<>(); - private Map tbRuleEngineNotificationTopics = new HashMap<>(); private Map> tbTransportServicesByType = new HashMap<>(); private List currentOtherServices; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueRoutingInfo.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueRoutingInfo.java new file mode 100644 index 0000000000..8afecc4413 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueRoutingInfo.java @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.queue.discovery; + +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.gen.transport.TransportProtos.GetQueueRoutingInfoResponseMsg; + +import java.util.UUID; + + +@Data +public class QueueRoutingInfo { + + private final TenantId tenantId; + private final String queueName; + private final String queueTopic; + private final int partitions; + + public QueueRoutingInfo(TenantId tenantId, String queueName, String queueTopic, int partitions) { + this.tenantId = tenantId; + this.queueName = queueName; + this.queueTopic = queueTopic; + this.partitions = partitions; + } + + public QueueRoutingInfo(Queue queue) { + this.tenantId = queue.getTenantId(); + this.queueName = queue.getName(); + this.queueTopic = queue.getTopic(); + this.partitions = queue.getPartitions(); + } + + public QueueRoutingInfo(GetQueueRoutingInfoResponseMsg routingInfo) { + this.tenantId = new TenantId(new UUID(routingInfo.getTenantIdMSB(), routingInfo.getTenantIdLSB())); + this.queueName = routingInfo.getQueueName(); + this.queueTopic = routingInfo.getQueueTopic(); + this.partitions = routingInfo.getPartitions(); + } +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueRoutingInfoService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueRoutingInfoService.java new file mode 100644 index 0000000000..366452d9e8 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueRoutingInfoService.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.queue.discovery; + +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.List; + +public interface QueueRoutingInfoService { + + List getAllQueuesRoutingInfo(); + + List getMainQueuesRoutingInfo(); + + List getQueuesRoutingInfo(TenantId tenantId); +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java index 6c788d93f3..53c1997f5c 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java @@ -57,6 +57,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceLwM2MC import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; +import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicInteger; @@ -67,6 +68,12 @@ public interface TransportService { GetEntityProfileResponseMsg getEntityProfile(GetEntityProfileRequestMsg msg); + List getQueueRoutingInfo(TransportProtos.GetAllMainQueueRoutingInfoRequestMsg msg); + + List getQueueRoutingInfo(TransportProtos.GetTenantQueueRoutingInfoRequestMsg msg); + + List getQueueRoutingInfo(TransportProtos.GetAllQueueRoutingInfoRequestMsg msg); + GetResourceResponseMsg getResource(GetResourceRequestMsg msg); GetSnmpDevicesResponseMsg getSnmpDevicesIds(GetSnmpDevicesRequestMsg requestMsg); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index 77738a1e9d..3de2a6a7d5 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -304,6 +304,42 @@ public class DefaultTransportService implements TransportService { } } + @Override + public List getQueueRoutingInfo(TransportProtos.GetAllQueueRoutingInfoRequestMsg msg) { + TbProtoQueueMsg protoMsg = + new TbProtoQueueMsg<>(UUID.randomUUID(), TransportProtos.TransportApiRequestMsg.newBuilder().setGetAllQueueRoutingInfoRequestMsg(msg).build()); + try { + TbProtoQueueMsg response = transportApiRequestTemplate.send(protoMsg).get(); + return response.getValue().getGetQueueRoutingInfoResponseMsgsList(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + @Override + public List getQueueRoutingInfo(TransportProtos.GetAllMainQueueRoutingInfoRequestMsg msg) { + TbProtoQueueMsg protoMsg = + new TbProtoQueueMsg<>(UUID.randomUUID(), TransportProtos.TransportApiRequestMsg.newBuilder().setGetAllMainQueueRoutingInfoRequestMsg(msg).build()); + try { + TbProtoQueueMsg response = transportApiRequestTemplate.send(protoMsg).get(); + return response.getValue().getGetQueueRoutingInfoResponseMsgsList(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + @Override + public List getQueueRoutingInfo(TransportProtos.GetTenantQueueRoutingInfoRequestMsg msg) { + TbProtoQueueMsg protoMsg = + new TbProtoQueueMsg<>(UUID.randomUUID(), TransportProtos.TransportApiRequestMsg.newBuilder().setGetTenantQueueRoutingInfoRequestMsg(msg).build()); + try { + TbProtoQueueMsg response = transportApiRequestTemplate.send(protoMsg).get(); + return response.getValue().getGetQueueRoutingInfoResponseMsgsList(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + @Override public TransportProtos.GetResourceResponseMsg getResource(TransportProtos.GetResourceRequestMsg msg) { TbProtoQueueMsg protoMsg = diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportQueueRoutingInfoService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportQueueRoutingInfoService.java new file mode 100644 index 0000000000..bcdc07b5b0 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportQueueRoutingInfoService.java @@ -0,0 +1,64 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.common.transport.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.transport.TransportService; +import org.thingsboard.server.gen.transport.TransportProtos.GetAllQueueRoutingInfoRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.GetAllMainQueueRoutingInfoRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.GetTenantQueueRoutingInfoRequestMsg; +import org.thingsboard.server.queue.discovery.QueueRoutingInfo; +import org.thingsboard.server.queue.discovery.QueueRoutingInfoService; + +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Service +@ConditionalOnExpression("'${service.type:null}'=='tb-transport'") +public class TransportQueueRoutingInfoService implements QueueRoutingInfoService { + + private TransportService transportService; + + @Lazy + @Autowired + public void setTransportService(TransportService transportService) { + this.transportService = transportService; + } + + @Override + public List getAllQueuesRoutingInfo() { + GetAllQueueRoutingInfoRequestMsg msg = GetAllQueueRoutingInfoRequestMsg.newBuilder().build(); + return transportService.getQueueRoutingInfo(msg).stream().map(QueueRoutingInfo::new).collect(Collectors.toList()); + } + + @Override + public List getMainQueuesRoutingInfo() { + GetAllMainQueueRoutingInfoRequestMsg msg = GetAllMainQueueRoutingInfoRequestMsg.newBuilder().build(); + return transportService.getQueueRoutingInfo(msg).stream().map(QueueRoutingInfo::new).collect(Collectors.toList()); + } + + @Override + public List getQueuesRoutingInfo(TenantId tenantId) { + GetTenantQueueRoutingInfoRequestMsg msg = GetTenantQueueRoutingInfoRequestMsg.newBuilder().build(); + return transportService.getQueueRoutingInfo(msg).stream().map(QueueRoutingInfo::new).collect(Collectors.toList()); + } +} From ac56d06a55ba71681f26f5f32e40c0f3c1575a34 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 1 Apr 2022 17:03:57 +0300 Subject: [PATCH 034/276] UI: Settings forms for Timeseries table widget --- .../json/system/widget_bundles/cards.json | 15 ++- .../widget/data-key-config.component.ts | 2 +- .../qrcode-widget-settings.component.html | 16 +-- .../qrcode-widget-settings.component.ts | 23 ++-- ...meseries-table-key-settings.component.html | 67 +++++++++++ ...timeseries-table-key-settings.component.ts | 80 +++++++++++++ ...s-table-latest-key-settings.component.html | 74 ++++++++++++ ...ies-table-latest-key-settings.component.ts | 96 +++++++++++++++ ...eries-table-widget-settings.component.html | 95 +++++++++++++++ ...eseries-table-widget-settings.component.ts | 98 +++++++++++++++ .../lib/settings/widget-settings.module.ts | 24 +++- .../widget/lib/settings/widget-settings.scss | 113 ++++++++++++++++++ .../widget/widget-settings.component.ts | 4 + .../pages/widget/widget-editor.component.html | 6 + .../shared/components/js-func.component.html | 7 +- .../shared/components/js-func.component.scss | 5 +- .../shared/components/js-func.component.ts | 2 + ui-ngx/src/app/shared/models/widget.models.ts | 9 +- .../assets/locale/locale.constant-en_US.json | 27 +++++ 19 files changed, 726 insertions(+), 37 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-key-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-key-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-latest-key-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-latest-key-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-widget-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.scss diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index 23b30f017f..dafd87b14b 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -55,10 +55,13 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeseriesTableWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.timeseriesTableWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n ignoreDataUpdateOnIntervalTick: true,\n hasAdditionalLatestDataKeys: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"enableSearch\": {\n \"title\": \"Enable search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyHeader\": {\n \"title\": \"Always display header\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyAction\": {\n \"title\": \"Always display actions column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"reserveSpaceForHiddenAction\": {\n \"title\": \"Hidden cell button actions display mode\",\n \"type\": \"string\",\n \"default\": \"true\"\n },\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showMilliseconds\": {\n \"title\": \"Display timestamp milliseconds\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"useEntityLabel\": {\n \"title\": \"Use entity label in tab name\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"hideEmptyLines\": {\n \"title\": \"Hide empty lines\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"disableStickyHeader\": {\n \"title\": \"Disable sticky header\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"useRowStyleFunction\": {\n \"title\": \"Use row style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"rowStyleFunction\": {\n \"title\": \"Row style function: f(rowData, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"enableSearch\",\n \"enableStickyHeader\",\n \"enableStickyAction\",\n {\n \"key\": \"reserveSpaceForHiddenAction\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"true\",\n \"label\": \"Show empty space instead of hidden cell button action\"\n },\n {\n \"value\": \"false\",\n \"label\": \"Don't reserve space for hidden action buttons\"\n }\n ]\n },\n \"showTimestamp\",\n \"showMilliseconds\",\n \"displayPagination\",\n \"useEntityLabel\",\n \"defaultPageSize\",\n \"hideEmptyLines\",\n \"useRowStyleFunction\",\n {\n \"key\": \"rowStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/timeseries/row_style_fn\",\n \"condition\": \"model.useRowStyleFunction === true\"\n }\n ]\n}", - "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value, rowData, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, rowData, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/timeseries/cell_style_fn\",\n \"condition\": \"model.useCellStyleFunction === true\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/timeseries/cell_content_fn\",\n \"condition\": \"model.useCellContentFunction === true\"\n }\n ]\n}", - "latestDataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"LatestDataKeySettings\",\n \"properties\": {\n \"show\": {\n \"title\": \"Show latest data column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"order\": {\n \"title\": \"Latest data column order\",\n \"type\": \"number\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value, rowData, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, rowData, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"show\",\n {\n \"key\": \"order\",\n \"condition\": \"model.show === true\"\n },\n {\n \"key\": \"useCellStyleFunction\",\n \"condition\": \"model.show === true\"\n },\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/timeseries/cell_style_fn\",\n \"condition\": \"model.show === true && model.useCellStyleFunction === true\"\n },\n {\n \"key\": \"useCellContentFunction\",\n \"condition\": \"model.show === true\"\n },\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/timeseries/cell_content_fn\",\n \"condition\": \"model.show === true && model.useCellContentFunction === true\"\n }\n ]\n}", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', amount = percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true,\"displayPagination\":true,\"defaultPageSize\":10},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\"}" + "settingsSchema": "", + "dataKeySettingsSchema": "", + "latestDataKeySettingsSchema": "", + "settingsDirective": "tb-timeseries-table-widget-settings", + "dataKeySettingsDirective": "tb-timeseries-table-key-settings", + "latestDataKeySettingsDirective": "tb-timeseries-table-latest-key-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}],\"latestDataKeys\":null}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true,\"displayPagination\":true,\"defaultPageSize\":10},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"displayTimewindow\":true}" } }, { @@ -164,8 +167,8 @@ "templateHtml": "\n", "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}", "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.qrCodeWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n datasourcesOptional: true\n };\n}\n\nself.onDestroy = function() {\n}\n\n", - "settingsSchema": "{}\n", - "dataKeySettingsSchema": "{}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "", "settingsDirective": "tb-qrcode-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7036904308224163,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"qrCodeTextPattern\":\"${entityName}\",\"useQrCodeTextFunction\":false,\"qrCodeTextFunction\":\"return data[0] ? data[0]['entityName'] : '';\"},\"title\":\"QR Code\"}" } diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts index 9c23be8144..b0d8847c1b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts @@ -287,7 +287,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con } }; } - if (this.displayAdvanced && !this.dataKeySettingsFormGroup.valid) { + if (this.displayAdvanced && (!this.dataKeySettingsFormGroup.valid || !this.modelValue.settings)) { return { dataKeySettings: { valid: false diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.html index 29a10e1d58..93d3b8086b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.html @@ -15,22 +15,22 @@ limitations under the License. --> -
- +
+ {{ 'widgets.qr-code.use-qr-code-text-function' | translate }} - - + + widgets.qr-code.qr-code-text-pattern {{ 'widgets.qr-code.qr-code-text-pattern-required' | translate }} -
- - +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.ts index 06e8d7d079..c9c43d2951 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.ts @@ -14,22 +14,19 @@ /// limitations under the License. /// -import { Component, ViewChild } from '@angular/core'; +import { Component } from '@angular/core'; import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; -import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { JsFuncComponent } from '@shared/components/js-func.component'; @Component({ selector: 'tb-qrcode-widget-settings', templateUrl: './qrcode-widget-settings.component.html', - styleUrls: [] + styleUrls: ['./widget-settings.scss'] }) export class QrCodeWidgetSettingsComponent extends WidgetSettingsComponent { - @ViewChild('qrCodeTextFunctionComponent', {static: true}) qrCodeTextFunctionComponent: JsFuncComponent; - qrCodeWidgetSettingsForm: FormGroup; constructor(protected store: Store, @@ -51,8 +48,8 @@ export class QrCodeWidgetSettingsComponent extends WidgetSettingsComponent { protected onSettingsSet(settings: WidgetSettings) { this.qrCodeWidgetSettingsForm = this.fb.group({ - qrCodeTextPattern: [settings.qrCodeTextPattern, []], - useQrCodeTextFunction: [settings.useQrCodeTextFunction, []], + qrCodeTextPattern: [settings.qrCodeTextPattern, [Validators.required]], + useQrCodeTextFunction: [settings.useQrCodeTextFunction, [Validators.required]], qrCodeTextFunction: [settings.qrCodeTextFunction, []] }); } @@ -64,13 +61,11 @@ export class QrCodeWidgetSettingsComponent extends WidgetSettingsComponent { protected updateValidators(emitEvent: boolean) { const useQrCodeTextFunction: boolean = this.qrCodeWidgetSettingsForm.get('useQrCodeTextFunction').value; if (useQrCodeTextFunction) { - this.qrCodeWidgetSettingsForm.get('qrCodeTextPattern').setValidators([]); - this.qrCodeWidgetSettingsForm.get('qrCodeTextFunction').setValidators([Validators.required, - (control: AbstractControl) => this.qrCodeTextFunctionComponent.validate(control as FormControl) - ]); + this.qrCodeWidgetSettingsForm.get('qrCodeTextPattern').disable(); + this.qrCodeWidgetSettingsForm.get('qrCodeTextFunction').enable(); } else { - this.qrCodeWidgetSettingsForm.get('qrCodeTextPattern').setValidators([Validators.required]); - this.qrCodeWidgetSettingsForm.get('qrCodeTextFunction').setValidators([]); + this.qrCodeWidgetSettingsForm.get('qrCodeTextPattern').enable(); + this.qrCodeWidgetSettingsForm.get('qrCodeTextFunction').disable(); } this.qrCodeWidgetSettingsForm.get('qrCodeTextPattern').updateValueAndValidity({emitEvent}); this.qrCodeWidgetSettingsForm.get('qrCodeTextFunction').updateValueAndValidity({emitEvent}); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-key-settings.component.html new file mode 100644 index 0000000000..bf568fc3eb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-key-settings.component.html @@ -0,0 +1,67 @@ + +
+
+ widgets.table.cell-style + + + + + {{ 'widgets.table.use-cell-style-function' | translate }} + + + + widget-config.advanced-settings + + + + + + + +
+
+ widgets.table.cell-content + + + + + {{ 'widgets.table.use-cell-content-function' | translate }} + + + + widget-config.advanced-settings + + + + + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-key-settings.component.ts new file mode 100644 index 0000000000..b2433dfde0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-key-settings.component.ts @@ -0,0 +1,80 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-timeseries-table-key-settings', + templateUrl: './timeseries-table-key-settings.component.html', + styleUrls: ['./widget-settings.scss'] +}) +export class TimeseriesTableKeySettingsComponent extends WidgetSettingsComponent { + + timeseriesTableKeySettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.timeseriesTableKeySettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + useCellStyleFunction: false, + cellStyleFunction: '', + useCellContentFunction: false, + cellContentFunction: '' + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.timeseriesTableKeySettingsForm = this.fb.group({ + useCellStyleFunction: [settings.useCellStyleFunction, []], + cellStyleFunction: [settings.cellStyleFunction, [Validators.required]], + useCellContentFunction: [settings.useCellContentFunction, []], + cellContentFunction: [settings.cellContentFunction, [Validators.required]], + }); + } + + protected validatorTriggers(): string[] { + return ['useCellStyleFunction', 'useCellContentFunction']; + } + + protected updateValidators(emitEvent: boolean) { + const useCellStyleFunction: boolean = this.timeseriesTableKeySettingsForm.get('useCellStyleFunction').value; + const useCellContentFunction: boolean = this.timeseriesTableKeySettingsForm.get('useCellContentFunction').value; + if (useCellStyleFunction) { + this.timeseriesTableKeySettingsForm.get('cellStyleFunction').enable(); + } else { + this.timeseriesTableKeySettingsForm.get('cellStyleFunction').disable(); + } + if (useCellContentFunction) { + this.timeseriesTableKeySettingsForm.get('cellContentFunction').enable(); + } else { + this.timeseriesTableKeySettingsForm.get('cellContentFunction').disable(); + } + this.timeseriesTableKeySettingsForm.get('cellStyleFunction').updateValueAndValidity({emitEvent}); + this.timeseriesTableKeySettingsForm.get('cellContentFunction').updateValueAndValidity({emitEvent}); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-latest-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-latest-key-settings.component.html new file mode 100644 index 0000000000..b005b45ce8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-latest-key-settings.component.html @@ -0,0 +1,74 @@ + +
+ + {{ 'widgets.table.show-latest-data-column' | translate }} + + + widgets.table.latest-data-column-order + + +
+ widgets.table.cell-style + + + + + {{ 'widgets.table.use-cell-style-function' | translate }} + + + + widget-config.advanced-settings + + + + + + + +
+
+ widgets.table.cell-content + + + + + {{ 'widgets.table.use-cell-content-function' | translate }} + + + + widget-config.advanced-settings + + + + + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-latest-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-latest-key-settings.component.ts new file mode 100644 index 0000000000..1d8d26ead3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-latest-key-settings.component.ts @@ -0,0 +1,96 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-timeseries-table-latest-key-settings', + templateUrl: './timeseries-table-latest-key-settings.component.html', + styleUrls: ['./widget-settings.scss'] +}) +export class TimeseriesTableLatestKeySettingsComponent extends WidgetSettingsComponent { + + timeseriesTableLatestKeySettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.timeseriesTableLatestKeySettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + show: true, + useCellStyleFunction: false, + cellStyleFunction: '', + useCellContentFunction: false, + cellContentFunction: '' + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.timeseriesTableLatestKeySettingsForm = this.fb.group({ + show: [settings.show, []], + order: [settings.order, []], + useCellStyleFunction: [settings.useCellStyleFunction, []], + cellStyleFunction: [settings.cellStyleFunction, [Validators.required]], + useCellContentFunction: [settings.useCellContentFunction, []], + cellContentFunction: [settings.cellContentFunction, [Validators.required]], + }); + } + + protected validatorTriggers(): string[] { + return ['show', 'useCellStyleFunction', 'useCellContentFunction']; + } + + protected updateValidators(emitEvent: boolean) { + const show: boolean = this.timeseriesTableLatestKeySettingsForm.get('show').value; + if (show) { + this.timeseriesTableLatestKeySettingsForm.get('order').enable(); + this.timeseriesTableLatestKeySettingsForm.get('useCellStyleFunction').enable({emitEvent: false}); + this.timeseriesTableLatestKeySettingsForm.get('useCellContentFunction').enable({emitEvent: false}); + const useCellStyleFunction: boolean = this.timeseriesTableLatestKeySettingsForm.get('useCellStyleFunction').value; + const useCellContentFunction: boolean = this.timeseriesTableLatestKeySettingsForm.get('useCellContentFunction').value; + if (useCellStyleFunction) { + this.timeseriesTableLatestKeySettingsForm.get('cellStyleFunction').enable(); + } else { + this.timeseriesTableLatestKeySettingsForm.get('cellStyleFunction').disable(); + } + if (useCellContentFunction) { + this.timeseriesTableLatestKeySettingsForm.get('cellContentFunction').enable(); + } else { + this.timeseriesTableLatestKeySettingsForm.get('cellContentFunction').disable(); + } + } else { + this.timeseriesTableLatestKeySettingsForm.get('order').disable(); + this.timeseriesTableLatestKeySettingsForm.get('useCellStyleFunction').disable({emitEvent: false}); + this.timeseriesTableLatestKeySettingsForm.get('cellStyleFunction').disable(); + this.timeseriesTableLatestKeySettingsForm.get('useCellContentFunction').disable({emitEvent: false}); + this.timeseriesTableLatestKeySettingsForm.get('cellContentFunction').disable(); + } + this.timeseriesTableLatestKeySettingsForm.get('order').updateValueAndValidity({emitEvent}); + this.timeseriesTableLatestKeySettingsForm.get('cellStyleFunction').updateValueAndValidity({emitEvent}); + this.timeseriesTableLatestKeySettingsForm.get('cellContentFunction').updateValueAndValidity({emitEvent}); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-widget-settings.component.html new file mode 100644 index 0000000000..5f90fd6aed --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-widget-settings.component.html @@ -0,0 +1,95 @@ + +
+
+ widgets.table.common-table-settings +
+
+ + {{ 'widgets.table.enable-search' | translate }} + +
+
+ + {{ 'widgets.table.enable-sticky-header' | translate }} + + + {{ 'widgets.table.enable-sticky-action' | translate }} + +
+
+ + widgets.table.hidden-cell-button-display-mode + + + {{ 'widgets.table.show-empty-space-hidden-action' | translate }} + + + {{ 'widgets.table.dont-reserve-space-hidden-action' | translate }} + + + +
+ + {{ 'widgets.table.display-timestamp' | translate }} + + + {{ 'widgets.table.display-milliseconds' | translate }} + +
+ + {{ 'widgets.table.display-pagination' | translate }} + + + widgets.table.default-page-size + + +
+ + {{ 'widgets.table.use-entity-label-tab-name' | translate }} + + + {{ 'widgets.table.hide-empty-lines' | translate }} + +
+
+
+ widgets.table.row-style + + + + + {{ 'widgets.table.use-row-style-function' | translate }} + + + + widget-config.advanced-settings + + + + + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-widget-settings.component.ts new file mode 100644 index 0000000000..d28b4f081a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-widget-settings.component.ts @@ -0,0 +1,98 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-timeseries-table-widget-settings', + templateUrl: './timeseries-table-widget-settings.component.html', + styleUrls: ['./widget-settings.scss'] +}) +export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsComponent { + + timeseriesTableWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.timeseriesTableWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + enableSearch: true, + enableStickyHeader: true, + enableStickyAction: true, + reserveSpaceForHiddenAction: 'true', + showTimestamp: true, + showMilliseconds: false, + displayPagination: true, + useEntityLabel: false, + defaultPageSize: 10, + hideEmptyLines: false, + disableStickyHeader: false, + useRowStyleFunction: false, + rowStyleFunction: '' + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.timeseriesTableWidgetSettingsForm = this.fb.group({ + enableSearch: [settings.enableSearch, []], + enableStickyHeader: [settings.enableStickyHeader, []], + enableStickyAction: [settings.enableStickyAction, []], + reserveSpaceForHiddenAction: [settings.reserveSpaceForHiddenAction, []], + showTimestamp: [settings.showTimestamp, []], + showMilliseconds: [settings.showMilliseconds, []], + displayPagination: [settings.displayPagination, []], + useEntityLabel: [settings.useEntityLabel, []], + defaultPageSize: [settings.defaultPageSize, [Validators.min(1)]], + hideEmptyLines: [settings.hideEmptyLines, []], + disableStickyHeader: [settings.disableStickyHeader, []], + useRowStyleFunction: [settings.useRowStyleFunction, []], + rowStyleFunction: [settings.rowStyleFunction, [Validators.required]] + }); + } + + protected validatorTriggers(): string[] { + return ['useRowStyleFunction', 'displayPagination']; + } + + protected updateValidators(emitEvent: boolean) { + const useRowStyleFunction: boolean = this.timeseriesTableWidgetSettingsForm.get('useRowStyleFunction').value; + const displayPagination: boolean = this.timeseriesTableWidgetSettingsForm.get('displayPagination').value; + if (useRowStyleFunction) { + this.timeseriesTableWidgetSettingsForm.get('rowStyleFunction').enable(); + } else { + this.timeseriesTableWidgetSettingsForm.get('rowStyleFunction').disable(); + } + if (displayPagination) { + this.timeseriesTableWidgetSettingsForm.get('defaultPageSize').enable(); + } else { + this.timeseriesTableWidgetSettingsForm.get('defaultPageSize').disable(); + } + this.timeseriesTableWidgetSettingsForm.get('rowStyleFunction').updateValueAndValidity({emitEvent}); + this.timeseriesTableWidgetSettingsForm.get('defaultPageSize').updateValueAndValidity({emitEvent}); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index 7a10d51465..e31eef1f87 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -20,10 +20,22 @@ import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; import { SharedHomeComponentsModule } from '@home/components/shared-home-components.module'; import { IWidgetSettingsComponent } from '@shared/models/widget.models'; +import { + TimeseriesTableWidgetSettingsComponent +} from '@home/components/widget/lib/settings/timeseries-table-widget-settings.component'; +import { + TimeseriesTableKeySettingsComponent +} from '@home/components/widget/lib/settings/timeseries-table-key-settings.component'; +import { + TimeseriesTableLatestKeySettingsComponent +} from '@home/components/widget/lib/settings/timeseries-table-latest-key-settings.component'; @NgModule({ declarations: [ - QrCodeWidgetSettingsComponent + QrCodeWidgetSettingsComponent, + TimeseriesTableWidgetSettingsComponent, + TimeseriesTableKeySettingsComponent, + TimeseriesTableLatestKeySettingsComponent ], imports: [ CommonModule, @@ -31,12 +43,18 @@ import { IWidgetSettingsComponent } from '@shared/models/widget.models'; SharedHomeComponentsModule ], exports: [ - QrCodeWidgetSettingsComponent + QrCodeWidgetSettingsComponent, + TimeseriesTableWidgetSettingsComponent, + TimeseriesTableKeySettingsComponent, + TimeseriesTableLatestKeySettingsComponent ] }) export class WidgetSettingsModule { } export const widgetSettingsComponentsMap: {[key: string]: Type} = { - 'tb-qrcode-widget-settings': QrCodeWidgetSettingsComponent + 'tb-qrcode-widget-settings': QrCodeWidgetSettingsComponent, + 'tb-timeseries-table-widget-settings': TimeseriesTableWidgetSettingsComponent, + 'tb-timeseries-table-key-settings': TimeseriesTableKeySettingsComponent, + 'tb-timeseries-table-latest-key-settings': TimeseriesTableLatestKeySettingsComponent }; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.scss new file mode 100644 index 0000000000..4b9e0edd97 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.scss @@ -0,0 +1,113 @@ +/** + * Copyright © 2016-2022 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. + */ +:host { + .tb-widget-settings { + .fields-group { + padding: 0 16px 8px; + margin-bottom: 10px; + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + + legend { + color: rgba(0, 0, 0, .7); + width: fit-content; + } + + legend + * { + margin-top: 16px; + } + + &.fields-group-slider { + padding: 0; + + legend { + margin-left: 16px; + } + + .tb-settings { + margin-top: 0; + padding: 0 16px 8px; + } + } + } + } +} + +:host ::ng-deep { + .tb-widget-settings { + .mat-checkbox-label { + white-space: normal; + } + + .mat-expansion-panel { + &.tb-settings { + box-shadow: none; + + .mat-content { + overflow: visible; + } + + .mat-expansion-panel-header { + padding: 0; + + &:hover { + background: none; + } + + .mat-expansion-indicator { + padding: 2px; + } + } + + .mat-expansion-panel-header-description { + align-items: center; + } + + .mat-expansion-panel-body { + padding: 0; + } + + .mat-checkbox-layout { + margin: 5px 0; + } + + .mat-checkbox-inner-container { + margin-right: 12px; + } + } + + .mat-expansion-panel-content { + font: inherit; + } + } + + .mat-slide { + margin: 8px 0; + } + + .slide-block { + display: block; + + &:not(:last-child) { + margin-bottom: 8px; + } + } + + .mat-slide-toggle-content { + white-space: normal; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.ts index 9226c4e1f1..15919447ea 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.ts @@ -43,6 +43,7 @@ import { JsonFormComponentData } from '@shared/components/json-form/json-form-co import { IWidgetSettingsComponent, Widget, WidgetSettings } from '@shared/models/widget.models'; import { widgetSettingsComponentsMap } from '@home/components/widget/lib/settings/widget-settings.module'; import { Dashboard } from '@shared/models/dashboard.models'; +import { WidgetService } from '@core/http/widget.service'; @Component({ selector: 'tb-widget-settings', @@ -100,6 +101,7 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On constructor(private translate: TranslateService, private cfr: ComponentFactoryResolver, + private widgetService: WidgetService, private fb: FormBuilder) { this.widgetSettingsFormGroup = this.fb.group({ settings: [null, Validators.required] @@ -143,6 +145,7 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On if (this.definedSettingsComponent) { this.definedSettingsComponent.dashboard = this.dashboard; this.definedSettingsComponent.widget = this.widget; + this.definedSettingsComponent.functionScopeVariables = this.widgetService.getWidgetScopeVariables(); this.definedSettingsComponent.settings = this.widgetSettingsFormData.model; this.changeSubscription = this.definedSettingsComponent.settingsChanged.subscribe((settings) => { this.updateModel(settings); @@ -196,6 +199,7 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On this.definedSettingsComponent = this.definedSettingsComponentRef.instance; this.definedSettingsComponent.dashboard = this.dashboard; this.definedSettingsComponent.widget = this.widget; + this.definedSettingsComponent.functionScopeVariables = this.widgetService.getWidgetScopeVariables(); this.changeSubscription = this.definedSettingsComponent.settingsChanged.subscribe((settings) => { this.updateModel(settings); }); diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html index 549230d6a3..13c03b7945 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html @@ -265,6 +265,12 @@ [(ngModel)]="widget.dataKeySettingsDirective" (ngModelChange)="isDirty = true"/> + + widget.latest-data-key-settings-form-selector + + diff --git a/ui-ngx/src/app/shared/components/js-func.component.html b/ui-ngx/src/app/shared/components/js-func.component.html index 3769e1ff45..a5f4040af1 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.html +++ b/ui-ngx/src/app/shared/components/js-func.component.html @@ -19,7 +19,8 @@ tb-fullscreen [fullscreen]="fullscreen" fxLayout="column">
- + +
-
+
- +
diff --git a/ui-ngx/src/app/shared/components/js-func.component.scss b/ui-ngx/src/app/shared/components/js-func.component.scss index 6cb0154c88..3a7ddc3d09 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.scss +++ b/ui-ngx/src/app/shared/components/js-func.component.scss @@ -26,9 +26,12 @@ .tb-js-func-panel { height: calc(100% - 80px); - margin-left: 15px; border: 1px solid #c0c0c0; + &:not(.tb-js-func-title) { + margin-left: 15px; + } + #tb-javascript-input { width: 100%; min-width: 200px; diff --git a/ui-ngx/src/app/shared/components/js-func.component.ts b/ui-ngx/src/app/shared/components/js-func.component.ts index 439677ffe5..be78c7480c 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.ts +++ b/ui-ngx/src/app/shared/components/js-func.component.ts @@ -69,6 +69,8 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, toastTargetId = `jsFuncEditor-${guid()}`; + @Input() functionTitle: string; + @Input() functionName: string; @Input() functionArgs: Array; diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index bbb2ea56d2..1f3cece5cb 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -615,6 +615,7 @@ export interface WidgetSize { export interface IWidgetSettingsComponent { dashboard: Dashboard; widget: Widget; + functionScopeVariables: string[]; settings: WidgetSettings; settingsChanged: Observable; validate(); @@ -630,12 +631,18 @@ export abstract class WidgetSettingsComponent extends PageComponent implements widget: Widget; + functionScopeVariables: string[]; + settingsValue: WidgetSettings; private settingsSet = false; set settings(value: WidgetSettings) { - this.settingsValue = value || this.defaultSettings(); + if (!value) { + this.settingsValue = this.defaultSettings(); + } else { + this.settingsValue = {...this.defaultSettings(), ...value}; + } if (!this.settingsSet) { this.settingsSet = true; this.setupSettings(this.settingsValue); diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index f904f10930..47d22d9b3f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3001,6 +3001,7 @@ "image-preview": "Image preview", "settings-form-selector": "Settings form selector", "data-key-settings-form-selector": "Data key settings form selector", + "latest-data-key-settings-form-selector": "Latest data key settings form selector", "javascript": "Javascript", "js": "JS", "remove-widget-type-title": "Are you sure you want to remove the widget type '{{widgetName}}'?", @@ -3366,6 +3367,32 @@ "drawCircleMarkerButton": "Create circle marker", "rotateButton": "Rotate polygon" } + }, + "table": { + "common-table-settings": "Common Table Settings", + "enable-search": "Enable search", + "enable-sticky-header": "Always display header", + "enable-sticky-action": "Always display actions column", + "hidden-cell-button-display-mode": "Hidden cell button actions display mode", + "show-empty-space-hidden-action": "Show empty space instead of hidden cell button action", + "dont-reserve-space-hidden-action": "Don't reserve space for hidden action buttons", + "display-timestamp": "Display timestamp column", + "display-milliseconds": "Display timestamp milliseconds", + "display-pagination": "Display pagination", + "default-page-size": "Default page size", + "use-entity-label-tab-name": "Use entity label in tab name", + "hide-empty-lines": "Hide empty lines", + "row-style": "Row style", + "use-row-style-function": "Use row style function", + "row-style-function": "Row style function", + "cell-style": "Cell style", + "use-cell-style-function": "Use cell style function", + "cell-style-function": "Cell style function", + "cell-content": "Cell content", + "use-cell-content-function": "Use cell content function", + "cell-content-function": "Cell content function", + "show-latest-data-column": "Show latest data column", + "latest-data-column-order": "Latest data column order" } }, "icon": { From 0177904eb20a8bf221e86afc79686d780b1a9686 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 1 Apr 2022 19:18:50 +0300 Subject: [PATCH 035/276] UI: Add markdown widget settings form --- .../json/system/widget_bundles/cards.json | 5 +- .../markdown-widget-settings.component.html | 38 +++++ .../markdown-widget-settings.component.ts | 76 +++++++++ .../lib/settings/widget-settings.module.ts | 12 +- .../app/shared/components/css.component.scss | 3 +- .../shared/components/js-func.component.html | 8 +- .../shared/components/js-func.component.scss | 22 ++- .../components/markdown-editor.component.html | 47 ++++++ .../components/markdown-editor.component.scss | 77 +++++++++ .../components/markdown-editor.component.ts | 154 ++++++++++++++++++ ui-ngx/src/app/shared/shared.module.ts | 3 + .../assets/locale/locale.constant-en_US.json | 8 + 12 files changed, 438 insertions(+), 15 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.ts create mode 100644 ui-ngx/src/app/shared/components/markdown-editor.component.html create mode 100644 ui-ngx/src/app/shared/components/markdown-editor.component.scss create mode 100644 ui-ngx/src/app/shared/components/markdown-editor.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index dafd87b14b..e574e1d98e 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -186,8 +186,9 @@ "templateHtml": "\n", "templateCss": "#container tb-markdown-widget {\n height: 100%;\n display: block;\n}\n\n#container tb-markdown-widget .tb-markdown-view {\n height: 100%;\n overflow: auto;\n}\n", "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.markdownWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'elementClick': {\n name: 'widget-action.element-click',\n multiple: true\n }\n };\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n datasourcesOptional: true,\n hasDataPageLink: true\n };\n}\n\nself.onDestroy = function() {\n}\n\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Markdown/HTML card\",\n \"properties\": {\n \"markdownTextPattern\": {\n \"title\": \"Markdown/HTML pattern (markdown or HTML with variables, for ex. '${entityName} or ${keyName} - some text.')\",\n \"type\": \"string\",\n \"default\": \"# Markdown/HTML card \\n - **Current entity**: **${entityName}**. \\n - **Current value**: **${Random}**.\"\n },\n \"markdownCss\": {\n \"title\": \"Markdown/HTML CSS\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useMarkdownTextFunction\": {\n \"title\": \"Use markdown/HTML value function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"markdownTextFunction\": {\n \"title\": \"Markdown/HTML value function: f(data)\",\n \"type\": \"string\",\n \"default\": \"return '# Some title\\\\n - Entity name: ' + data[0]['entityName'];\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useMarkdownTextFunction\",\n {\n \"key\": \"markdownTextPattern\",\n \"type\": \"markdown\",\n \"condition\": \"model.useMarkdownTextFunction !== true\"\n },\n {\n \"key\": \"markdownTextFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/markdown/markdown_text_fn\",\n \"condition\": \"model.useMarkdownTextFunction === true\"\n },\n {\n \"key\": \"markdownCss\",\n \"type\": \"css\"\n }\n ]\n}\n", - "dataKeySettingsSchema": "{}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-markdown-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"markdownTextPattern\":\"### Markdown/HTML card\\n - **Current entity**: ${entityName}.\\n - **Current value**: ${Random}.\",\"markdownTextFunction\":\"return '# Some title\\\\n - Entity name: ' + data[0]['entityName'];\",\"useMarkdownTextFunction\":false},\"title\":\"Markdown/HTML Card\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" } }, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.html new file mode 100644 index 0000000000..f586552d15 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.html @@ -0,0 +1,38 @@ + +
+ + {{ 'widgets.markdown.use-markdown-text-function' | translate }} + +
+ + +
+
+ + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.ts new file mode 100644 index 0000000000..db78d8b42f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.ts @@ -0,0 +1,76 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-markdown-widget-settings', + templateUrl: './markdown-widget-settings.component.html', + styleUrls: ['./widget-settings.scss'] +}) +export class MarkdownWidgetSettingsComponent extends WidgetSettingsComponent { + + markdownWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.markdownWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + useMarkdownTextFunction: false, + markdownTextPattern: '# Markdown/HTML card \\n - **Current entity**: **${entityName}**. \\n - **Current value**: **${Random}**.', + markdownTextFunction: 'return \'# Some title\\\\n - Entity name: \' + data[0][\'entityName\'];', + markdownCss: '' + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.markdownWidgetSettingsForm = this.fb.group({ + useMarkdownTextFunction: [settings.useMarkdownTextFunction, []], + markdownTextPattern: [settings.markdownTextPattern, []], + markdownTextFunction: [settings.qrCodeTextFunction, []], + markdownCss: [settings.markdownCss, []] + }); + } + + protected validatorTriggers(): string[] { + return ['useMarkdownTextFunction']; + } + + protected updateValidators(emitEvent: boolean) { + const useMarkdownTextFunction: boolean = this.markdownWidgetSettingsForm.get('useMarkdownTextFunction').value; + if (useMarkdownTextFunction) { + this.markdownWidgetSettingsForm.get('markdownTextPattern').disable(); + this.markdownWidgetSettingsForm.get('markdownTextFunction').enable(); + } else { + this.markdownWidgetSettingsForm.get('markdownTextPattern').enable(); + this.markdownWidgetSettingsForm.get('markdownTextFunction').disable(); + } + this.markdownWidgetSettingsForm.get('markdownTextPattern').updateValueAndValidity({emitEvent}); + this.markdownWidgetSettingsForm.get('markdownTextFunction').updateValueAndValidity({emitEvent}); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index e31eef1f87..03e04bed31 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -29,13 +29,17 @@ import { import { TimeseriesTableLatestKeySettingsComponent } from '@home/components/widget/lib/settings/timeseries-table-latest-key-settings.component'; +import { + MarkdownWidgetSettingsComponent +} from '@home/components/widget/lib/settings/markdown-widget-settings.component'; @NgModule({ declarations: [ QrCodeWidgetSettingsComponent, TimeseriesTableWidgetSettingsComponent, TimeseriesTableKeySettingsComponent, - TimeseriesTableLatestKeySettingsComponent + TimeseriesTableLatestKeySettingsComponent, + MarkdownWidgetSettingsComponent ], imports: [ CommonModule, @@ -46,7 +50,8 @@ import { QrCodeWidgetSettingsComponent, TimeseriesTableWidgetSettingsComponent, TimeseriesTableKeySettingsComponent, - TimeseriesTableLatestKeySettingsComponent + TimeseriesTableLatestKeySettingsComponent, + MarkdownWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -56,5 +61,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type -
@@ -37,10 +37,10 @@
-
+
-
- +
+
diff --git a/ui-ngx/src/app/shared/components/js-func.component.scss b/ui-ngx/src/app/shared/components/js-func.component.scss index 3a7ddc3d09..6b11058333 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.scss +++ b/ui-ngx/src/app/shared/components/js-func.component.scss @@ -24,14 +24,22 @@ height: 100%; } + &:not(.tb-js-func-title) { + .tb-js-func-panel { + margin-left: 15px; + } + } + + &.tb-js-func-title { + .tb-js-func-panel { + height: calc(100% - 40px); + } + } + .tb-js-func-panel { height: calc(100% - 80px); border: 1px solid #c0c0c0; - &:not(.tb-js-func-title) { - margin-left: 15px; - } - #tb-javascript-input { width: 100%; min-width: 200px; @@ -43,6 +51,12 @@ } } + &:not(.tb-fullscreen) { + &.tb-js-func-title { + padding-bottom: 15px; + } + } + .tb-js-func-toolbar { & > * { &:not(:last-child) { diff --git a/ui-ngx/src/app/shared/components/markdown-editor.component.html b/ui-ngx/src/app/shared/components/markdown-editor.component.html new file mode 100644 index 0000000000..e31ad8bc3f --- /dev/null +++ b/ui-ngx/src/app/shared/components/markdown-editor.component.html @@ -0,0 +1,47 @@ + +
+
+ +
+
+
+ + + +
+
+
+ +
+
+
+ +
+
diff --git a/ui-ngx/src/app/shared/components/markdown-editor.component.scss b/ui-ngx/src/app/shared/components/markdown-editor.component.scss new file mode 100644 index 0000000000..a4149c54d7 --- /dev/null +++ b/ui-ngx/src/app/shared/components/markdown-editor.component.scss @@ -0,0 +1,77 @@ +/** + * Copyright © 2016-2022 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. + */ +.markdown-content { + min-width: 400px; + &.tb-edit-mode { + .tb-markdown-view-container { + border: 1px solid #c0c0c0; + } + .tb-markdown-view { + padding: 20px; + } + &:not(.tb-fullscreen) { + padding-bottom: 15px; + .markdown-content-editor { + min-height: 200px; + max-height: 200px; + height: 200px; + } + .tb-markdown-view-container { + min-height: 200px; + max-height: 200px; + height: 200px; + } + } + } + &.tb-fullscreen { + background: #fff; + .markdown-content-editor { + height: calc(100% - 40px); + } + } + .markdown-content-editor { + position: relative; + height: 100%; + } + .tb-markdown-editor { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border: 1px solid #c0c0c0; + } + .tb-markdown-view-container { + overflow: auto; + height: 100%; + } + .buttons-panel { + position: absolute; + top: 5px; + right: 24px; + z-index: 1; + button.edit-toggle { + min-width: 32px; + min-height: 15px; + padding: 4px; + margin: 0; + font-size: .8rem; + line-height: 15px; + color: #7b7b7b; + background: rgba(220, 220, 220, .35); + } + } +} diff --git a/ui-ngx/src/app/shared/components/markdown-editor.component.ts b/ui-ngx/src/app/shared/components/markdown-editor.component.ts new file mode 100644 index 0000000000..7c1c9c7c02 --- /dev/null +++ b/ui-ngx/src/app/shared/components/markdown-editor.component.ts @@ -0,0 +1,154 @@ +/// +/// Copyright © 2016-2022 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 { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Ace } from 'ace-builds'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { getAce } from '@shared/models/ace/ace.models'; + +@Component({ + selector: 'tb-markdown-editor', + templateUrl: './markdown-editor.component.html', + styleUrls: ['./markdown-editor.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MarkdownEditorComponent), + multi: true + } + ] +}) +export class MarkdownEditorComponent implements OnInit, ControlValueAccessor { + + @Input() label: string; + + @Input() disabled: boolean; + + @Input() readonly: boolean; + + @ViewChild('markdownEditor', {static: true}) + markdownEditorElmRef: ElementRef; + + private markdownEditor: Ace.Editor; + + editorMode = true; + + fullscreen = false; + + markdownValue: string; + renderValue: string; + + ignoreChange = false; + + private propagateChange = null; + + private requiredValue: boolean; + + get required(): boolean { + return this.requiredValue; + } + + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + constructor() { + } + + ngOnInit(): void { + if (!this.readonly) { + const editorElement = this.markdownEditorElmRef.nativeElement; + let editorOptions: Partial = { + mode: 'ace/mode/markdown', + showGutter: true, + showPrintMargin: false, + readOnly: false + }; + + const advancedOptions = { + enableSnippets: true, + enableBasicAutocompletion: true, + enableLiveAutocompletion: true + }; + + editorOptions = {...editorOptions, ...advancedOptions}; + + getAce().subscribe( + (ace) => { + this.markdownEditor = ace.edit(editorElement, editorOptions); + this.markdownEditor.session.setUseWrapMode(false); + this.markdownEditor.setValue(this.markdownValue ? this.markdownValue : '', -1); + this.markdownEditor.on('change', () => { + if (!this.ignoreChange) { + this.updateView(); + } + }); + } + ); + + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: string): void { + this.editorMode = true; + this.markdownValue = value; + this.renderValue = this.markdownValue ? this.markdownValue : ' '; + if (this.markdownEditor) { + this.ignoreChange = true; + this.markdownEditor.setValue(this.markdownValue ? this.markdownValue : '', -1); + this.ignoreChange = false; + } + } + + updateView() { + const editorValue = this.markdownEditor.getValue(); + if (this.markdownValue !== editorValue) { + this.markdownValue = editorValue; + this.renderValue = this.markdownValue ? this.markdownValue : ' '; + this.propagateChange(this.markdownValue); + } + } + + onFullscreen() { + if (this.markdownEditor) { + setTimeout(() => { + this.markdownEditor.resize(); + }, 0); + } + } + + toggleEditMode() { + this.editorMode = !this.editorMode; + if (this.editorMode && this.markdownEditor) { + setTimeout(() => { + this.markdownEditor.resize(); + }, 0); + } + } +} diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index a356fa5921..9d3d6864fe 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -79,6 +79,7 @@ import { EnumToArrayPipe } from '@shared/pipe/enum-to-array.pipe'; import { ClipboardModule } from 'ngx-clipboard'; import { ValueInputComponent } from '@shared/components/value-input.component'; import { MarkdownModule, MarkedOptions } from 'ngx-markdown'; +import { MarkdownEditorComponent } from '@shared/components/markdown-editor.component'; import { FullscreenDirective } from '@shared/components/fullscreen.directive'; import { HighlightPipe } from '@shared/pipe/highlight.pipe'; import { DashboardAutocompleteComponent } from '@shared/components/dashboard-autocomplete.component'; @@ -258,6 +259,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) KeyValMapComponent, NavTreeComponent, LedLightComponent, + MarkdownEditorComponent, NospacePipe, MillisecondsToTimeStringPipe, EnumToArrayPipe, @@ -452,6 +454,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) KeyValMapComponent, NavTreeComponent, LedLightComponent, + MarkdownEditorComponent, NospacePipe, MillisecondsToTimeStringPipe, EnumToArrayPipe, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 47d22d9b3f..272ea90d7d 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2390,6 +2390,8 @@ "error": "Login error" }, "markdown": { + "edit": "Edit", + "preview": "Preview", "copy-code": "Click to copy", "copied": "Copied!" }, @@ -3368,6 +3370,12 @@ "rotateButton": "Rotate polygon" } }, + "markdown": { + "use-markdown-text-function": "Use markdown/HTML value function", + "markdown-text-function": "Markdown/HTML value function", + "markdown-text-pattern": "Markdown/HTML pattern (markdown or HTML with variables, for ex. '${entityName} or ${keyName} - some text.')", + "markdown-css": "Markdown/HTML CSS" + }, "table": { "common-table-settings": "Common Table Settings", "enable-search": "Enable search", From 7cc553021bbf5c79af395a27da12334baf53b93f Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Sat, 2 Apr 2022 15:09:40 +0300 Subject: [PATCH 036/276] UI: Add label widget settings form --- .../json/system/widget_bundles/cards.json | 3 +- .../settings/label-widget-font.component.html | 71 +++++++++++ .../settings/label-widget-font.component.ts | 104 ++++++++++++++++ .../label-widget-label.component.html | 72 +++++++++++ .../label-widget-label.component.scss | 40 +++++++ .../settings/label-widget-label.component.ts | 113 ++++++++++++++++++ .../label-widget-settings.component.html | 49 ++++++++ .../label-widget-settings.component.scss | 32 +++++ .../label-widget-settings.component.ts | 92 ++++++++++++++ .../markdown-widget-settings.component.html | 24 ++-- .../qrcode-widget-settings.component.html | 15 ++- .../lib/settings/widget-settings.module.ts | 16 ++- .../assets/locale/locale.constant-en_US.json | 26 ++++ 13 files changed, 632 insertions(+), 25 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-font.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-font.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-label.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-label.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-label.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-settings.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-settings.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index e574e1d98e..7debb29ff9 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -113,8 +113,9 @@ "templateHtml": "", "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}", "controllerScript": "self.onInit = function() {\n self.ctx.varsRegex = /\\$\\{([^\\}]*)\\}/g;\n \n var imageUrl = self.ctx.settings.backgroundImageUrl ? self.ctx.settings.backgroundImageUrl :\n 'data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==';\n\n self.ctx.$container.css('background', 'url(\"'+imageUrl+'\") no-repeat');\n self.ctx.$container.css('backgroundSize', 'contain');\n self.ctx.$container.css('backgroundPosition', '50% 50%');\n \n function processLabelPattern(pattern, data) {\n var match = self.ctx.varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split(':');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n \n if (label.startsWith('#')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = n;\n }\n }\n if (variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = self.ctx.varsRegex.exec(pattern);\n }\n return replaceInfo;\n }\n\n var configuredLabels = self.ctx.settings.labels;\n if (!configuredLabels) {\n configuredLabels = [];\n }\n \n self.ctx.labels = [];\n\n for (var l = 0; l < configuredLabels.length; l++) {\n var labelConfig = configuredLabels[l];\n var localConfig = {};\n localConfig.font = {};\n \n localConfig.pattern = labelConfig.pattern ? labelConfig.pattern : '${#0}';\n localConfig.x = labelConfig.x ? labelConfig.x : 0;\n localConfig.y = labelConfig.y ? labelConfig.y : 0;\n localConfig.backgroundColor = labelConfig.backgroundColor ? labelConfig.backgroundColor : 'rgba(0,0,0,0)';\n \n var settingsFont = labelConfig.font;\n if (!settingsFont) {\n settingsFont = {};\n }\n \n localConfig.font.family = settingsFont.family || 'Roboto';\n localConfig.font.size = settingsFont.size ? settingsFont.size : 6;\n localConfig.font.style = settingsFont.style ? settingsFont.style : 'normal';\n localConfig.font.weight = settingsFont.weight ? settingsFont.weight : '500';\n localConfig.font.color = settingsFont.color ? settingsFont.color : '#fff';\n \n localConfig.replaceInfo = processLabelPattern(localConfig.pattern, self.ctx.data);\n \n var label = {};\n var labelElement = $('
');\n labelElement.css('position', 'absolute');\n labelElement.css('display', 'none');\n labelElement.css('top', '0');\n labelElement.css('left', '0');\n labelElement.css('backgroundColor', localConfig.backgroundColor);\n labelElement.css('color', localConfig.font.color);\n labelElement.css('fontFamily', localConfig.font.family);\n labelElement.css('fontStyle', localConfig.font.style);\n labelElement.css('fontWeight', localConfig.font.weight);\n \n labelElement.html(localConfig.pattern);\n self.ctx.$container.append(labelElement);\n label.element = labelElement;\n label.config = localConfig;\n label.htmlSet = false;\n label.visible = false;\n self.ctx.labels.push(label);\n }\n\n var bgImg = $('');\n bgImg.hide();\n bgImg.bind('load', function()\n {\n self.ctx.bImageHeight = $(this).height();\n self.ctx.bImageWidth = $(this).width();\n self.onResize();\n });\n self.ctx.$container.append(bgImg);\n bgImg.attr('src', imageUrl);\n \n self.onDataUpdated();\n}\n\nself.onDataUpdated = function() {\n updateLabels();\n}\n\nself.onResize = function() {\n if (self.ctx.bImageHeight && self.ctx.bImageWidth) {\n var backgroundRect = {};\n var imageRatio = self.ctx.bImageWidth / self.ctx.bImageHeight;\n var componentRatio = self.ctx.width / self.ctx.height;\n if (componentRatio >= imageRatio) {\n backgroundRect.top = 0;\n backgroundRect.bottom = 1.0;\n backgroundRect.xRatio = imageRatio / componentRatio;\n backgroundRect.yRatio = 1;\n var offset = (1 - backgroundRect.xRatio) / 2;\n backgroundRect.left = offset;\n backgroundRect.right = 1 - offset;\n } else {\n backgroundRect.left = 0;\n backgroundRect.right = 1.0;\n backgroundRect.xRatio = 1;\n backgroundRect.yRatio = componentRatio / imageRatio;\n var offset = (1 - backgroundRect.yRatio) / 2;\n backgroundRect.top = offset;\n backgroundRect.bottom = 1 - offset;\n }\n for (var l = 0; l < self.ctx.labels.length; l++) {\n var label = self.ctx.labels[l];\n var labelLeft = backgroundRect.left*100 + (label.config.x*backgroundRect.xRatio);\n var labelTop = backgroundRect.top*100 + (label.config.y*backgroundRect.yRatio);\n var fontSize = self.ctx.height * backgroundRect.yRatio * label.config.font.size / 100;\n label.element.css('top', labelTop + '%');\n label.element.css('left', labelLeft + '%');\n label.element.css('fontSize', fontSize + 'px');\n if (!label.visible) {\n label.element.css('display', 'block');\n label.visible = true;\n }\n }\n } \n}\n\n\nfunction isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n}\n\nfunction padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n\n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n\n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split('.');\n s = int - strVal[0].length;\n\n for (; i < s; ++i) {\n strVal[0] = '0' + strVal[0];\n }\n\n strVal = (n ? '-' : '') + strVal[0] + '.' + strVal[1];\n }\n\n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n\n for (; i < s; ++i) {\n strVal = '0' + strVal;\n }\n\n strVal = (n ? '-' : '') + strVal;\n }\n\n return strVal;\n}\n\nfunction updateLabels() {\n for (var l = 0; l < self.ctx.labels.length; l++) {\n var label = self.ctx.labels[l];\n var text = label.config.pattern;\n var replaceInfo = label.config.replaceInfo;\n var updated = false;\n for (var v = 0; v < replaceInfo.variables.length; v++) {\n var variableInfo = replaceInfo.variables[v];\n var txtVal = '';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = self.ctx.data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n updated = true;\n } else {\n txtVal = val;\n updated = true;\n }\n }\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n if (updated || !label.htmlSet) {\n label.element.html(text);\n if (!label.htmlSet) {\n label.htmlSet = true;\n }\n }\n }\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n singleEntity: true\n };\n};\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"required\": [\"backgroundImageUrl\"],\n \"properties\": {\n \"backgroundImageUrl\": {\n \"title\": \"Background image\",\n \"type\": \"string\",\n \"default\": \"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==\"\n },\n \"labels\": {\n \"title\": \"Labels\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Label\",\n \"type\": \"object\",\n \"required\": [\"pattern\"],\n \"properties\": {\n \"pattern\": {\n \"title\": \"Pattern ( for ex. 'Text ${keyName} units.' or '${#} units' )\",\n \"type\": \"string\",\n \"default\": \"${#0}\"\n },\n \"x\": {\n \"title\": \"X (Percentage relative to background)\",\n \"type\": \"number\",\n \"default\": 50\n },\n \"y\": {\n \"title\": \"Y (Percentage relative to background)\",\n \"type\": \"number\",\n \"default\": 50\n },\n \"backgroundColor\": {\n \"title\": \"Backround color\",\n \"type\": \"string\",\n \"default\": \"rgba(0,0,0,0)\"\n },\n \"font\": {\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"Roboto\"\n },\n \"size\": {\n \"title\": \"Relative font size (percents)\",\n \"type\": \"number\",\n \"default\": 6\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#fff\"\n }\n }\n }\n }\n }\n }\n }\n },\n \"form\": [\n {\n \"key\": \"backgroundImageUrl\",\n \"type\": \"image\"\n },\n {\n \"key\": \"labels\",\n \"items\": [\n \"labels[].pattern\",\n \"labels[].x\",\n \"labels[].y\",\n {\n \"key\": \"labels[].backgroundColor\",\n \"type\": \"color\"\n },\n \"labels[].font.family\",\n \"labels[].font.size\",\n {\n \"key\": \"labels[].font.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n\n },\n {\n \"key\": \"labels[].font.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"labels[].font.color\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-label-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"var\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"backgroundImageUrl\":\"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==\",\"labels\":[{\"pattern\":\"Value: ${#0:2} units.\",\"x\":20,\"y\":47,\"font\":{\"color\":\"#515151\",\"family\":\"Roboto\",\"size\":6,\"style\":\"normal\",\"weight\":\"500\"}}]},\"title\":\"Label widget\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" } }, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-font.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-font.component.html new file mode 100644 index 0000000000..1bd94abe6a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-font.component.html @@ -0,0 +1,71 @@ + +
+ + widgets.label-widget.font-family + + + + widgets.label-widget.relative-font-size + + + + widgets.label-widget.font-style + + + {{ 'widgets.label-widget.font-style-normal' | translate }} + + + {{ 'widgets.label-widget.font-style-italic' | translate }} + + + {{ 'widgets.label-widget.font-style-oblique' | translate }} + + + + + widgets.label-widget.font-weight + + + {{ 'widgets.label-widget.font-weight-normal' | translate }} + + + {{ 'widgets.label-widget.font-weight-bold' | translate }} + + + {{ 'widgets.label-widget.font-weight-bolder' | translate }} + + + {{ 'widgets.label-widget.font-weight-lighter' | translate }} + + 100 + 200 + 300 + 400 + 500 + 600 + 700 + 800 + 900 + + + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-font.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-font.component.ts new file mode 100644 index 0000000000..e535f835c0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-font.component.ts @@ -0,0 +1,104 @@ +/// +/// Copyright © 2016-2022 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 { Component, forwardRef, HostBinding, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; + +export interface LabelWidgetFont { + family: string; + size: number; + style: 'normal' | 'italic' | 'oblique'; + weight: 'normal' | 'bold' | 'bolder' | 'lighter' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'; + color: string; +} + +@Component({ + selector: 'tb-label-widget-font', + templateUrl: './label-widget-font.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => LabelWidgetFontComponent), + multi: true + } + ] +}) +export class LabelWidgetFontComponent extends PageComponent implements OnInit, ControlValueAccessor { + + @HostBinding('style.display') display = 'block'; + + @Input() + disabled: boolean; + + private modelValue: LabelWidgetFont; + + private propagateChange = null; + + public labelWidgetFontFormGroup: FormGroup; + + constructor(protected store: Store, + private translate: TranslateService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.labelWidgetFontFormGroup = this.fb.group({ + family: [null, []], + size: [null, [Validators.min(1)]], + style: [null, []], + weight: [null, []], + color: [null, []] + }); + this.labelWidgetFontFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.labelWidgetFontFormGroup.disable({emitEvent: false}); + } else { + this.labelWidgetFontFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: LabelWidgetFont): void { + this.modelValue = value; + this.labelWidgetFontFormGroup.patchValue( + value, {emitEvent: false} + ); + } + + private updateModel() { + const value: LabelWidgetFont = this.labelWidgetFontFormGroup.value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-label.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-label.component.html new file mode 100644 index 0000000000..6f067e88f9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-label.component.html @@ -0,0 +1,72 @@ + + + +
+ +
+ {{ labelWidgetLabelFormGroup.get('pattern').value }} +
+
+ + +
+
+ +
+ +
+ + widgets.label-widget.label-pattern + + + {{ 'widgets.label-widget.label-pattern-required' | translate }} + + + +
+ widgets.label-widget.label-position +
+ + widgets.label-widget.x-pos + + + + widgets.label-widget.y-pos + + +
+
+ + +
+ widgets.label-widget.font-settings + +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-label.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-label.component.scss new file mode 100644 index 0000000000..0df6cc0626 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-label.component.scss @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2022 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. + */ +:host { + display: block; + .mat-expansion-panel { + box-shadow: none; + &.label-widget-label { + border: 1px groove rgba(0, 0, 0, .25); + .mat-expansion-panel-header { + padding: 0 24px 0 8px; + &.mat-expanded { + height: 48px; + } + } + } + } +} + +:host ::ng-deep { + .mat-expansion-panel { + &.label-widget-label { + .mat-expansion-panel-body { + padding: 0 8px 8px; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-label.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-label.component.ts new file mode 100644 index 0000000000..65f0cca39c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-label.component.ts @@ -0,0 +1,113 @@ +/// +/// Copyright © 2016-2022 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 { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { LabelWidgetFont } from '@home/components/widget/lib/settings/label-widget-font.component'; + +export interface LabelWidgetLabel { + pattern: string; + x: number; + y: number; + backgroundColor: string; + font: LabelWidgetFont; +} + +@Component({ + selector: 'tb-label-widget-label', + templateUrl: './label-widget-label.component.html', + styleUrls: ['./label-widget-label.component.scss', './widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => LabelWidgetLabelComponent), + multi: true + } + ] +}) +export class LabelWidgetLabelComponent extends PageComponent implements OnInit, ControlValueAccessor { + + @Input() + disabled: boolean; + + @Input() + expanded = false; + + @Output() + removeLabel = new EventEmitter(); + + private modelValue: LabelWidgetLabel; + + private propagateChange = null; + + public labelWidgetLabelFormGroup: FormGroup; + + constructor(protected store: Store, + private translate: TranslateService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.labelWidgetLabelFormGroup = this.fb.group({ + pattern: [null, [Validators.required]], + x: [null, [Validators.min(0), Validators.max(100)]], + y: [null, [Validators.min(0), Validators.max(100)]], + backgroundColor: [null, []], + font: [null, []] + }); + this.labelWidgetLabelFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.labelWidgetLabelFormGroup.disable({emitEvent: false}); + } else { + this.labelWidgetLabelFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: LabelWidgetLabel): void { + this.modelValue = value; + this.labelWidgetLabelFormGroup.patchValue( + value, {emitEvent: false} + ); + } + + private updateModel() { + const value: LabelWidgetLabel = this.labelWidgetLabelFormGroup.value; + this.modelValue = value; + if (this.labelWidgetLabelFormGroup.valid) { + this.propagateChange(this.modelValue); + } else { + this.propagateChange(null); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-settings.component.html new file mode 100644 index 0000000000..55e4a6816d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-settings.component.html @@ -0,0 +1,49 @@ + +
+ + +
+ widgets.label-widget.labels +
+
+
+ + +
+
+
+ widgets.label-widget.no-labels +
+
+ +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-settings.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-settings.component.scss new file mode 100644 index 0000000000..5125ee03e4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-settings.component.scss @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2022 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 '../../../../../../../scss/constants'; + +:host { + .tb-label-widget-labels { + overflow-y: auto; + &.mat-padding { + padding: 8px; + @media #{$mat-gt-sm} { + padding: 16px; + } + } + } + + .tb-prompt{ + margin: 30px 0; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-settings.component.ts new file mode 100644 index 0000000000..0535b55a33 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-settings.component.ts @@ -0,0 +1,92 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { AbstractControl, FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { LabelWidgetLabel } from '@home/components/widget/lib/settings/label-widget-label.component'; + +@Component({ + selector: 'tb-label-widget-settings', + templateUrl: './label-widget-settings.component.html', + styleUrls: ['./label-widget-settings.component.scss', './widget-settings.scss'] +}) +export class LabelWidgetSettingsComponent extends WidgetSettingsComponent { + + labelWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.labelWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + backgroundImageUrl: 'data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==', + labels: [] + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + const labelsControls: Array = []; + if (settings.labels) { + settings.labels.forEach((label) => { + labelsControls.push(this.fb.control(label, [Validators.required])); + }); + } + this.labelWidgetSettingsForm = this.fb.group({ + backgroundImageUrl: [settings.backgroundImageUrl, [Validators.required]], + labels: this.fb.array(labelsControls) + }); + } + + labelsFormArray(): FormArray { + return this.labelWidgetSettingsForm.get('labels') as FormArray; + } + + public trackByLabel(index: number, labelControl: AbstractControl): number { + return index; + } + + public removeLabel(index: number) { + (this.labelWidgetSettingsForm.get('labels') as FormArray).removeAt(index); + } + + public addLabel() { + const label: LabelWidgetLabel = { + pattern: '${#0}', + x: 50, + y: 50, + backgroundColor: 'rgba(0,0,0,0)', + font: { + family: 'Roboto', + size: 6, + style: 'normal', + weight: '500', + color: '#fff' + } + }; + const labelsArray = this.labelWidgetSettingsForm.get('labels') as FormArray; + labelsArray.push(this.fb.control(label, [Validators.required])); + this.labelWidgetSettingsForm.updateValueAndValidity(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.html index f586552d15..3859a17e90 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.html @@ -19,19 +19,17 @@ {{ 'widgets.markdown.use-markdown-text-function' | translate }} -
- - -
-
- - -
+ + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.html index 93d3b8086b..c38207c717 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.html @@ -26,12 +26,11 @@ {{ 'widgets.qr-code.qr-code-text-pattern-required' | translate }} -
- - -
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index 03e04bed31..ef2445f6c2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -32,6 +32,9 @@ import { import { MarkdownWidgetSettingsComponent } from '@home/components/widget/lib/settings/markdown-widget-settings.component'; +import { LabelWidgetFontComponent } from '@home/components/widget/lib/settings/label-widget-font.component'; +import { LabelWidgetLabelComponent } from '@home/components/widget/lib/settings/label-widget-label.component'; +import { LabelWidgetSettingsComponent } from '@home/components/widget/lib/settings/label-widget-settings.component'; @NgModule({ declarations: [ @@ -39,7 +42,10 @@ import { TimeseriesTableWidgetSettingsComponent, TimeseriesTableKeySettingsComponent, TimeseriesTableLatestKeySettingsComponent, - MarkdownWidgetSettingsComponent + MarkdownWidgetSettingsComponent, + LabelWidgetFontComponent, + LabelWidgetLabelComponent, + LabelWidgetSettingsComponent ], imports: [ CommonModule, @@ -51,7 +57,10 @@ import { TimeseriesTableWidgetSettingsComponent, TimeseriesTableKeySettingsComponent, TimeseriesTableLatestKeySettingsComponent, - MarkdownWidgetSettingsComponent + MarkdownWidgetSettingsComponent, + LabelWidgetFontComponent, + LabelWidgetLabelComponent, + LabelWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -62,5 +71,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type${keyName} units.' or ${#<key index>} units'", + "label-pattern-required": "Pattern is required", + "label-position": "Position (Percentage relative to background)", + "x-pos": "X", + "y-pos": "Y", + "background-color": "Background color", + "font-settings": "Font settings", + "background-image": "Background image", + "labels": "Labels", + "no-labels": "No labels configured", + "add-label": "Add label" + }, "persistent-table": { "rpc-id": "RPC ID", "message-type": "Message type", From a6da5644a361322fb5c3e12674f224f991e36f86 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Sat, 2 Apr 2022 17:49:39 +0300 Subject: [PATCH 037/276] UI: Add simple card widget settings form --- .../json/system/widget_bundles/cards.json | 5 +- ...simple-card-widget-settings.component.html | 30 +++++++++++ .../simple-card-widget-settings.component.ts | 52 +++++++++++++++++++ .../lib/settings/widget-settings.module.ts | 12 +++-- .../assets/locale/locale.constant-en_US.json | 5 ++ 5 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/simple-card-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/simple-card-widget-settings.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index 7debb29ff9..f421f48b61 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -95,8 +95,9 @@ "templateHtml": "", "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n\n.tbDatasource-table {\n width: 100%;\n height: 100%;\n border-collapse: collapse;\n white-space: nowrap;\n font-weight: 100;\n text-align: right;\n}\n\n.tbDatasource-table td {\n padding: 12px;\n position: relative;\n box-sizing: border-box;\n}\n\n.tbDatasource-data-key {\n opacity: 0.7;\n font-weight: 400;\n font-size: 3.500rem;\n}\n\n.tbDatasource-value {\n font-size: 5.000rem;\n}", "controllerScript": "self.onInit = function() {\n\n self.ctx.labelPosition = self.ctx.settings.labelPosition || 'left';\n \n if (self.ctx.datasources.length > 0) {\n var tbDatasource = self.ctx.datasources[0];\n var datasourceId = 'tbDatasource' + 0;\n self.ctx.$container.append(\n \"
\"\n );\n \n self.ctx.datasourceContainer = $('#' + datasourceId,\n self.ctx.$container);\n \n var tableId = 'table' + 0;\n self.ctx.datasourceContainer.append(\n \"
\"\n );\n var table = $('#' + tableId, self.ctx.$container);\n if (self.ctx.labelPosition === 'top') {\n table.css('text-align', 'left');\n }\n \n if (tbDatasource.dataKeys.length > 0) {\n var dataKey = tbDatasource.dataKeys[0];\n var labelCellId = 'labelCell' + 0;\n var cellId = 'cell' + 0;\n if (self.ctx.labelPosition === 'left') {\n table.append(\n \"\" +\n dataKey.label +\n \"\");\n } else {\n table.append(\n \"\" +\n dataKey.label +\n \"\");\n }\n self.ctx.labelCell = $('#' + labelCellId, table);\n self.ctx.valueCell = $('#' + cellId, table);\n self.ctx.valueCell.html(0 + ' ' + self.ctx.units);\n }\n }\n \n $.fn.textWidth = function(){\n var html_org = $(this).html();\n var html_calc = '' + html_org + '';\n $(this).html(html_calc);\n var width = $(this).find('span:first').width();\n $(this).html(html_org);\n return width;\n }; \n \n self.onResize();\n};\n\nself.onDataUpdated = function() {\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n\n if (self.ctx.valueCell && self.ctx.data.length > 0) {\n var cellData = self.ctx.data[0];\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length -\n 1];\n var value = tvPair[1];\n var txtValue;\n if (isNumber(value)) {\n var decimals = self.ctx.decimals;\n var units = self.ctx.units;\n if (self.ctx.datasources.length > 0 && self.ctx.datasources[0].dataKeys.length > 0) {\n dataKey = self.ctx.datasources[0].dataKeys[0];\n if (dataKey.decimals || dataKey.decimals === 0) {\n decimals = dataKey.decimals;\n }\n if (dataKey.units) {\n units = dataKey.units;\n }\n }\n txtValue = self.ctx.utils.formatValue(value, decimals, units, true);\n } else {\n txtValue = value;\n }\n self.ctx.valueCell.html(txtValue);\n var targetWidth;\n var minDelta;\n if (self.ctx.labelPosition === 'left') {\n targetWidth = self.ctx.datasourceContainer.width() - self.ctx.labelCell.width();\n minDelta = self.ctx.width/16 + self.ctx.padding;\n } else {\n targetWidth = self.ctx.datasourceContainer.width();\n minDelta = self.ctx.padding;\n }\n var delta = targetWidth - self.ctx.valueCell.textWidth();\n var fontSize = self.ctx.valueFontSize;\n if (targetWidth > minDelta) {\n while (delta < minDelta && fontSize > 6) {\n fontSize--;\n self.ctx.valueCell.css('font-size', fontSize+'px');\n delta = targetWidth - self.ctx.valueCell.textWidth();\n }\n }\n }\n } \n \n};\n\nself.onResize = function() {\n var labelFontSize;\n if (self.ctx.labelPosition === 'top') {\n self.ctx.padding = self.ctx.height/20;\n labelFontSize = self.ctx.height/4;\n self.ctx.valueFontSize = self.ctx.height/2;\n } else {\n self.ctx.padding = self.ctx.width/50;\n labelFontSize = self.ctx.height/2.5;\n self.ctx.valueFontSize = self.ctx.height/2;\n if (self.ctx.width/self.ctx.height <= 2.7) {\n labelFontSize = self.ctx.width/7;\n self.ctx.valueFontSize = self.ctx.width/6;\n }\n }\n self.ctx.padding = Math.min(12, self.ctx.padding);\n \n if (self.ctx.labelCell) {\n self.ctx.labelCell.css('font-size', labelFontSize+'px');\n self.ctx.labelCell.css('padding', self.ctx.padding+'px');\n }\n if (self.ctx.valueCell) {\n self.ctx.valueCell.css('font-size', self.ctx.valueFontSize+'px');\n self.ctx.valueCell.css('padding', self.ctx.padding+'px');\n } \n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n};\n\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"labelPosition\": {\n \"title\": \"Label position\",\n \"type\": \"string\",\n \"default\": \"left\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"labelPosition\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"left\",\n \"label\": \"Left\"\n },\n {\n \"value\": \"top\",\n \"label\": \"Top\"\n }\n ]\n }\n ]\n}", - "dataKeySettingsSchema": "{}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-simple-card-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ff5722\",\"color\":\"rgba(255, 255, 255, 0.87)\",\"padding\":\"16px\",\"settings\":{\"labelPosition\":\"top\"},\"title\":\"Simple card\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" } }, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/simple-card-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/simple-card-widget-settings.component.html new file mode 100644 index 0000000000..5fe97385c7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/simple-card-widget-settings.component.html @@ -0,0 +1,30 @@ + +
+ + widgets.simple-card.label-position + + + {{ 'widgets.simple-card.label-position-left' | translate }} + + + {{ 'widgets.simple-card.label-position-top' | translate }} + + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/simple-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/simple-card-widget-settings.component.ts new file mode 100644 index 0000000000..d690c4f434 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/simple-card-widget-settings.component.ts @@ -0,0 +1,52 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-simple-card-widget-settings', + templateUrl: './simple-card-widget-settings.component.html', + styleUrls: [] +}) +export class SimpleCardWidgetSettingsComponent extends WidgetSettingsComponent { + + simpleCardWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.simpleCardWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + labelPosition: 'left' + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.simpleCardWidgetSettingsForm = this.fb.group({ + labelPosition: [settings.labelPosition, []] + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index ef2445f6c2..577738dbdb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -35,6 +35,9 @@ import { import { LabelWidgetFontComponent } from '@home/components/widget/lib/settings/label-widget-font.component'; import { LabelWidgetLabelComponent } from '@home/components/widget/lib/settings/label-widget-label.component'; import { LabelWidgetSettingsComponent } from '@home/components/widget/lib/settings/label-widget-settings.component'; +import { + SimpleCardWidgetSettingsComponent +} from '@home/components/widget/lib/settings/simple-card-widget-settings.component'; @NgModule({ declarations: [ @@ -45,7 +48,8 @@ import { LabelWidgetSettingsComponent } from '@home/components/widget/lib/settin MarkdownWidgetSettingsComponent, LabelWidgetFontComponent, LabelWidgetLabelComponent, - LabelWidgetSettingsComponent + LabelWidgetSettingsComponent, + SimpleCardWidgetSettingsComponent ], imports: [ CommonModule, @@ -60,7 +64,8 @@ import { LabelWidgetSettingsComponent } from '@home/components/widget/lib/settin MarkdownWidgetSettingsComponent, LabelWidgetFontComponent, LabelWidgetLabelComponent, - LabelWidgetSettingsComponent + LabelWidgetSettingsComponent, + SimpleCardWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -72,5 +77,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type Date: Sat, 2 Apr 2022 18:27:01 +0300 Subject: [PATCH 038/276] UI: Add dashboard state widget settings form --- .../json/system/widget_bundles/cards.json | 5 +- ...board-state-widget-settings.component.html | 64 ++++++++++++ ...shboard-state-widget-settings.component.ts | 99 +++++++++++++++++++ .../lib/settings/widget-settings.module.ts | 12 ++- .../assets/locale/locale.constant-en_US.json | 8 ++ 5 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/dashboard-state-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/dashboard-state-widget-settings.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index f421f48b61..05a008080e 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -207,8 +207,9 @@ "templateHtml": "\n\n
\n
Dashboard state widget
\n
(Specify dashboard state id in the advanced widget settings)
\n
\n", "templateCss": ".dashboard-state-widget-prompt {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n font-size: 32px;\n font-weight: 500;\n color: #999;\n}\n\n.dashboard-state-widget-prompt .title {\n font-size: 32px;\n font-weight: 500;\n color: #999;\n}\n\n.dashboard-state-widget-prompt .subtitle {\n font-size: 24px;\n font-weight: normal;\n color: #999;\n text-align: center;\n}", "controllerScript": "self.onInit = function() {\n var $injector = self.ctx.$scope.$injector;\n self.ctx.$scope.stateId = self.ctx.settings.stateId || \"\";\n self.ctx.$scope.defaultAutofillLayout = self.ctx.settings.defaultAutofillLayout;\n self.ctx.$scope.defaultMargin = self.ctx.settings.defaultMargin;\n self.ctx.$scope.defaultBackgroundColor = self.ctx.settings.defaultBackgroundColor;\n self.ctx.$scope.syncParentStateParams = self.ctx.settings.syncParentStateParams !== false;\n}\n\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"required\": [],\n \"properties\": {\n \"stateId\": {\n \"title\": \"Dashboard state id\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"defaultAutofillLayout\": {\n \"title\": \"Autofill state layout height by default\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultMargin\": {\n \"title\": \"Default widgets margin\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"defaultBackgroundColor\": {\n \"title\": \"Default background color\",\n \"type\": \"string\",\n \"default\": \"#fff\"\n },\n \"syncParentStateParams\": {\n \"title\": \"Sync state params with parent dashboard\",\n \"type\": \"boolean\",\n \"default\": true\n }\n }\n },\n \"form\": [\n \"stateId\",\n \"defaultAutofillLayout\",\n \"defaultMargin\",\n {\n \"key\": \"defaultBackgroundColor\",\n \"type\": \"color\"\n },\n \"syncParentStateParams\"\n ]\n}", - "dataKeySettingsSchema": "{}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-dashboard-state-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"syncParentStateParams\":true,\"defaultAutofillLayout\":true,\"defaultMargin\":0,\"defaultBackgroundColor\":\"#fff\"},\"title\":\"Dashboard state widget\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\",\"showLegend\":false}" } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/dashboard-state-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/dashboard-state-widget-settings.component.html new file mode 100644 index 0000000000..a17d3a7a35 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/dashboard-state-widget-settings.component.html @@ -0,0 +1,64 @@ + +
+
+ widgets.dashboard-state.dashboard-state-settings + + + + + + + + + + + + + widget-config.advanced-settings + + + + + {{ 'widgets.dashboard-state.autofill-state-layout' | translate }} + + + widgets.dashboard-state.default-margin + + + + + + {{ 'widgets.dashboard-state.sync-parent-state-params' | translate }} + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/dashboard-state-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/dashboard-state-widget-settings.component.ts new file mode 100644 index 0000000000..7d2a11e9cc --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/dashboard-state-widget-settings.component.ts @@ -0,0 +1,99 @@ +/// +/// Copyright © 2016-2022 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 { Component, ElementRef, ViewChild } from '@angular/core'; +import { WidgetActionType, WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Observable, of } from 'rxjs'; +import { map, mergeMap, startWith } from 'rxjs/operators'; + +@Component({ + selector: 'tb-dashboard-state-widget-settings', + templateUrl: './dashboard-state-widget-settings.component.html', + styleUrls: ['./widget-settings.scss'] +}) +export class DashboardStateWidgetSettingsComponent extends WidgetSettingsComponent { + + @ViewChild('dashboardStateInput') dashboardStateInput: ElementRef; + + dashboardStateWidgetSettingsForm: FormGroup; + + filteredDashboardStates: Observable>; + dashboardStateSearchText = ''; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.dashboardStateWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + stateId: '', + defaultAutofillLayout: true, + defaultMargin: 0, + defaultBackgroundColor: '#fff', + syncParentStateParams: true + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.dashboardStateWidgetSettingsForm = this.fb.group({ + stateId: [settings.stateId, []], + defaultAutofillLayout: [settings.defaultAutofillLayout, []], + defaultMargin: [settings.defaultMargin, [Validators.min(0)]], + defaultBackgroundColor: [settings.defaultBackgroundColor, []], + syncParentStateParams: [settings.syncParentStateParams, []] + }); + this.dashboardStateSearchText = ''; + this.filteredDashboardStates = this.dashboardStateWidgetSettingsForm.get('stateId').valueChanges + .pipe( + startWith(''), + map(value => value ? value : ''), + mergeMap(name => this.fetchDashboardStates(name) ) + ); + } + + public clearDashboardState(value: string = '') { + this.dashboardStateInput.nativeElement.value = value; + this.dashboardStateWidgetSettingsForm.get('stateId').patchValue(value, {emitEvent: true}); + setTimeout(() => { + this.dashboardStateInput.nativeElement.blur(); + this.dashboardStateInput.nativeElement.focus(); + }, 0); + } + + private fetchDashboardStates(searchText?: string): Observable> { + this.dashboardStateSearchText = searchText; + const stateIds = Object.keys(this.dashboard.configuration.states); + const result = searchText ? stateIds.filter(this.createFilterForDashboardState(searchText)) : stateIds; + if (result && result.length) { + return of(result); + } else { + return of([searchText]); + } + } + + private createFilterForDashboardState(query: string): (stateId: string) => boolean { + const lowercaseQuery = query.toLowerCase(); + return stateId => stateId.toLowerCase().indexOf(lowercaseQuery) === 0; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index 577738dbdb..13510dcd93 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -38,6 +38,9 @@ import { LabelWidgetSettingsComponent } from '@home/components/widget/lib/settin import { SimpleCardWidgetSettingsComponent } from '@home/components/widget/lib/settings/simple-card-widget-settings.component'; +import { + DashboardStateWidgetSettingsComponent +} from '@home/components/widget/lib/settings/dashboard-state-widget-settings.component'; @NgModule({ declarations: [ @@ -49,7 +52,8 @@ import { LabelWidgetFontComponent, LabelWidgetLabelComponent, LabelWidgetSettingsComponent, - SimpleCardWidgetSettingsComponent + SimpleCardWidgetSettingsComponent, + DashboardStateWidgetSettingsComponent ], imports: [ CommonModule, @@ -65,7 +69,8 @@ import { LabelWidgetFontComponent, LabelWidgetLabelComponent, LabelWidgetSettingsComponent, - SimpleCardWidgetSettingsComponent + SimpleCardWidgetSettingsComponent, + DashboardStateWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -78,5 +83,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type Date: Sat, 2 Apr 2022 18:54:48 +0300 Subject: [PATCH 039/276] UI: Add entities hierarchy widget settings form --- .../json/system/widget_bundles/cards.json | 7 +- ...s-hierarchy-widget-settings.component.html | 74 +++++++++++++++++++ ...ies-hierarchy-widget-settings.component.ts | 64 ++++++++++++++++ .../lib/settings/widget-settings.module.ts | 12 ++- .../assets/locale/locale.constant-en_US.json | 13 ++++ 5 files changed, 164 insertions(+), 6 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-hierarchy-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-hierarchy-widget-settings.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index 05a008080e..aa7c991d52 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -151,9 +151,10 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesHierarchyWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'nodeSelected': {\n name: 'widget-action.node-selected',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesHierarchySettings\",\n \"properties\": {\n \"nodeRelationQueryFunction\": {\n \"title\": \"Node relations query function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeHasChildrenFunction\": {\n \"title\": \"Node has children function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeOpenedFunction\": {\n \"title\": \"Default node opened function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeDisabledFunction\": {\n \"title\": \"Node disabled function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeIconFunction\": {\n \"title\": \"Node icon function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeTextFunction\": {\n \"title\": \"Node text function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodesSortFunction\": {\n \"title\": \"Nodes sort function: f(nodeCtx1, nodeCtx2)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"nodeRelationQueryFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entities_hierarchy/node_relation_query_fn\"\n },\n {\n \"key\": \"nodeHasChildrenFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entities_hierarchy/node_has_children_fn\"\n },\n {\n \"key\": \"nodeOpenedFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entities_hierarchy/node_opened_fn\"\n },\n {\n \"key\": \"nodeDisabledFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entities_hierarchy/node_disabled_fn\"\n },\n {\n \"key\": \"nodeIconFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entities_hierarchy/node_icon_fn\"\n },\n {\n \"key\": \"nodeTextFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entities_hierarchy/node_text_fn\"\n },\n {\n \"key\": \"nodesSortFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entities_hierarchy/nodes_sort_fn\"\n }\n ]\n}", - "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {},\n \"required\": []\n },\n \"form\": []\n}", - "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"nodeRelationQueryFunction\":\"/**\\n\\n// Function should return relations query object for current node used to fetch entity children.\\n// Function can return 'default' string value. In this case default relations query will be used.\\n\\n// The following example code will construct simple relations query that will fetch relations of type 'Contains'\\n// from the current entity.\\n\\nvar entity = nodeCtx.entity;\\nvar query = {\\n parameters: {\\n rootId: entity.id.id,\\n rootType: entity.id.entityType,\\n direction: \\\"FROM\\\",\\n maxLevel: 1\\n },\\n filters: [{\\n relationType: \\\"Contains\\\",\\n entityTypes: []\\n }]\\n};\\nreturn query;\\n\\n**/\\n\",\"nodeHasChildrenFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node has children (whether it can be expanded).\\n\\n// The following example code will restrict entities hierarchy expansion up to third level.\\n\\nreturn nodeCtx.level <= 2;\\n\\n// The next example code will restrict entities expansion according to the value of example 'nodeHasChildren' attribute.\\n\\nvar data = nodeCtx.data;\\nif (data.hasOwnProperty('nodeHasChildren') && data['nodeHasChildren'] !== null) {\\n return data['nodeHasChildren'] === 'true';\\n} else {\\n return true;\\n}\\n \\n**/\\n \",\"nodeTextFunction\":\"/**\\n\\n// Function should return text (can be HTML code) for the current node.\\n\\n// The following example code will generate node text consisting of entity name and temperature if temperature value is present in entity attributes/timeseries.\\n\\nvar data = nodeCtx.data;\\nvar entity = nodeCtx.entity;\\nvar text = entity.name;\\nif (data.hasOwnProperty('temperature') && data['temperature'] !== null) {\\n text += \\\" \\\"+ data['temperature'] +\\\" °C\\\";\\n}\\nreturn text;\\n\\n**/\",\"nodeIconFunction\":\"/** \\n\\n// Function should return node icon info object.\\n// Resulting object should contain either 'materialIcon' or 'iconUrl' property. \\n// Where:\\n - 'materialIcon' - name of the material icon to be used from the Material Icons Library (https://material.io/tools/icons);\\n - 'iconUrl' - url of the external image to be used as node icon.\\n// Function can return 'default' string value. In this case default icons according to entity type will be used.\\n\\n// The following example code shows how to use external image for devices which name starts with 'Test' and use \\n// default icons for the rest of entities.\\n\\nvar entity = nodeCtx.entity;\\nif (entity.id.entityType === 'DEVICE' && entity.name.startsWith('Test')) {\\n return {iconUrl: 'https://avatars1.githubusercontent.com/u/14793288?v=4&s=117'};\\n} else {\\n return 'default';\\n}\\n \\n**/\",\"nodeDisabledFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node should be disabled (not selectable).\\n\\n// The following example code will disable current node according to the value of example 'nodeDisabled' attribute.\\n\\nvar data = nodeCtx.data;\\nif (data.hasOwnProperty('nodeDisabled') && data['nodeDisabled'] !== null) {\\n return data['nodeDisabled'] === 'true';\\n} else {\\n return false;\\n}\\n \\n**/\\n\",\"nodesSortFunction\":\"/**\\n\\n// This function is used to sort nodes of the same level. Function should compare two nodes and return \\n// integer value: \\n// - less than 0 - sort nodeCtx1 to an index lower than nodeCtx2\\n// - 0 - leave nodeCtx1 and nodeCtx2 unchanged with respect to each other\\n// - greater than 0 - sort nodeCtx2 to an index lower than nodeCtx1\\n\\n// The following example code will sort entities first by entity type in alphabetical order then\\n// by entity name in alphabetical order.\\n\\nvar result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType);\\nif (result === 0) {\\n result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name);\\n}\\nreturn result;\\n \\n**/\",\"nodeOpenedFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node should be opened (expanded) when it first loaded.\\n\\n// The following example code will open by default nodes up to third level.\\n\\nreturn nodeCtx.level <= 2;\\n\\n**/\\n \"},\"title\":\"Entities hierarchy\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"widgetStyle\":{},\"actions\":{}}" + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-entities-hierarchy-widget-settings", + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"nodeRelationQueryFunction\":\"var entity = nodeCtx.entity;\\nvar query = {\\n parameters: {\\n rootId: entity.id.id,\\n rootType: entity.id.entityType,\\n direction: \\\"FROM\\\",\\n maxLevel: 1\\n },\\n filters: [{\\n relationType: \\\"Contains\\\",\\n entityTypes: []\\n }]\\n};\\nreturn query;\\n\\n/**\\n\\n// Function should return relations query object for current node used to fetch entity children.\\n// Function can return 'default' string value. In this case default relations query will be used.\\n\\n// The following example code will construct simple relations query that will fetch relations of type 'Contains'\\n// from the current entity.\\n\\nvar entity = nodeCtx.entity;\\nvar query = {\\n parameters: {\\n rootId: entity.id.id,\\n rootType: entity.id.entityType,\\n direction: \\\"FROM\\\",\\n maxLevel: 1\\n },\\n filters: [{\\n relationType: \\\"Contains\\\",\\n entityTypes: []\\n }]\\n};\\nreturn query;\\n\\n**/\\n\",\"nodeHasChildrenFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node has children (whether it can be expanded).\\n\\n// The following example code will restrict entities hierarchy expansion up to third level.\\n\\nreturn nodeCtx.level <= 2;\\n\\n// The next example code will restrict entities expansion according to the value of example 'nodeHasChildren' attribute.\\n\\nvar data = nodeCtx.data;\\nif (data.hasOwnProperty('nodeHasChildren') && data['nodeHasChildren'] !== null) {\\n return data['nodeHasChildren'] === 'true';\\n} else {\\n return true;\\n}\\n \\n**/\\n \",\"nodeOpenedFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node should be opened (expanded) when it first loaded.\\n\\n// The following example code will open by default nodes up to third level.\\n\\nreturn nodeCtx.level <= 2;\\n\\n**/\\n \",\"nodeDisabledFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node should be disabled (not selectable).\\n\\n// The following example code will disable current node according to the value of example 'nodeDisabled' attribute.\\n\\nvar data = nodeCtx.data;\\nif (data.hasOwnProperty('nodeDisabled') && data['nodeDisabled'] !== null) {\\n return data['nodeDisabled'] === 'true';\\n} else {\\n return false;\\n}\\n \\n**/\\n\",\"nodeIconFunction\":\"/** \\n\\n// Function should return node icon info object.\\n// Resulting object should contain either 'materialIcon' or 'iconUrl' property. \\n// Where:\\n - 'materialIcon' - name of the material icon to be used from the Material Icons Library (https://material.io/tools/icons);\\n - 'iconUrl' - url of the external image to be used as node icon.\\n// Function can return 'default' string value. In this case default icons according to entity type will be used.\\n\\n// The following example code shows how to use external image for devices which name starts with 'Test' and use \\n// default icons for the rest of entities.\\n\\nvar entity = nodeCtx.entity;\\nif (entity.id.entityType === 'DEVICE' && entity.name.startsWith('Test')) {\\n return {iconUrl: 'https://avatars1.githubusercontent.com/u/14793288?v=4&s=117'};\\n} else {\\n return 'default';\\n}\\n \\n**/\",\"nodeTextFunction\":\"/**\\n\\n// Function should return text (can be HTML code) for the current node.\\n\\n// The following example code will generate node text consisting of entity name and temperature if temperature value is present in entity attributes/timeseries.\\n\\nvar data = nodeCtx.data;\\nvar entity = nodeCtx.entity;\\nvar text = entity.name;\\nif (data.hasOwnProperty('temperature') && data['temperature'] !== null) {\\n text += \\\" \\\"+ data['temperature'] +\\\" °C\\\";\\n}\\nreturn text;\\n\\n**/\",\"nodesSortFunction\":\"/**\\n\\n// This function is used to sort nodes of the same level. Function should compare two nodes and return \\n// integer value: \\n// - less than 0 - sort nodeCtx1 to an index lower than nodeCtx2\\n// - 0 - leave nodeCtx1 and nodeCtx2 unchanged with respect to each other\\n// - greater than 0 - sort nodeCtx2 to an index lower than nodeCtx1\\n\\n// The following example code will sort entities first by entity type in alphabetical order then\\n// by entity name in alphabetical order.\\n\\nvar result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType);\\nif (result === 0) {\\n result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name);\\n}\\nreturn result;\\n \\n**/\"},\"title\":\"Entities hierarchy\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"widgetStyle\":{},\"actions\":{}}" } }, { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-hierarchy-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-hierarchy-widget-settings.component.html new file mode 100644 index 0000000000..2a2f16d581 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-hierarchy-widget-settings.component.html @@ -0,0 +1,74 @@ + +
+
+ widgets.entities-hierarchy.hierarchy-data-settings + + + + +
+
+ widgets.entities-hierarchy.node-state-settings + + + + +
+
+ widgets.entities-hierarchy.display-settings + + + + +
+
+ widgets.entities-hierarchy.sort-settings + + +
+
+ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-hierarchy-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-hierarchy-widget-settings.component.ts new file mode 100644 index 0000000000..c0757b4904 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-hierarchy-widget-settings.component.ts @@ -0,0 +1,64 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-entities-hierarchy-widget-settings', + templateUrl: './entities-hierarchy-widget-settings.component.html', + styleUrls: ['./widget-settings.scss'] +}) +export class EntitiesHierarchyWidgetSettingsComponent extends WidgetSettingsComponent { + + entitiesHierarchyWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.entitiesHierarchyWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + nodeRelationQueryFunction: '', + nodeHasChildrenFunction: '', + nodeOpenedFunction: '', + nodeDisabledFunction: '', + nodeIconFunction: '', + nodeTextFunction: '', + nodesSortFunction: '', + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.entitiesHierarchyWidgetSettingsForm = this.fb.group({ + nodeRelationQueryFunction: [settings.nodeRelationQueryFunction, []], + nodeHasChildrenFunction: [settings.nodeHasChildrenFunction, []], + nodeOpenedFunction: [settings.nodeOpenedFunction, []], + nodeDisabledFunction: [settings.nodeDisabledFunction, []], + nodeIconFunction: [settings.nodeIconFunction, []], + nodeTextFunction: [settings.nodeTextFunction, []], + nodesSortFunction: [settings.nodesSortFunction, []] + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index 13510dcd93..009e0fd3f7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -41,6 +41,9 @@ import { import { DashboardStateWidgetSettingsComponent } from '@home/components/widget/lib/settings/dashboard-state-widget-settings.component'; +import { + EntitiesHierarchyWidgetSettingsComponent +} from '@home/components/widget/lib/settings/entities-hierarchy-widget-settings.component'; @NgModule({ declarations: [ @@ -53,7 +56,8 @@ import { LabelWidgetLabelComponent, LabelWidgetSettingsComponent, SimpleCardWidgetSettingsComponent, - DashboardStateWidgetSettingsComponent + DashboardStateWidgetSettingsComponent, + EntitiesHierarchyWidgetSettingsComponent ], imports: [ CommonModule, @@ -70,7 +74,8 @@ import { LabelWidgetLabelComponent, LabelWidgetSettingsComponent, SimpleCardWidgetSettingsComponent, - DashboardStateWidgetSettingsComponent + DashboardStateWidgetSettingsComponent, + EntitiesHierarchyWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -84,5 +89,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type Date: Mon, 4 Apr 2022 13:14:41 +0300 Subject: [PATCH 040/276] UI: Add HTML card/HTML value card widget settings form --- .../json/system/widget_bundles/cards.json | 10 +- .../html-card-widget-settings.component.html | 25 +++ .../html-card-widget-settings.component.ts | 54 +++++ .../qrcode-widget-settings.component.html | 3 +- .../qrcode-widget-settings.component.ts | 2 +- .../lib/settings/widget-settings.module.ts | 12 +- .../app/shared/components/css.component.html | 2 +- .../app/shared/components/css.component.ts | 6 +- .../app/shared/components/html.component.html | 41 ++++ .../app/shared/components/html.component.scss | 65 ++++++ .../app/shared/components/html.component.ts | 211 ++++++++++++++++++ .../shared/components/js-func.component.html | 8 +- .../shared/components/js-func.component.ts | 9 +- .../components/markdown-editor.component.html | 2 +- ui-ngx/src/app/shared/shared.module.ts | 3 + .../assets/locale/locale.constant-en_US.json | 4 + 16 files changed, 440 insertions(+), 17 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/html-card-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/html-card-widget-settings.component.ts create mode 100644 ui-ngx/src/app/shared/components/html.component.html create mode 100644 ui-ngx/src/app/shared/components/html.component.scss create mode 100644 ui-ngx/src/app/shared/components/html.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index aa7c991d52..7a83eb1fe1 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -37,8 +37,9 @@ "templateHtml": "", "templateCss": "", "controllerScript": "self.onInit = function() {\n var $injector = self.ctx.$scope.$injector;\n var utils = $injector.get(self.ctx.servicesMap.get('utils'));\n\n var cssParser = new cssjs();\n cssParser.testMode = false;\n var namespace = 'html-card-' + hashCode(self.ctx.settings.cardCss);\n cssParser.cssPreviewNamespace = namespace;\n cssParser.createStyleElement(namespace, self.ctx.settings.cardCss);\n self.ctx.$container.addClass(namespace);\n var evtFnPrefix = 'htmlCard_' + Math.abs(hashCode(self.ctx.settings.cardCss + self.ctx.settings.cardHtml + self.ctx.widget.id));\n cardHtml = '
' + \n self.ctx.settings.cardHtml + \n '
';\n cardHtml = replaceCustomTranslations(cardHtml);\n self.ctx.$container.html(cardHtml);\n\n window[evtFnPrefix + '_onClickFn'] = function (event) {\n self.ctx.actionsApi.elementClick(event);\n }\n\n function hashCode(str) {\n var hash = 0;\n var i, char;\n if (str.length === 0) return hash;\n for (i = 0; i < str.length; i++) {\n char = str.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash = hash & hash;\n }\n return hash;\n }\n \n function replaceCustomTranslations (pattern) {\n var customTranslationRegex = new RegExp('{i18n:[^{}]+}', 'g');\n pattern = pattern.replace(customTranslationRegex, getTranslationText);\n return pattern;\n }\n \n function getTranslationText (variable) {\n return utils.customTranslation(variable, variable);\n \n }\n}\n\nself.actionSources = function() {\n return {\n 'elementClick': {\n name: 'widget-action.element-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"required\": [\"cardHtml\"],\n \"properties\": {\n \"cardCss\": {\n \"title\": \"CSS\",\n \"type\": \"string\",\n \"default\": \".card {\\n font-weight: bold; \\n}\"\n },\n \"cardHtml\": {\n \"title\": \"HTML\",\n \"type\": \"string\",\n \"default\": \"
HTML code here
\"\n }\n }\n },\n \"form\": [\n {\n \"key\": \"cardCss\",\n \"type\": \"css\"\n }, \n {\n \"key\": \"cardHtml\",\n \"type\": \"html\"\n } \n ]\n}", - "dataKeySettingsSchema": "{}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-html-card-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"cardHtml\":\"
HTML code here
\",\"cardCss\":\".card {\\n font-weight: bold;\\n font-size: 32px;\\n color: #999;\\n width: 100%;\\n height: 100%;\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n}\"},\"title\":\"HTML Card\",\"dropShadow\":true}" } }, @@ -77,8 +78,9 @@ "templateHtml": "", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.varsRegex = /\\$\\{([^\\}]*)\\}/g;\n self.ctx.htmlSet = false;\n \n var cssParser = new cssjs();\n cssParser.testMode = false;\n var namespace = 'html-value-card-' + hashCode(self.ctx.settings.cardCss);\n cssParser.cssPreviewNamespace = namespace;\n cssParser.createStyleElement(namespace, self.ctx.settings.cardCss);\n self.ctx.$container.addClass(namespace);\n var evtFnPrefix = 'htmlValueCard_' + Math.abs(hashCode(self.ctx.settings.cardCss + self.ctx.settings.cardHtml + self.ctx.widget.id));\n self.ctx.html = '
' + \n self.ctx.settings.cardHtml + \n '
';\n\n self.ctx.replaceInfo = processHtmlPattern(self.ctx.html, self.ctx.data);\n \n updateHtml();\n \n window[evtFnPrefix + '_onClickFn'] = function (event) {\n self.ctx.actionsApi.elementClick(event);\n }\n\n function hashCode(str) {\n var hash = 0;\n var i, char;\n if (str.length === 0) return hash;\n for (i = 0; i < str.length; i++) {\n char = str.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash = hash & hash;\n }\n return hash;\n }\n \n function processHtmlPattern(pattern, data) {\n var match = self.ctx.varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split(':');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n if (label == 'entityName') {\n variableInfo.isEntityName = true;\n } else if (label == 'entityLabel') {\n variableInfo.isEntityLabel = true;\n } else if (label.startsWith('#')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = n;\n }\n }\n if (!variableInfo.isEntityName && !variableInfo.isEntityLabel && variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = self.ctx.varsRegex.exec(pattern);\n }\n return replaceInfo;\n } \n}\n\nself.onDataUpdated = function() {\n updateHtml();\n}\n\nself.actionSources = function() {\n return {\n 'elementClick': {\n name: 'widget-action.element-click',\n multiple: true\n }\n };\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n singleEntity: true,\n dataKeysOptional: true\n };\n}\n\n\nself.onDestroy = function() {\n}\n\nfunction isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n}\n\nfunction padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n\n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n\n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split('.');\n s = int - strVal[0].length;\n\n for (; i < s; ++i) {\n strVal[0] = '0' + strVal[0];\n }\n\n strVal = (n ? '-' : '') + strVal[0] + '.' + strVal[1];\n }\n\n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n\n for (; i < s; ++i) {\n strVal = '0' + strVal;\n }\n\n strVal = (n ? '-' : '') + strVal;\n }\n\n return strVal;\n}\n\nfunction updateHtml() {\n var $injector = self.ctx.$scope.$injector;\n var utils = $injector.get(self.ctx.servicesMap.get('utils'));\n var text = self.ctx.html;\n var updated = false;\n for (var v in self.ctx.replaceInfo.variables) {\n var variableInfo = self.ctx.replaceInfo.variables[v];\n var txtVal = '';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = self.ctx.data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n } else {\n txtVal = val;\n }\n }\n } else if (variableInfo.isEntityName) {\n if (self.ctx.defaultSubscription.datasources.length) {\n txtVal = self.ctx.defaultSubscription.datasources[0].entityName;\n } else {\n txtVal = 'Unknown';\n }\n } else if (variableInfo.isEntityLabel) {\n if (self.ctx.defaultSubscription.datasources.length) {\n txtVal = self.ctx.defaultSubscription.datasources[0].entityLabel || self.ctx.defaultSubscription.datasources[0].entityName;\n } else {\n txtVal = 'Unknown';\n }\n }\n if (typeof variableInfo.lastVal === undefined ||\n variableInfo.lastVal !== txtVal) {\n updated = true;\n variableInfo.lastVal = txtVal;\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n if (updated || !self.ctx.htmlSet) {\n text = replaceCustomTranslations(text);\n self.ctx.$container.html(text);\n if (!self.ctx.htmlSet) {\n self.ctx.htmlSet = true;\n }\n }\n \n function replaceCustomTranslations (pattern) {\n var customTranslationRegex = new RegExp('{i18n:[^{}]+}', 'g');\n pattern = pattern.replace(customTranslationRegex, getTranslationText);\n return pattern;\n }\n \n function getTranslationText (variable) {\n return utils.customTranslation(variable, variable);\n \n }\n}\n\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"required\": [\"cardHtml\"],\n \"properties\": {\n \"cardCss\": {\n \"title\": \"CSS\",\n \"type\": \"string\",\n \"default\": \".card {\\n font-weight: bold; \\n}\"\n },\n \"cardHtml\": {\n \"title\": \"HTML\",\n \"type\": \"string\",\n \"default\": \"
HTML code here
\"\n }\n }\n },\n \"form\": [\n {\n \"key\": \"cardCss\",\n \"type\": \"css\"\n }, \n {\n \"key\": \"cardHtml\",\n \"type\": \"html\"\n } \n ]\n}", - "dataKeySettingsSchema": "{}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-html-card-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"My value\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.random() * 5.45;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"cardCss\":\".card {\\n width: 100%;\\n height: 100%;\\n border: 2px solid #ccc;\\n box-sizing: border-box;\\n}\\n\\n.card .content {\\n padding: 20px;\\n display: flex;\\n flex-direction: row;\\n align-items: center;\\n justify-content: space-around;\\n height: 100%;\\n box-sizing: border-box;\\n}\\n\\n.card .content .column {\\n display: flex;\\n flex-direction: column; \\n justify-content: space-around;\\n height: 100%;\\n}\\n\\n.card h1 {\\n text-transform: uppercase;\\n color: #999;\\n font-size: 20px;\\n font-weight: bold;\\n margin: 0;\\n padding-bottom: 10px;\\n line-height: 32px;\\n}\\n\\n.card .value {\\n font-size: 38px;\\n font-weight: 200;\\n}\\n\\n.card .description {\\n font-size: 20px;\\n color: #999;\\n}\\n\",\"cardHtml\":\"
\\n
\\n
\\n

Value title

\\n
\\n ${My value:2} units.\\n
\\n
\\n Value description text\\n
\\n
\\n \\n
\\n
\"},\"title\":\"HTML Value Card\",\"dropShadow\":false,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/html-card-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/html-card-widget-settings.component.html new file mode 100644 index 0000000000..c95d3a8b98 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/html-card-widget-settings.component.html @@ -0,0 +1,25 @@ + +
+ + + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/html-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/html-card-widget-settings.component.ts new file mode 100644 index 0000000000..1dc0f398e0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/html-card-widget-settings.component.ts @@ -0,0 +1,54 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-html-card-widget-settings', + templateUrl: './html-card-widget-settings.component.html', + styleUrls: [] +}) +export class HtmlCardWidgetSettingsComponent extends WidgetSettingsComponent { + + htmlCardWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.htmlCardWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + cardHtml: '
HTML code here
', + cardCss: '.card {\n font-weight: bold; \n}' + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.htmlCardWidgetSettingsForm = this.fb.group({ + cardHtml: [settings.cardHtml, [Validators.required]], + cardCss: [settings.cardCss, []] + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.html index c38207c717..715d1f1fe6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.html @@ -26,7 +26,8 @@ {{ 'widgets.qr-code.qr-code-text-pattern-required' | translate }} -
- + +
+
+ +
+
+
+
+
+
+ diff --git a/ui-ngx/src/app/shared/components/html.component.scss b/ui-ngx/src/app/shared/components/html.component.scss new file mode 100644 index 0000000000..585dca667a --- /dev/null +++ b/ui-ngx/src/app/shared/components/html.component.scss @@ -0,0 +1,65 @@ +/** + * Copyright © 2016-2022 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-html { + position: relative; + + &.tb-disabled { + color: rgba(0, 0, 0, .38); + } + + &.fill-height { + height: 100%; + } + + .tb-html-content-panel { + height: calc(100% - 40px); + border: 1px solid #c0c0c0; + + #tb-html-input { + width: 100%; + min-width: 200px; + height: 100%; + + &:not(.fill-height) { + min-height: 200px; + } + } + } + + .tb-html-toolbar { + & > * { + &:not(:last-child) { + margin-right: 4px; + } + } + button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 { + background: rgba(220, 220, 220, .35); + align-items: center; + vertical-align: middle; + min-width: 32px; + min-height: 15px; + padding: 4px; + font-size: .8rem; + line-height: 15px; + &:not(.tb-help-popup-button) { + color: #7b7b7b; + } + } + .tb-help-popup-button-loading { + background: #f3f3f3; + } + } +} diff --git a/ui-ngx/src/app/shared/components/html.component.ts b/ui-ngx/src/app/shared/components/html.component.ts new file mode 100644 index 0000000000..696873507c --- /dev/null +++ b/ui-ngx/src/app/shared/components/html.component.ts @@ -0,0 +1,211 @@ +/// +/// Copyright © 2016-2022 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 { + ChangeDetectorRef, + Component, + ElementRef, + forwardRef, + Input, + OnDestroy, + OnInit, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; +import { Ace } from 'ace-builds'; +import { getAce } from '@shared/models/ace/ace.models'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; +import { ResizeObserver } from '@juggle/resize-observer'; +import { beautifyHtml } from '@shared/models/beautify.models'; + +@Component({ + selector: 'tb-html', + templateUrl: './html.component.html', + styleUrls: ['./html.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => HtmlComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => HtmlComponent), + multi: true, + } + ], + encapsulation: ViewEncapsulation.None +}) +export class HtmlComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator { + + @ViewChild('htmlEditor', {static: true}) + htmlEditorElmRef: ElementRef; + + private htmlEditor: Ace.Editor; + private editorsResizeCaf: CancelAnimationFrame; + private editorResize$: ResizeObserver; + private ignoreChange = false; + + @Input() label: string; + + @Input() disabled: boolean; + + @Input() fillHeight: boolean; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + fullscreen = false; + + modelValue: string; + + hasErrors = false; + + private propagateChange = null; + + constructor(public elementRef: ElementRef, + private utils: UtilsService, + private translate: TranslateService, + protected store: Store, + private raf: RafService, + private cd: ChangeDetectorRef) { + } + + ngOnInit(): void { + const editorElement = this.htmlEditorElmRef.nativeElement; + let editorOptions: Partial = { + mode: 'ace/mode/html', + showGutter: true, + showPrintMargin: true, + readOnly: this.disabled + }; + + const advancedOptions = { + enableSnippets: true, + enableBasicAutocompletion: true, + enableLiveAutocompletion: true + }; + + editorOptions = {...editorOptions, ...advancedOptions}; + getAce().subscribe( + (ace) => { + this.htmlEditor = ace.edit(editorElement, editorOptions); + this.htmlEditor.session.setUseWrapMode(true); + this.htmlEditor.setValue(this.modelValue ? this.modelValue : '', -1); + this.htmlEditor.setReadOnly(this.disabled); + this.htmlEditor.on('change', () => { + if (!this.ignoreChange) { + this.updateView(); + } + }); + // @ts-ignore + this.htmlEditor.session.on('changeAnnotation', () => { + const annotations = this.htmlEditor.session.getAnnotations(); + const hasErrors = annotations.filter(annotation => annotation.type === 'error').length > 0; + if (this.hasErrors !== hasErrors) { + this.hasErrors = hasErrors; + this.propagateChange(this.modelValue); + this.cd.markForCheck(); + } + }); + this.editorResize$ = new ResizeObserver(() => { + this.onAceEditorResize(); + }); + this.editorResize$.observe(editorElement); + } + ); + } + + ngOnDestroy(): void { + if (this.editorResize$) { + this.editorResize$.disconnect(); + } + } + + private onAceEditorResize() { + if (this.editorsResizeCaf) { + this.editorsResizeCaf(); + this.editorsResizeCaf = null; + } + this.editorsResizeCaf = this.raf.raf(() => { + this.htmlEditor.resize(); + this.htmlEditor.renderer.updateFull(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.htmlEditor) { + this.htmlEditor.setReadOnly(this.disabled); + } + } + + public validate(c: FormControl) { + return (!this.hasErrors) ? null : { + html: { + valid: false, + }, + }; + } + + beautifyHtml() { + beautifyHtml(this.modelValue, {indent_size: 4}).subscribe( + (res) => { + if (this.modelValue !== res) { + this.htmlEditor.setValue(res ? res : '', -1); + this.updateView(); + } + } + ); + } + + writeValue(value: string): void { + this.modelValue = value; + if (this.htmlEditor) { + this.ignoreChange = true; + this.htmlEditor.setValue(this.modelValue ? this.modelValue : '', -1); + this.ignoreChange = false; + } + } + + updateView() { + const editorValue = this.htmlEditor.getValue(); + if (this.modelValue !== editorValue) { + this.modelValue = editorValue; + this.propagateChange(this.modelValue); + this.cd.markForCheck(); + } + } +} diff --git a/ui-ngx/src/app/shared/components/js-func.component.html b/ui-ngx/src/app/shared/components/js-func.component.html index a57bb6c124..093bce582d 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.html +++ b/ui-ngx/src/app/shared/components/js-func.component.html @@ -19,8 +19,10 @@ tb-fullscreen [fullscreen]="fullscreen" fxLayout="column">
- - + +
- +
diff --git a/ui-ngx/src/app/shared/components/js-func.component.ts b/ui-ngx/src/app/shared/components/js-func.component.ts index be78c7480c..ce995c1f1c 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.ts +++ b/ui-ngx/src/app/shared/components/js-func.component.ts @@ -15,6 +15,7 @@ /// import { + ChangeDetectorRef, Component, ElementRef, forwardRef, @@ -125,13 +126,14 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, errorAnnotationId = -1; private propagateChange = null; - private hasErrors = false; + public hasErrors = false; constructor(public elementRef: ElementRef, private utils: UtilsService, private translate: TranslateService, protected store: Store, - private raf: RafService) { + private raf: RafService, + private cd: ChangeDetectorRef) { } ngOnInit(): void { @@ -185,6 +187,7 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, if (this.hasErrors !== hasErrors) { this.hasErrors = hasErrors; this.propagateChange(this.modelValue); + this.cd.markForCheck(); } }); } @@ -273,6 +276,7 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, this.functionValid = this.validateJsFunc(); if (!this.functionValid) { this.propagateChange(this.modelValue); + this.cd.markForCheck(); this.store.dispatch(new ActionNotificationShow( { message: this.validationError, @@ -400,6 +404,7 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, this.modelValue = editorValue; this.functionValid = true; this.propagateChange(this.modelValue); + this.cd.markForCheck(); } } } diff --git a/ui-ngx/src/app/shared/components/markdown-editor.component.html b/ui-ngx/src/app/shared/components/markdown-editor.component.html index e31ad8bc3f..53d4e95827 100644 --- a/ui-ngx/src/app/shared/components/markdown-editor.component.html +++ b/ui-ngx/src/app/shared/components/markdown-editor.component.html @@ -18,7 +18,7 @@
- +
diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 9d3d6864fe..1222abea5a 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -159,6 +159,7 @@ import { HELP_MARKDOWN_COMPONENT_TOKEN, SHARED_MODULE_TOKEN } from '@shared/comp import { TbMarkdownComponent } from '@shared/components/markdown.component'; import { ProtobufContentComponent } from '@shared/components/protobuf-content.component'; import { CssComponent } from '@shared/components/css.component'; +import { HtmlComponent } from '@shared/components/html.component'; import { SafePipe } from '@shared/pipe/safe.pipe'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { @@ -240,6 +241,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) JsonContentComponent, JsFuncComponent, CssComponent, + HtmlComponent, FabTriggerDirective, FabActionsDirective, FabToolbarComponent, @@ -389,6 +391,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) JsonContentComponent, JsFuncComponent, CssComponent, + HtmlComponent, FabTriggerDirective, FabActionsDirective, FabToolbarComponent, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index bb722a026c..3482da92c3 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3260,6 +3260,10 @@ "sort-settings": "Sort settings", "nodes-sort-function": "Nodes sort function" }, + "html-card": { + "html": "HTML", + "css": "CSS" + }, "input-widgets": { "attribute-not-allowed": "Attribute parameter cannot be used in this widget", "blocked-location": "Geolocation is blocked in your browser", From a735586f1ef6b5ff8de04403c13a693e3cb11e47 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 4 Apr 2022 14:07:35 +0300 Subject: [PATCH 041/276] UI: Add entities table widget and key settings form --- .../json/system/widget_bundles/cards.json | 8 +- ...entities-table-key-settings.component.html | 95 ++++++++++++++ .../entities-table-key-settings.component.ts | 86 +++++++++++++ ...ities-table-widget-settings.component.html | 116 +++++++++++++++++ ...ntities-table-widget-settings.component.ts | 118 ++++++++++++++++++ ...meseries-table-key-settings.component.html | 6 +- ...s-table-latest-key-settings.component.html | 6 +- ...eries-table-widget-settings.component.html | 3 +- .../lib/settings/widget-settings.module.ts | 18 ++- .../assets/locale/locale.constant-en_US.json | 17 ++- 10 files changed, 461 insertions(+), 12 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-table-key-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-table-key-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-table-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-table-widget-settings.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index 7a83eb1fe1..4930219741 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -135,9 +135,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyHeader\": {\n \"title\": \"Always display header\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyAction\": {\n \"title\": \"Always display actions column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"reserveSpaceForHiddenAction\": {\n \"title\": \"Hidden cell button actions display mode\",\n \"type\": \"string\",\n \"default\": \"true\"\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n },\n \"useRowStyleFunction\": {\n \"title\": \"Use row style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"rowStyleFunction\": {\n \"title\": \"Row style function: f(entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"enableStickyHeader\",\n \"enableStickyAction\",\n {\n \"key\": \"reserveSpaceForHiddenAction\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"true\",\n \"label\": \"Show empty space instead of hidden cell button action\"\n },\n {\n \"value\": \"false\",\n \"label\": \"Don't reserve space for hidden action buttons\"\n }\n ]\n },\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\",\n \"useRowStyleFunction\",\n {\n \"key\": \"rowStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entity/row_style_fn\",\n \"condition\": \"model.useRowStyleFunction === true\"\n }\n ]\n}", - "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value, entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"defaultColumnVisibility\": {\n \"title\": \"Default column visibility\",\n \"type\": \"string\",\n \"default\": \"visible\"\n },\n \"columnSelectionToDisplay\": {\n \"title\": \"Column selection in 'Columns to Display'\",\n \"type\": \"string\",\n \"default\": \"enabled\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entity/cell_style_fn\",\n \"condition\": \"model.useCellStyleFunction === true\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entity/cell_content_fn\",\n \"condition\": \"model.useCellContentFunction === true\"\n },\n {\n \"key\": \"defaultColumnVisibility\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"visible\",\n \"label\": \"Visible\"\n },\n {\n \"value\": \"hidden\",\n \"label\": \"Hidden\"\n } \n ]\n },\n {\n \"key\": \"columnSelectionToDisplay\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"enabled\",\n \"label\": \"Enabled\"\n },\n {\n \"value\": \"disabled\",\n \"label\": \"Disabled\"\n } \n ]\n }\n ]\n}", - "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}]}" + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-entities-table-widget-settings", + "dataKeySettingsDirective": "tb-entities-table-key-settings", + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"entitiesTitle\":\"\",\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"reserveSpaceForHiddenAction\":\"true\",\"displayEntityName\":true,\"entityNameColumnTitle\":\"\",\"displayEntityLabel\":false,\"displayEntityType\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"useRowStyleFunction\":false},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"useCellContentFunction\":false,\"defaultColumnVisibility\":\"visible\",\"columnSelectionToDisplay\":\"enabled\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}]}" } }, { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-table-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-table-key-settings.component.html new file mode 100644 index 0000000000..8714a5b820 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-table-key-settings.component.html @@ -0,0 +1,95 @@ + +
+ + widgets.table.column-width + + +
+ widgets.table.cell-style + + + + + {{ 'widgets.table.use-cell-style-function' | translate }} + + + + widget-config.advanced-settings + + + + + + + +
+
+ widgets.table.cell-content + + + + + {{ 'widgets.table.use-cell-content-function' | translate }} + + + + widget-config.advanced-settings + + + + + + + +
+ + widgets.table.default-column-visibility + + + {{ 'widgets.table.column-visibility-visible' | translate }} + + + {{ 'widgets.table.column-visibility-hidden' | translate }} + + + + + widgets.table.column-selection-to-display + + + {{ 'widgets.table.column-selection-to-display-enabled' | translate }} + + + {{ 'widgets.table.column-selection-to-display-disabled' | translate }} + + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-table-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-table-key-settings.component.ts new file mode 100644 index 0000000000..9ec2482de8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-table-key-settings.component.ts @@ -0,0 +1,86 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-entities-table-key-settings', + templateUrl: './entities-table-key-settings.component.html', + styleUrls: ['./widget-settings.scss'] +}) +export class EntitiesTableKeySettingsComponent extends WidgetSettingsComponent { + + entitiesTableKeySettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.entitiesTableKeySettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + columnWidth: '0px', + useCellStyleFunction: false, + cellStyleFunction: '', + useCellContentFunction: false, + cellContentFunction: '', + defaultColumnVisibility: 'visible', + columnSelectionToDisplay: 'enabled' + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.entitiesTableKeySettingsForm = this.fb.group({ + columnWidth: [settings.columnWidth, []], + useCellStyleFunction: [settings.useCellStyleFunction, []], + cellStyleFunction: [settings.cellStyleFunction, [Validators.required]], + useCellContentFunction: [settings.useCellContentFunction, []], + cellContentFunction: [settings.cellContentFunction, [Validators.required]], + defaultColumnVisibility: [settings.defaultColumnVisibility, []], + columnSelectionToDisplay: [settings.columnSelectionToDisplay, []], + }); + } + + protected validatorTriggers(): string[] { + return ['useCellStyleFunction', 'useCellContentFunction']; + } + + protected updateValidators(emitEvent: boolean) { + const useCellStyleFunction: boolean = this.entitiesTableKeySettingsForm.get('useCellStyleFunction').value; + const useCellContentFunction: boolean = this.entitiesTableKeySettingsForm.get('useCellContentFunction').value; + if (useCellStyleFunction) { + this.entitiesTableKeySettingsForm.get('cellStyleFunction').enable(); + } else { + this.entitiesTableKeySettingsForm.get('cellStyleFunction').disable(); + } + if (useCellContentFunction) { + this.entitiesTableKeySettingsForm.get('cellContentFunction').enable(); + } else { + this.entitiesTableKeySettingsForm.get('cellContentFunction').disable(); + } + this.entitiesTableKeySettingsForm.get('cellStyleFunction').updateValueAndValidity({emitEvent}); + this.entitiesTableKeySettingsForm.get('cellContentFunction').updateValueAndValidity({emitEvent}); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-table-widget-settings.component.html new file mode 100644 index 0000000000..e58f083bea --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-table-widget-settings.component.html @@ -0,0 +1,116 @@ + +
+
+ widgets.table.common-table-settings + + widgets.table.entities-table-title + + +
+
+ + {{ 'widgets.table.enable-search' | translate }} + + + {{ 'widgets.table.enable-select-column-display' | translate }} + +
+
+ + {{ 'widgets.table.enable-sticky-header' | translate }} + + + {{ 'widgets.table.enable-sticky-action' | translate }} + +
+
+ + widgets.table.hidden-cell-button-display-mode + + + {{ 'widgets.table.show-empty-space-hidden-action' | translate }} + + + {{ 'widgets.table.dont-reserve-space-hidden-action' | translate }} + + + +
+
+ + {{ 'widgets.table.display-entity-name' | translate }} + + + widgets.table.entity-name-column-title + + +
+
+ + {{ 'widgets.table.display-entity-label' | translate }} + + + widgets.table.entity-label-column-title + + +
+ + {{ 'widgets.table.display-entity-type' | translate }} + +
+ + {{ 'widgets.table.display-pagination' | translate }} + + + widgets.table.default-page-size + + +
+ + widgets.table.default-sort-order + + +
+
+
+ widgets.table.row-style + + + + + {{ 'widgets.table.use-row-style-function' | translate }} + + + + widget-config.advanced-settings + + + + + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-table-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-table-widget-settings.component.ts new file mode 100644 index 0000000000..3d40a01484 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-table-widget-settings.component.ts @@ -0,0 +1,118 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-entities-table-widget-settings', + templateUrl: './entities-table-widget-settings.component.html', + styleUrls: ['./widget-settings.scss'] +}) +export class EntitiesTableWidgetSettingsComponent extends WidgetSettingsComponent { + + entitiesTableWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.entitiesTableWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + entitiesTitle: '', + enableSearch: true, + enableSelectColumnDisplay: true, + enableStickyHeader: true, + enableStickyAction: true, + reserveSpaceForHiddenAction: 'true', + displayEntityName: true, + entityNameColumnTitle: '', + displayEntityLabel: false, + entityLabelColumnTitle: '', + displayEntityType: true, + displayPagination: true, + defaultPageSize: 10, + defaultSortOrder: 'entityName', + useRowStyleFunction: false, + rowStyleFunction: '' + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.entitiesTableWidgetSettingsForm = this.fb.group({ + entitiesTitle: [settings.entitiesTitle, []], + enableSearch: [settings.enableSearch, []], + enableSelectColumnDisplay: [settings.enableSelectColumnDisplay, []], + enableStickyHeader: [settings.enableStickyHeader, []], + enableStickyAction: [settings.enableStickyAction, []], + reserveSpaceForHiddenAction: [settings.reserveSpaceForHiddenAction, []], + displayEntityName: [settings.displayEntityName, []], + entityNameColumnTitle: [settings.entityNameColumnTitle, []], + displayEntityLabel: [settings.displayEntityLabel, []], + entityLabelColumnTitle: [settings.entityLabelColumnTitle, []], + displayEntityType: [settings.displayEntityType, []], + displayPagination: [settings.displayPagination, []], + defaultPageSize: [settings.defaultPageSize, [Validators.min(1)]], + defaultSortOrder: [settings.defaultSortOrder, []], + useRowStyleFunction: [settings.useRowStyleFunction, []], + rowStyleFunction: [settings.rowStyleFunction, [Validators.required]] + }); + } + + protected validatorTriggers(): string[] { + return ['useRowStyleFunction', 'displayPagination', 'displayEntityName', 'displayEntityLabel']; + } + + protected updateValidators(emitEvent: boolean) { + const useRowStyleFunction: boolean = this.entitiesTableWidgetSettingsForm.get('useRowStyleFunction').value; + const displayPagination: boolean = this.entitiesTableWidgetSettingsForm.get('displayPagination').value; + const displayEntityName: boolean = this.entitiesTableWidgetSettingsForm.get('displayEntityName').value; + const displayEntityLabel: boolean = this.entitiesTableWidgetSettingsForm.get('displayEntityLabel').value; + if (useRowStyleFunction) { + this.entitiesTableWidgetSettingsForm.get('rowStyleFunction').enable(); + } else { + this.entitiesTableWidgetSettingsForm.get('rowStyleFunction').disable(); + } + if (displayPagination) { + this.entitiesTableWidgetSettingsForm.get('defaultPageSize').enable(); + } else { + this.entitiesTableWidgetSettingsForm.get('defaultPageSize').disable(); + } + if (displayEntityName) { + this.entitiesTableWidgetSettingsForm.get('entityNameColumnTitle').enable(); + } else { + this.entitiesTableWidgetSettingsForm.get('entityNameColumnTitle').disable(); + } + if (displayEntityLabel) { + this.entitiesTableWidgetSettingsForm.get('entityLabelColumnTitle').enable(); + } else { + this.entitiesTableWidgetSettingsForm.get('entityLabelColumnTitle').disable(); + } + this.entitiesTableWidgetSettingsForm.get('rowStyleFunction').updateValueAndValidity({emitEvent}); + this.entitiesTableWidgetSettingsForm.get('defaultPageSize').updateValueAndValidity({emitEvent}); + this.entitiesTableWidgetSettingsForm.get('entityNameColumnTitle').updateValueAndValidity({emitEvent}); + this.entitiesTableWidgetSettingsForm.get('entityLabelColumnTitle').updateValueAndValidity({emitEvent}); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-key-settings.component.html index bf568fc3eb..f8a2b852d2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-key-settings.component.html @@ -31,7 +31,8 @@ - - - - - Date: Mon, 4 Apr 2022 14:12:56 +0300 Subject: [PATCH 042/276] UI: Refactoring --- ...board-state-widget-settings.component.html | 0 ...shboard-state-widget-settings.component.ts | 2 +- ...s-hierarchy-widget-settings.component.html | 0 ...ies-hierarchy-widget-settings.component.ts | 2 +- ...entities-table-key-settings.component.html | 0 .../entities-table-key-settings.component.ts | 2 +- ...ities-table-widget-settings.component.html | 0 ...ntities-table-widget-settings.component.ts | 2 +- .../html-card-widget-settings.component.html | 0 .../html-card-widget-settings.component.ts | 0 .../label-widget-font.component.html | 0 .../label-widget-font.component.ts | 0 .../label-widget-label.component.html | 0 .../label-widget-label.component.scss | 0 .../label-widget-label.component.ts | 4 +-- .../label-widget-settings.component.html | 0 .../label-widget-settings.component.scss | 2 +- .../label-widget-settings.component.ts | 4 +-- .../markdown-widget-settings.component.html | 0 .../markdown-widget-settings.component.ts | 2 +- .../qrcode-widget-settings.component.html | 0 .../qrcode-widget-settings.component.ts | 2 +- ...simple-card-widget-settings.component.html | 0 .../simple-card-widget-settings.component.ts | 0 ...meseries-table-key-settings.component.html | 0 ...timeseries-table-key-settings.component.ts | 2 +- ...s-table-latest-key-settings.component.html | 0 ...ies-table-latest-key-settings.component.ts | 2 +- ...eries-table-widget-settings.component.html | 0 ...eseries-table-widget-settings.component.ts | 2 +- .../lib/settings/widget-settings.module.ts | 28 +++++++++---------- 31 files changed, 28 insertions(+), 28 deletions(-) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/dashboard-state-widget-settings.component.html (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/dashboard-state-widget-settings.component.ts (98%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/entities-hierarchy-widget-settings.component.html (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/entities-hierarchy-widget-settings.component.ts (98%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/entities-table-key-settings.component.html (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/entities-table-key-settings.component.ts (98%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/entities-table-widget-settings.component.html (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/entities-table-widget-settings.component.ts (99%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/html-card-widget-settings.component.html (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/html-card-widget-settings.component.ts (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/label-widget-font.component.html (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/label-widget-font.component.ts (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/label-widget-label.component.html (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/label-widget-label.component.scss (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/label-widget-label.component.ts (96%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/label-widget-settings.component.html (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/label-widget-settings.component.scss (94%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/label-widget-settings.component.ts (97%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/markdown-widget-settings.component.html (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/markdown-widget-settings.component.ts (98%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/qrcode-widget-settings.component.html (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/qrcode-widget-settings.component.ts (98%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/simple-card-widget-settings.component.html (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/simple-card-widget-settings.component.ts (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/timeseries-table-key-settings.component.html (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/timeseries-table-key-settings.component.ts (98%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/timeseries-table-latest-key-settings.component.html (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/timeseries-table-latest-key-settings.component.ts (98%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/timeseries-table-widget-settings.component.html (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{ => cards}/timeseries-table-widget-settings.component.ts (98%) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/dashboard-state-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/dashboard-state-widget-settings.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/dashboard-state-widget-settings.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/dashboard-state-widget-settings.component.html diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/dashboard-state-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/dashboard-state-widget-settings.component.ts similarity index 98% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/dashboard-state-widget-settings.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/dashboard-state-widget-settings.component.ts index 7d2a11e9cc..7c8d9de6e4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/dashboard-state-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/dashboard-state-widget-settings.component.ts @@ -25,7 +25,7 @@ import { map, mergeMap, startWith } from 'rxjs/operators'; @Component({ selector: 'tb-dashboard-state-widget-settings', templateUrl: './dashboard-state-widget-settings.component.html', - styleUrls: ['./widget-settings.scss'] + styleUrls: ['./../widget-settings.scss'] }) export class DashboardStateWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-hierarchy-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-hierarchy-widget-settings.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-hierarchy-widget-settings.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-hierarchy-widget-settings.component.html diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-hierarchy-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-hierarchy-widget-settings.component.ts similarity index 98% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-hierarchy-widget-settings.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-hierarchy-widget-settings.component.ts index c0757b4904..d9e850b14f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-hierarchy-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-hierarchy-widget-settings.component.ts @@ -23,7 +23,7 @@ import { AppState } from '@core/core.state'; @Component({ selector: 'tb-entities-hierarchy-widget-settings', templateUrl: './entities-hierarchy-widget-settings.component.html', - styleUrls: ['./widget-settings.scss'] + styleUrls: ['./../widget-settings.scss'] }) export class EntitiesHierarchyWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-table-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-table-key-settings.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.html diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-table-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.ts similarity index 98% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-table-key-settings.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.ts index 9ec2482de8..738e99223c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-table-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.ts @@ -23,7 +23,7 @@ import { AppState } from '@core/core.state'; @Component({ selector: 'tb-entities-table-key-settings', templateUrl: './entities-table-key-settings.component.html', - styleUrls: ['./widget-settings.scss'] + styleUrls: ['./../widget-settings.scss'] }) export class EntitiesTableKeySettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-table-widget-settings.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.html diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-table-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.ts similarity index 99% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-table-widget-settings.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.ts index 3d40a01484..789323679b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entities-table-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.ts @@ -23,7 +23,7 @@ import { AppState } from '@core/core.state'; @Component({ selector: 'tb-entities-table-widget-settings', templateUrl: './entities-table-widget-settings.component.html', - styleUrls: ['./widget-settings.scss'] + styleUrls: ['./../widget-settings.scss'] }) export class EntitiesTableWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/html-card-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/html-card-widget-settings.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/html-card-widget-settings.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/html-card-widget-settings.component.html diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/html-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/html-card-widget-settings.component.ts similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/html-card-widget-settings.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/html-card-widget-settings.component.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-font.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-font.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-font.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-font.component.html diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-font.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-font.component.ts similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-font.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-font.component.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-label.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-label.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.html diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-label.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.scss similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-label.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.scss diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-label.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.ts similarity index 96% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-label.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.ts index 65f0cca39c..0c56e66e6f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-label.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.ts @@ -20,7 +20,7 @@ import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { TranslateService } from '@ngx-translate/core'; -import { LabelWidgetFont } from '@home/components/widget/lib/settings/label-widget-font.component'; +import { LabelWidgetFont } from '@home/components/widget/lib/settings/cards/label-widget-font.component'; export interface LabelWidgetLabel { pattern: string; @@ -33,7 +33,7 @@ export interface LabelWidgetLabel { @Component({ selector: 'tb-label-widget-label', templateUrl: './label-widget-label.component.html', - styleUrls: ['./label-widget-label.component.scss', './widget-settings.scss'], + styleUrls: ['./label-widget-label.component.scss', './../widget-settings.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-settings.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.html diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-settings.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.scss similarity index 94% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-settings.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.scss index 5125ee03e4..563f2ae465 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-settings.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.scss @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import '../../../../../../../scss/constants'; +@import '../../../../../../../../scss/constants'; :host { .tb-label-widget-labels { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.ts similarity index 97% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-settings.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.ts index 0535b55a33..cde33be48c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/label-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.ts @@ -19,12 +19,12 @@ import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.m import { AbstractControl, FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { LabelWidgetLabel } from '@home/components/widget/lib/settings/label-widget-label.component'; +import { LabelWidgetLabel } from '@home/components/widget/lib/settings/cards/label-widget-label.component'; @Component({ selector: 'tb-label-widget-settings', templateUrl: './label-widget-settings.component.html', - styleUrls: ['./label-widget-settings.component.scss', './widget-settings.scss'] + styleUrls: ['./label-widget-settings.component.scss', './../widget-settings.scss'] }) export class LabelWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/markdown-widget-settings.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/markdown-widget-settings.component.html diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/markdown-widget-settings.component.ts similarity index 98% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/markdown-widget-settings.component.ts index db78d8b42f..59667b7b77 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/markdown-widget-settings.component.ts @@ -23,7 +23,7 @@ import { AppState } from '@core/core.state'; @Component({ selector: 'tb-markdown-widget-settings', templateUrl: './markdown-widget-settings.component.html', - styleUrls: ['./widget-settings.scss'] + styleUrls: ['./../widget-settings.scss'] }) export class MarkdownWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/qrcode-widget-settings.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/qrcode-widget-settings.component.html diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/qrcode-widget-settings.component.ts similarity index 98% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/qrcode-widget-settings.component.ts index 7405852357..f5c204a136 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/qrcode-widget-settings.component.ts @@ -23,7 +23,7 @@ import { AppState } from '@core/core.state'; @Component({ selector: 'tb-qrcode-widget-settings', templateUrl: './qrcode-widget-settings.component.html', - styleUrls: ['./widget-settings.scss'] + styleUrls: ['./../widget-settings.scss'] }) export class QrCodeWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/simple-card-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/simple-card-widget-settings.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.html diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/simple-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.ts similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/simple-card-widget-settings.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-key-settings.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.ts similarity index 98% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-key-settings.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.ts index b2433dfde0..2330b95752 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.ts @@ -23,7 +23,7 @@ import { AppState } from '@core/core.state'; @Component({ selector: 'tb-timeseries-table-key-settings', templateUrl: './timeseries-table-key-settings.component.html', - styleUrls: ['./widget-settings.scss'] + styleUrls: ['./../widget-settings.scss'] }) export class TimeseriesTableKeySettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-latest-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-latest-key-settings.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-latest-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.ts similarity index 98% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-latest-key-settings.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.ts index 1d8d26ead3..1abba3ae0b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-latest-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.ts @@ -23,7 +23,7 @@ import { AppState } from '@core/core.state'; @Component({ selector: 'tb-timeseries-table-latest-key-settings', templateUrl: './timeseries-table-latest-key-settings.component.html', - styleUrls: ['./widget-settings.scss'] + styleUrls: ['./../widget-settings.scss'] }) export class TimeseriesTableLatestKeySettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-widget-settings.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts similarity index 98% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-widget-settings.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts index d28b4f081a..d7a6c83e8d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/timeseries-table-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts @@ -23,7 +23,7 @@ import { AppState } from '@core/core.state'; @Component({ selector: 'tb-timeseries-table-widget-settings', templateUrl: './timeseries-table-widget-settings.component.html', - styleUrls: ['./widget-settings.scss'] + styleUrls: ['./../widget-settings.scss'] }) export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index 7098cf99a8..2beaed1d92 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -15,44 +15,44 @@ /// import { NgModule, Type } from '@angular/core'; -import { QrCodeWidgetSettingsComponent } from '@home/components/widget/lib/settings/qrcode-widget-settings.component'; +import { QrCodeWidgetSettingsComponent } from '@home/components/widget/lib/settings/cards/qrcode-widget-settings.component'; import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; import { SharedHomeComponentsModule } from '@home/components/shared-home-components.module'; import { IWidgetSettingsComponent } from '@shared/models/widget.models'; import { TimeseriesTableWidgetSettingsComponent -} from '@home/components/widget/lib/settings/timeseries-table-widget-settings.component'; +} from '@home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component'; import { TimeseriesTableKeySettingsComponent -} from '@home/components/widget/lib/settings/timeseries-table-key-settings.component'; +} from '@home/components/widget/lib/settings/cards/timeseries-table-key-settings.component'; import { TimeseriesTableLatestKeySettingsComponent -} from '@home/components/widget/lib/settings/timeseries-table-latest-key-settings.component'; +} from '@home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component'; import { MarkdownWidgetSettingsComponent -} from '@home/components/widget/lib/settings/markdown-widget-settings.component'; -import { LabelWidgetFontComponent } from '@home/components/widget/lib/settings/label-widget-font.component'; -import { LabelWidgetLabelComponent } from '@home/components/widget/lib/settings/label-widget-label.component'; -import { LabelWidgetSettingsComponent } from '@home/components/widget/lib/settings/label-widget-settings.component'; +} from '@home/components/widget/lib/settings/cards/markdown-widget-settings.component'; +import { LabelWidgetFontComponent } from '@home/components/widget/lib/settings/cards/label-widget-font.component'; +import { LabelWidgetLabelComponent } from '@home/components/widget/lib/settings/cards/label-widget-label.component'; +import { LabelWidgetSettingsComponent } from '@home/components/widget/lib/settings/cards/label-widget-settings.component'; import { SimpleCardWidgetSettingsComponent -} from '@home/components/widget/lib/settings/simple-card-widget-settings.component'; +} from '@home/components/widget/lib/settings/cards/simple-card-widget-settings.component'; import { DashboardStateWidgetSettingsComponent -} from '@home/components/widget/lib/settings/dashboard-state-widget-settings.component'; +} from '@home/components/widget/lib/settings/cards/dashboard-state-widget-settings.component'; import { EntitiesHierarchyWidgetSettingsComponent -} from '@home/components/widget/lib/settings/entities-hierarchy-widget-settings.component'; +} from '@home/components/widget/lib/settings/cards/entities-hierarchy-widget-settings.component'; import { HtmlCardWidgetSettingsComponent -} from '@home/components/widget/lib/settings/html-card-widget-settings.component'; +} from '@home/components/widget/lib/settings/cards/html-card-widget-settings.component'; import { EntitiesTableWidgetSettingsComponent -} from '@home/components/widget/lib/settings/entities-table-widget-settings.component'; +} from '@home/components/widget/lib/settings/cards/entities-table-widget-settings.component'; import { EntitiesTableKeySettingsComponent -} from '@home/components/widget/lib/settings/entities-table-key-settings.component'; +} from '@home/components/widget/lib/settings/cards/entities-table-key-settings.component'; @NgModule({ declarations: [ From 03828d2a1f633769c5799cce68ab105c71d6dec0 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 4 Apr 2022 15:58:58 +0300 Subject: [PATCH 043/276] UI: Add alarms table widget/key settings forms --- .../system/widget_bundles/alarm_widgets.json | 6 +- .../alarms-table-key-settings.component.html | 95 +++++++++++++++ .../alarms-table-key-settings.component.ts | 86 ++++++++++++++ ...larms-table-widget-settings.component.html | 110 ++++++++++++++++++ .../alarms-table-widget-settings.component.ts | 104 +++++++++++++++++ .../lib/settings/widget-settings.module.ts | 18 ++- .../assets/locale/locale.constant-en_US.json | 9 +- 7 files changed, 422 insertions(+), 6 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/alarm_widgets.json b/application/src/main/data/json/system/widget_bundles/alarm_widgets.json index a5b5146004..448fcaf52c 100644 --- a/application/src/main/data/json/system/widget_bundles/alarm_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/alarm_widgets.json @@ -19,8 +19,10 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.alarmsTableWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"AlarmTableSettings\",\n \"properties\": {\n \"alarmsTitle\": {\n \"title\": \"Alarms table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSelection\": {\n \"title\": \"Enable alarms selection\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSearch\": {\n \"title\": \"Enable alarms search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableFilter\": {\n \"title\": \"Enable alarm filter\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyHeader\": {\n \"title\": \"Always display header\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyAction\": {\n \"title\": \"Always display actions column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"reserveSpaceForHiddenAction\": {\n \"title\": \"Hidden cell button actions display mode\",\n \"type\": \"string\",\n \"default\": \"true\"\n },\n \"displayDetails\": {\n \"title\": \"Display alarm details\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowAcknowledgment\": {\n \"title\": \"Allow alarms acknowledgment\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowClear\": {\n \"title\": \"Allow alarms clear\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"-createdTime\"\n },\n \"useRowStyleFunction\": {\n \"title\": \"Use row style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"rowStyleFunction\": {\n \"title\": \"Row style function: f(alarm, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"alarmsTitle\",\n \"enableSelection\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"enableFilter\",\n \"enableStickyHeader\",\n \"enableStickyAction\",\n {\n \"key\": \"reserveSpaceForHiddenAction\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"true\",\n \"label\": \"Show empty space instead of hidden cell button action\"\n },\n {\n \"value\": \"false\",\n \"label\": \"Don't reserve space for hidden action buttons\"\n }\n ]\n },\n \"displayDetails\",\n \"allowAcknowledgment\",\n \"allowClear\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\",\n \"useRowStyleFunction\",\n {\n \"key\": \"rowStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/alarm/row_style_fn\",\n \"condition\": \"model.useRowStyleFunction === true\"\n }\n ]\n}", - "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value, alarm, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, alarm, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"defaultColumnVisibility\": {\n \"title\": \"Default column visibility\",\n \"type\": \"string\",\n \"default\": \"visible\"\n },\n \"columnSelectionToDisplay\": {\n \"title\": \"Column selection in 'Columns to Display'\",\n \"type\": \"string\",\n \"default\": \"enabled\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/alarm/cell_style_fn\",\n \"condition\": \"model.useCellStyleFunction === true\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/alarm/cell_content_fn\",\n \"condition\": \"model.useCellContentFunction === true\"\n },\n {\n \"key\": \"defaultColumnVisibility\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"visible\",\n \"label\": \"Visible\"\n },\n {\n \"value\": \"hidden\",\n \"label\": \"Hidden\"\n } \n ]\n },\n {\n \"key\": \"columnSelectionToDisplay\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"enabled\",\n \"label\": \"Enabled\"\n },\n {\n \"value\": \"disabled\",\n \"label\": \"Disabled\"\n } \n ]\n }\n ]\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-alarms-table-widget-settings", + "dataKeySettingsDirective": "tb-alarms-table-key-settings", "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStickyAction\":false,\"enableFilter\":true},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{},\"alarmStatusList\":[],\"alarmSeverityList\":[],\"alarmTypeList\":[],\"searchPropagatedAlarms\":false}" } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.html new file mode 100644 index 0000000000..6931d256e8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.html @@ -0,0 +1,95 @@ + +
+ + widgets.table.column-width + + +
+ widgets.table.cell-style + + + + + {{ 'widgets.table.use-cell-style-function' | translate }} + + + + widget-config.advanced-settings + + + + + + + +
+
+ widgets.table.cell-content + + + + + {{ 'widgets.table.use-cell-content-function' | translate }} + + + + widget-config.advanced-settings + + + + + + + +
+ + widgets.table.default-column-visibility + + + {{ 'widgets.table.column-visibility-visible' | translate }} + + + {{ 'widgets.table.column-visibility-hidden' | translate }} + + + + + widgets.table.column-selection-to-display + + + {{ 'widgets.table.column-selection-to-display-enabled' | translate }} + + + {{ 'widgets.table.column-selection-to-display-disabled' | translate }} + + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.ts new file mode 100644 index 0000000000..9f9cbbf5e1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.ts @@ -0,0 +1,86 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-alarms-table-key-settings', + templateUrl: './alarms-table-key-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class AlarmsTableKeySettingsComponent extends WidgetSettingsComponent { + + alarmsTableKeySettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.alarmsTableKeySettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + columnWidth: '0px', + useCellStyleFunction: false, + cellStyleFunction: '', + useCellContentFunction: false, + cellContentFunction: '', + defaultColumnVisibility: 'visible', + columnSelectionToDisplay: 'enabled' + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.alarmsTableKeySettingsForm = this.fb.group({ + columnWidth: [settings.columnWidth, []], + useCellStyleFunction: [settings.useCellStyleFunction, []], + cellStyleFunction: [settings.cellStyleFunction, [Validators.required]], + useCellContentFunction: [settings.useCellContentFunction, []], + cellContentFunction: [settings.cellContentFunction, [Validators.required]], + defaultColumnVisibility: [settings.defaultColumnVisibility, []], + columnSelectionToDisplay: [settings.columnSelectionToDisplay, []], + }); + } + + protected validatorTriggers(): string[] { + return ['useCellStyleFunction', 'useCellContentFunction']; + } + + protected updateValidators(emitEvent: boolean) { + const useCellStyleFunction: boolean = this.alarmsTableKeySettingsForm.get('useCellStyleFunction').value; + const useCellContentFunction: boolean = this.alarmsTableKeySettingsForm.get('useCellContentFunction').value; + if (useCellStyleFunction) { + this.alarmsTableKeySettingsForm.get('cellStyleFunction').enable(); + } else { + this.alarmsTableKeySettingsForm.get('cellStyleFunction').disable(); + } + if (useCellContentFunction) { + this.alarmsTableKeySettingsForm.get('cellContentFunction').enable(); + } else { + this.alarmsTableKeySettingsForm.get('cellContentFunction').disable(); + } + this.alarmsTableKeySettingsForm.get('cellStyleFunction').updateValueAndValidity({emitEvent}); + this.alarmsTableKeySettingsForm.get('cellContentFunction').updateValueAndValidity({emitEvent}); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html new file mode 100644 index 0000000000..3b5516c26c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html @@ -0,0 +1,110 @@ + +
+
+ widgets.table.common-table-settings + + widgets.table.alarms-table-title + + +
+
+ + {{ 'widgets.table.enable-alarms-selection' | translate }} + + + {{ 'widgets.table.enable-alarms-search' | translate }} + + + {{ 'widgets.table.enable-select-column-display' | translate }} + + + {{ 'widgets.table.enable-alarm-filter' | translate }} + +
+
+ + {{ 'widgets.table.enable-sticky-header' | translate }} + + + {{ 'widgets.table.enable-sticky-action' | translate }} + +
+
+ + widgets.table.hidden-cell-button-display-mode + + + {{ 'widgets.table.show-empty-space-hidden-action' | translate }} + + + {{ 'widgets.table.dont-reserve-space-hidden-action' | translate }} + + + +
+ + {{ 'widgets.table.display-alarm-details' | translate }} + + + {{ 'widgets.table.allow-alarms-ack' | translate }} + + + {{ 'widgets.table.allow-alarms-clear' | translate }} + +
+ + {{ 'widgets.table.display-pagination' | translate }} + + + widgets.table.default-page-size + + +
+ + widgets.table.default-sort-order + + +
+
+
+ widgets.table.row-style + + + + + {{ 'widgets.table.use-row-style-function' | translate }} + + + + widget-config.advanced-settings + + + + + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.ts new file mode 100644 index 0000000000..c57ae7d6f5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.ts @@ -0,0 +1,104 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-alarms-table-widget-settings', + templateUrl: './alarms-table-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class AlarmsTableWidgetSettingsComponent extends WidgetSettingsComponent { + + alarmsTableWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.alarmsTableWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + alarmsTitle: '', + enableSelection: true, + enableSearch: true, + enableSelectColumnDisplay: true, + enableFilter: true, + enableStickyHeader: true, + enableStickyAction: true, + reserveSpaceForHiddenAction: 'true', + displayDetails: true, + allowAcknowledgment: true, + allowClear: true, + displayPagination: true, + defaultPageSize: 10, + defaultSortOrder: '-createdTime', + useRowStyleFunction: false, + rowStyleFunction: '' + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.alarmsTableWidgetSettingsForm = this.fb.group({ + alarmsTitle: [settings.alarmsTitle, []], + enableSelection: [settings.enableSelection, []], + enableSearch: [settings.enableSearch, []], + enableSelectColumnDisplay: [settings.enableSelectColumnDisplay, []], + enableFilter: [settings.enableFilter, []], + enableStickyHeader: [settings.enableStickyHeader, []], + enableStickyAction: [settings.enableStickyAction, []], + reserveSpaceForHiddenAction: [settings.reserveSpaceForHiddenAction, []], + displayDetails: [settings.displayDetails, []], + allowAcknowledgment: [settings.allowAcknowledgment, []], + allowClear: [settings.allowClear, []], + displayPagination: [settings.displayPagination, []], + defaultPageSize: [settings.defaultPageSize, [Validators.min(1)]], + defaultSortOrder: [settings.defaultSortOrder, []], + useRowStyleFunction: [settings.useRowStyleFunction, []], + rowStyleFunction: [settings.rowStyleFunction, [Validators.required]] + }); + } + + protected validatorTriggers(): string[] { + return ['useRowStyleFunction', 'displayPagination']; + } + + protected updateValidators(emitEvent: boolean) { + const useRowStyleFunction: boolean = this.alarmsTableWidgetSettingsForm.get('useRowStyleFunction').value; + const displayPagination: boolean = this.alarmsTableWidgetSettingsForm.get('displayPagination').value; + if (useRowStyleFunction) { + this.alarmsTableWidgetSettingsForm.get('rowStyleFunction').enable(); + } else { + this.alarmsTableWidgetSettingsForm.get('rowStyleFunction').disable(); + } + if (displayPagination) { + this.alarmsTableWidgetSettingsForm.get('defaultPageSize').enable(); + } else { + this.alarmsTableWidgetSettingsForm.get('defaultPageSize').disable(); + } + this.alarmsTableWidgetSettingsForm.get('rowStyleFunction').updateValueAndValidity({emitEvent}); + this.alarmsTableWidgetSettingsForm.get('defaultPageSize').updateValueAndValidity({emitEvent}); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index 2beaed1d92..14e5cc21bf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -53,6 +53,12 @@ import { import { EntitiesTableKeySettingsComponent } from '@home/components/widget/lib/settings/cards/entities-table-key-settings.component'; +import { + AlarmsTableWidgetSettingsComponent +} from '@home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component'; +import { + AlarmsTableKeySettingsComponent +} from '@home/components/widget/lib/settings/alarm/alarms-table-key-settings.component'; @NgModule({ declarations: [ @@ -69,7 +75,9 @@ import { EntitiesHierarchyWidgetSettingsComponent, HtmlCardWidgetSettingsComponent, EntitiesTableWidgetSettingsComponent, - EntitiesTableKeySettingsComponent + EntitiesTableKeySettingsComponent, + AlarmsTableWidgetSettingsComponent, + AlarmsTableKeySettingsComponent ], imports: [ CommonModule, @@ -90,7 +98,9 @@ import { EntitiesHierarchyWidgetSettingsComponent, HtmlCardWidgetSettingsComponent, EntitiesTableWidgetSettingsComponent, - EntitiesTableKeySettingsComponent + EntitiesTableKeySettingsComponent, + AlarmsTableWidgetSettingsComponent, + AlarmsTableKeySettingsComponent ] }) export class WidgetSettingsModule { @@ -108,5 +118,7 @@ export const widgetSettingsComponentsMap: {[key: string]: Type Date: Mon, 4 Apr 2022 17:06:40 +0200 Subject: [PATCH 044/276] added consumerPerPartition to Queue entity --- .../thingsboard/server/common/data/queue/Queue.java | 1 + .../thingsboard/server/dao/model/ModelConstants.java | 1 + .../thingsboard/server/dao/model/sql/QueueEntity.java | 5 +++++ dao/src/main/resources/sql/schema-entities.sql | 1 + .../server/dao/service/BaseQueueServiceTest.java | 2 +- .../home/pages/admin/queue/queue.component.html | 11 ++++++++++- .../modules/home/pages/admin/queue/queue.component.ts | 2 ++ ui-ngx/src/app/shared/models/queue.models.ts | 1 + ui-ngx/src/assets/locale/locale.constant-en_US.json | 2 ++ 9 files changed, 24 insertions(+), 2 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java index 84776c88f4..004bd0057d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java @@ -29,6 +29,7 @@ public class Queue extends BaseData implements HasName, HasTenantId { private String topic; private int pollInterval; private int partitions; + private boolean consumerPerPartition; private long packProcessingTimeout; private SubmitStrategy submitStrategy; private ProcessingStrategy processingStrategy; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index cb209c2ee7..73c19cab3c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -592,6 +592,7 @@ public class ModelConstants { public static final String QUEUE_TOPIC_PROPERTY = "topic"; public static final String QUEUE_POLL_INTERVAL_PROPERTY = "poll_interval"; public static final String QUEUE_PARTITIONS_PROPERTY = "partitions"; + public static final String QUEUE_CONSUMER_PER_PARTITION = "consumer_per_partition"; public static final String QUEUE_PACK_PROCESSING_TIMEOUT_PROPERTY = "pack_processing_timeout"; public static final String QUEUE_SUBMIT_STRATEGY_PROPERTY = "submit_strategy"; public static final String QUEUE_PROCESSING_STRATEGY_PROPERTY = "processing_strategy"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/QueueEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/QueueEntity.java index 3791ab4ac3..fefe686c9e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/QueueEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/QueueEntity.java @@ -59,6 +59,9 @@ public class QueueEntity extends BaseSqlEntity { @Column(name = ModelConstants.QUEUE_PARTITIONS_PROPERTY) private int partitions; + @Column(name = ModelConstants.QUEUE_CONSUMER_PER_PARTITION) + private boolean consumerPerPartition; + @Column(name = ModelConstants.QUEUE_PACK_PROCESSING_TIMEOUT_PROPERTY) private long packProcessingTimeout; @@ -83,6 +86,7 @@ public class QueueEntity extends BaseSqlEntity { this.topic = queue.getTopic(); this.pollInterval = queue.getPollInterval(); this.partitions = queue.getPartitions(); + this.consumerPerPartition = queue.isConsumerPerPartition(); this.packProcessingTimeout = queue.getPackProcessingTimeout(); this.submitStrategy = mapper.valueToTree(queue.getSubmitStrategy()); this.processingStrategy = mapper.valueToTree(queue.getProcessingStrategy()); @@ -97,6 +101,7 @@ public class QueueEntity extends BaseSqlEntity { queue.setTopic(topic); queue.setPollInterval(pollInterval); queue.setPartitions(partitions); + queue.setConsumerPerPartition(consumerPerPartition); queue.setPackProcessingTimeout(packProcessingTimeout); queue.setSubmitStrategy(mapper.convertValue(this.submitStrategy, SubmitStrategy.class)); queue.setProcessingStrategy(mapper.convertValue(this.processingStrategy, ProcessingStrategy.class)); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 5335ce6f5f..54c3cbfa93 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -642,6 +642,7 @@ CREATE TABLE IF NOT EXISTS queue( topic varchar(255), poll_interval int, partitions int, + consumer_per_partition boolean, pack_processing_timeout bigint, submit_strategy varchar(255), processing_strategy varchar(255) diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseQueueServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseQueueServiceTest.java index 173f596287..8899224bd5 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseQueueServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseQueueServiceTest.java @@ -150,7 +150,7 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { } @Test(expected = DataValidationException.class) - public void testSaveQueueWithEmptyPoolInterval() { + public void testSaveQueueWithEmptyPollInterval() { Queue queue = new Queue(); queue.setTenantId(tenantId); queue.setName("Test"); diff --git a/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.html b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.html index ae163d9ba7..ae8f08e348 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.html @@ -56,6 +56,16 @@ {{ 'queue.partitions-min-value' | translate }} + + + + + + +
{{ 'queue.consumer-per-partition' | translate }}
+
{{'queue.consumer-per-partition-hint' | translate}}
+
+ queue.processing-timeout @@ -69,7 +79,6 @@
- diff --git a/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.ts b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.ts index b694e21ddf..f9a60773df 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.ts @@ -71,6 +71,7 @@ export class QueueComponent extends EntityComponent { entity && entity.partitions ? entity.partitions : 10, [Validators.min(1), Validators.required] ], + consumerPerPartition: [entity ? entity.consumerPerPartition : false, []], packProcessingTimeout: [ entity && entity.packProcessingTimeout ? entity.packProcessingTimeout : 2000, [Validators.min(1), Validators.required] @@ -118,6 +119,7 @@ export class QueueComponent extends EntityComponent { name: entity.name, pollInterval: entity.pollInterval, partitions: entity.partitions, + consumerPerPartition: entity.consumerPerPartition, packProcessingTimeout: entity.packProcessingTimeout, submitStrategy: { type: entity.submitStrategy?.type, diff --git a/ui-ngx/src/app/shared/models/queue.models.ts b/ui-ngx/src/app/shared/models/queue.models.ts index 2fe2f511f4..508cf2aee1 100644 --- a/ui-ngx/src/app/shared/models/queue.models.ts +++ b/ui-ngx/src/app/shared/models/queue.models.ts @@ -45,6 +45,7 @@ export enum QueueProcessingStrategyTypes { export interface QueueInfo extends BaseData { packProcessingTimeout: number; partitions: number; + consumerPerPartition: boolean, pollInterval: number; processingStrategy: { type: QueueProcessingStrategyTypes, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 75dbdf4f5f..4e30b773b1 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2714,6 +2714,8 @@ "processing-strategy": "Processing Strategy", "poll-interval": "Poll interval", "partitions": "Partitions", + "consumer-per-partition": "Consumer per partition", + "consumer-per-partition-hint": "Enable separate consumer(s) per each partition", "processing-timeout": "Processing timeout", "batch-size": "Batch size", "retries": "Retries (0 - unlimited)", From 8fa493eb1ce6b4155da1f410d99507a371ee49ee Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 6 Apr 2022 16:07:42 +0300 Subject: [PATCH 045/276] UI: Add analogue gauges widget settings forms --- .../widget_bundles/analogue_gauges.json | 15 +- ui-ngx/package.json | 1 - .../widget/data-keys.component.html | 2 +- .../widget/data-keys.component.scss | 67 +- .../widget/lib/analogue-compass.models.ts | 319 -------- .../components/widget/lib/analogue-compass.ts | 10 +- .../widget/lib/analogue-gauge.models.ts | 771 ------------------ .../lib/analogue-linear-gauge.models.ts | 64 +- .../widget/lib/analogue-linear-gauge.ts | 9 +- .../lib/analogue-radial-gauge.models.ts | 31 +- .../widget/lib/analogue-radial-gauge.ts | 10 +- .../cards/label-widget-label.component.html | 6 +- .../cards/label-widget-label.component.ts | 4 +- .../label-widget-settings.component.html | 7 +- .../cards/label-widget-settings.component.ts | 19 +- .../widget-font.component.html} | 30 +- .../widget-font.component.ts} | 38 +- ...gue-compass-widget-settings.component.html | 172 ++++ ...logue-compass-widget-settings.component.ts | 171 ++++ ...logue-gauge-widget-settings.component.html | 336 ++++++++ ...nalogue-gauge-widget-settings.component.ts | 250 ++++++ ...-linear-gauge-widget-settings.component.ts | 66 ++ ...-radial-gauge-widget-settings.component.ts | 57 ++ .../gauge/gauge-highlight.component.html | 60 ++ .../gauge-highlight.component.scss} | 28 +- .../gauge/gauge-highlight.component.ts | 116 +++ .../lib/settings/widget-settings.module.ts | 33 +- .../widget/lib/settings/widget-settings.scss | 19 + .../widget/widget-config.component.html | 20 +- .../widget/widget-config.component.scss | 13 +- .../widget/widget-config.component.ts | 19 +- ui-ngx/src/app/shared/shared.module.ts | 6 +- .../assets/locale/locale.constant-en_US.json | 104 ++- ui-ngx/src/styles.scss | 14 + ui-ngx/yarn.lock | 7 - 35 files changed, 1536 insertions(+), 1358 deletions(-) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{cards/label-widget-font.component.html => common/widget-font.component.html} (67%) rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{cards/label-widget-font.component.ts => common/widget-font.component.ts} (70%) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-compass-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-compass-widget-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-gauge-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-gauge-widget-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-linear-gauge-widget-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-radial-gauge-widget-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/gauge-highlight.component.html rename ui-ngx/src/app/modules/home/components/widget/lib/settings/{cards/label-widget-settings.component.scss => gauge/gauge-highlight.component.scss} (61%) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/gauge-highlight.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/analogue_gauges.json b/application/src/main/data/json/system/widget_bundles/analogue_gauges.json index 5a4eab0522..b2ae314324 100644 --- a/application/src/main/data/json/system/widget_bundles/analogue_gauges.json +++ b/application/src/main/data/json/system/widget_bundles/analogue_gauges.json @@ -18,9 +18,10 @@ "resources": [], "templateHtml": "", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueCompass(self.ctx, 'compass');\n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.getSettingsSchema = function() {\n return TbAnalogueCompass.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueCompass(self.ctx, 'compass');\n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-analogue-compass-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"minorTicks\":22,\"needleCircleSize\":15,\"showBorder\":true,\"borderOuterWidth\":10,\"colorPlate\":\"#222\",\"colorMajorTicks\":\"#f5f5f5\",\"colorMinorTicks\":\"#ddd\",\"colorNeedle\":\"#f08080\",\"colorNeedleCircle\":\"#e8e8e8\",\"colorBorder\":\"#ccc\",\"majorTickFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#ccc\"},\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\",\"animationTarget\":\"needle\",\"majorTicks\":[\"N\",\"NE\",\"E\",\"SE\",\"S\",\"SW\",\"W\",\"NW\"]},\"title\":\"Compass\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -36,9 +37,10 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueLinearGauge(self.ctx, 'linearGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.getSettingsSchema = function() {\n return TbAnalogueLinearGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueLinearGauge(self.ctx, 'linearGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-analogue-linear-gauge-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 30 - 15;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":100,\"defaultColor\":\"#e64a19\",\"barStrokeWidth\":2.5,\"colorBar\":\"rgba(255, 255, 255, 0.4)\",\"colorBarEnd\":\"rgba(221, 221, 221, 0.38)\",\"showUnitTitle\":true,\"minorTicks\":2,\"valueBox\":true,\"valueInt\":3,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"colorNeedleShadowUp\":\"rgba(2,255,255,0.2)\",\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"highlightsWidth\":10,\"animation\":true,\"animationDuration\":1500,\"animationRule\":\"linear\",\"showBorder\":false,\"majorTicksCount\":8,\"numbersFont\":{\"family\":\"Arial\",\"size\":18,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#78909c\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":26,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#37474f\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":40,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#444\",\"shadowColor\":\"rgba(0,0,0,0.3)\"},\"minValue\":-60,\"highlights\":[{\"from\":-60,\"to\":-40,\"color\":\"#90caf9\"},{\"from\":-40,\"to\":-20,\"color\":\"rgba(144, 202, 249, 0.66)\"},{\"from\":-20,\"to\":0,\"color\":\"rgba(144, 202, 249, 0.33)\"},{\"from\":0,\"to\":20,\"color\":\"rgba(244, 67, 54, 0.2)\"},{\"from\":20,\"to\":40,\"color\":\"rgba(244, 67, 54, 0.4)\"},{\"from\":40,\"to\":60,\"color\":\"rgba(244, 67, 54, 0.6)\"},{\"from\":60,\"to\":80,\"color\":\"rgba(244, 67, 54, 0.8)\"},{\"from\":80,\"to\":100,\"color\":\"#f44336\"}],\"unitTitle\":\"Temperature\",\"units\":\"°C\",\"colorBarProgress\":\"#90caf9\",\"colorBarProgressEnd\":\"#f44336\",\"colorBarStroke\":\"#b0bec5\",\"valueDec\":1},\"title\":\"Thermometer scale\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" } }, @@ -54,9 +56,10 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueRadialGauge(self.ctx, 'radialGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.getSettingsSchema = function() {\n return TbAnalogueRadialGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueRadialGauge(self.ctx, 'radialGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-analogue-radial-gauge-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":60,\"startAngle\":67.5,\"ticksAngle\":225,\"showBorder\":true,\"defaultColor\":\"#e65100\",\"needleCircleSize\":7,\"highlights\":[{\"from\":-60,\"to\":-50,\"color\":\"#42a5f5\"},{\"from\":-50,\"to\":-40,\"color\":\"rgba(66, 165, 245, 0.83)\"},{\"from\":-40,\"to\":-30,\"color\":\"rgba(66, 165, 245, 0.66)\"},{\"from\":-30,\"to\":-20,\"color\":\"rgba(66, 165, 245, 0.5)\"},{\"from\":-20,\"to\":-10,\"color\":\"rgba(66, 165, 245, 0.33)\"},{\"from\":-10,\"to\":0,\"color\":\"rgba(66, 165, 245, 0.16)\"},{\"from\":0,\"to\":10,\"color\":\"rgba(229, 115, 115, 0.16)\"},{\"from\":10,\"to\":20,\"color\":\"rgba(229, 115, 115, 0.33)\"},{\"from\":20,\"to\":30,\"color\":\"rgba(229, 115, 115, 0.5)\"},{\"from\":30,\"to\":40,\"color\":\"rgba(229, 115, 115, 0.66)\"},{\"from\":40,\"to\":50,\"color\":\"rgba(229, 115, 115, 0.83)\"},{\"from\":50,\"to\":60,\"color\":\"#e57373\"}],\"showUnitTitle\":true,\"colorPlate\":\"#cfd8dc\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":2,\"valueInt\":3,\"valueDec\":1,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":1000,\"animationRule\":\"bounce\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"units\":\"°C\",\"majorTicksCount\":12,\"numbersFont\":{\"family\":\"Roboto\",\"size\":20,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"size\":30,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\"},\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"unitTitle\":\"Temperature\",\"minValue\":-60},\"title\":\"Temperature radial gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" } }, @@ -72,9 +75,10 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueRadialGauge(self.ctx, 'radialGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.getSettingsSchema = function() {\n return TbAnalogueRadialGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueRadialGauge(self.ctx, 'radialGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-analogue-radial-gauge-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 220) {\\n\\tvalue = 220;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":180,\"startAngle\":45,\"ticksAngle\":270,\"showBorder\":false,\"defaultColor\":\"#e65100\",\"needleCircleSize\":7,\"highlights\":[{\"from\":80,\"to\":120,\"color\":\"#fdd835\"},{\"color\":\"#e57373\",\"from\":120,\"to\":180}],\"showUnitTitle\":false,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":2,\"valueInt\":3,\"minValue\":0,\"valueDec\":0,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":1500,\"animationRule\":\"linear\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"units\":\"MPH\",\"majorTicksCount\":9,\"numbersFont\":{\"family\":\"Roboto\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"size\":32,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\",\"family\":\"Segment7Standard\"},\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\"},\"title\":\"Speed gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" } }, @@ -90,9 +94,10 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueRadialGauge(self.ctx, 'radialGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.getSettingsSchema = function() {\n return TbAnalogueRadialGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.gauge = new TbAnalogueRadialGauge(self.ctx, 'radialGauge'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.gauge.update();\n}\n\nself.onResize = function() {\n self.ctx.gauge.resize();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.gauge.mobileModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n self.ctx.gauge.destroy();\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-analogue-radial-gauge-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nif (value < -100) {\\n\\tvalue = -100;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":100,\"startAngle\":45,\"ticksAngle\":270,\"showBorder\":true,\"defaultColor\":\"#e65100\",\"needleCircleSize\":10,\"highlights\":[],\"showUnitTitle\":true,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":10,\"valueInt\":3,\"valueDec\":0,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"numbersFont\":{\"family\":\"Roboto\",\"size\":18,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"size\":36,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\"},\"minValue\":-100,\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\"},\"title\":\"Radial gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" } } diff --git a/ui-ngx/package.json b/ui-ngx/package.json index a6e28516a3..775454c11a 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -68,7 +68,6 @@ "ngx-clipboard": "^14.0.2", "ngx-color-picker": "^11.0.0", "ngx-daterangepicker-material": "^5.0.1", - "ngx-drag-drop": "^2.0.0", "ngx-flowchart": "https://github.com/thingsboard/ngx-flowchart.git#release/1.0.0", "ngx-hm-carousel": "^2.0.1", "ngx-markdown": "^12.0.1", diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.html b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.html index 216927fe76..3527a5ca2e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.html @@ -17,7 +17,7 @@ --> - { - static get settingsSchema(): JsonSettingsSchema { - return analogueCompassSettingsSchemaValue; - } - constructor(ctx: WidgetContext, canvasId: string) { super(ctx, canvasId); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts index 55fd12fc9f..de57c455d9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts @@ -16,7 +16,6 @@ import * as CanvasGauges from 'canvas-gauges'; import { FontSettings, getFontFamily } from '@home/components/widget/lib/settings.models'; -import { JsonSettingsSchema } from '@shared/models/widget.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { isDefined } from '@core/utils'; import * as tinycolor_ from 'tinycolor2'; @@ -67,776 +66,6 @@ export interface AnalogueGaugeSettings { animationRule: AnimationRule; } -export const analogueGaugeSettingsSchema: JsonSettingsSchema = { - schema: { - type: 'object', - title: 'Settings', - properties: { - minValue: { - title: 'Minimum value', - type: 'number', - default: 0 - }, - maxValue: { - title: 'Maximum value', - type: 'number', - default: 100 - }, - unitTitle: { - title: 'Unit title', - type: 'string', - default: null - }, - showUnitTitle: { - title: 'Show unit title', - type: 'boolean', - default: true - }, - majorTicksCount: { - title: 'Major ticks count', - type: 'number', - default: null - }, - minorTicks: { - title: 'Minor ticks count', - type: 'number', - default: 2 - }, - valueBox: { - title: 'Show value box', - type: 'boolean', - default: true - }, - valueInt: { - title: 'Digits count for integer part of value', - type: 'number', - default: 3 - }, - defaultColor: { - title: 'Default color', - type: 'string', - default: null - }, - colorPlate: { - title: 'Plate color', - type: 'string', - default: '#fff' - }, - colorMajorTicks: { - title: 'Major ticks color', - type: 'string', - default: '#444' - }, - colorMinorTicks: { - title: 'Minor ticks color', - type: 'string', - default: '#666' - }, - colorNeedle: { - title: 'Needle color', - type: 'string', - default: null - }, - colorNeedleEnd: { - title: 'Needle color - end gradient', - type: 'string', - default: null - }, - colorNeedleShadowUp: { - title: 'Upper half of the needle shadow color', - type: 'string', - default: 'rgba(2,255,255,0.2)' - }, - colorNeedleShadowDown: { - title: 'Drop shadow needle color.', - type: 'string', - default: 'rgba(188,143,143,0.45)' - }, - colorValueBoxRect: { - title: 'Value box rectangle stroke color', - type: 'string', - default: '#888' - }, - colorValueBoxRectEnd: { - title: 'Value box rectangle stroke color - end gradient', - type: 'string', - default: '#666' - }, - colorValueBoxBackground: { - title: 'Value box background color', - type: 'string', - default: '#babab2' - }, - colorValueBoxShadow: { - title: 'Value box shadow color', - type: 'string', - default: 'rgba(0,0,0,1)' - }, - highlights: { - title: 'Highlights', - type: 'array', - items: { - title: 'Highlight', - type: 'object', - properties: { - from: { - title: 'From', - type: 'number' - }, - to: { - title: 'To', - type: 'number' - }, - color: { - title: 'Color', - type: 'string' - } - } - } - }, - highlightsWidth: { - title: 'Highlights width', - type: 'number', - default: 15 - }, - showBorder: { - title: 'Show border', - type: 'boolean', - default: true - }, - numbersFont: { - title: 'Tick numbers font', - type: 'object', - properties: { - family: { - title: 'Font family', - type: 'string', - default: 'Roboto' - }, - size: { - title: 'Size', - type: 'number', - default: 18 - }, - style: { - title: 'Style', - type: 'string', - default: 'normal' - }, - weight: { - title: 'Weight', - type: 'string', - default: '500' - }, - color: { - title: 'color', - type: 'string', - default: null - } - } - }, - titleFont: { - title: 'Title text font', - type: 'object', - properties: { - family: { - title: 'Font family', - type: 'string', - default: 'Roboto' - }, - size: { - title: 'Size', - type: 'number', - default: 24 - }, - style: { - title: 'Style', - type: 'string', - default: 'normal' - }, - weight: { - title: 'Weight', - type: 'string', - default: '500' - }, - color: { - title: 'color', - type: 'string', - default: '#888' - } - } - }, - unitsFont: { - title: 'Units text font', - type: 'object', - properties: { - family: { - title: 'Font family', - type: 'string', - default: 'Roboto' - }, - size: { - title: 'Size', - type: 'number', - default: 22 - }, - style: { - title: 'Style', - type: 'string', - default: 'normal' - }, - weight: { - title: 'Weight', - type: 'string', - default: '500' - }, - color: { - title: 'color', - type: 'string', - default: '#888' - } - } - }, - valueFont: { - title: 'Value text font', - type: 'object', - properties: { - family: { - title: 'Font family', - type: 'string', - default: 'Roboto' - }, - size: { - title: 'Size', - type: 'number', - default: 40 - }, - style: { - title: 'Style', - type: 'string', - default: 'normal' - }, - weight: { - title: 'Weight', - type: 'string', - default: '500' - }, - color: { - title: 'color', - type: 'string', - default: '#444' - }, - shadowColor: { - title: 'Shadow color', - type: 'string', - default: 'rgba(0,0,0,0.3)' - } - } - }, - animation: { - title: 'Enable animation', - type: 'boolean', - default: true - }, - animationDuration: { - title: 'Animation duration', - type: 'number', - default: 500 - }, - animationRule: { - title: 'Animation rule', - type: 'string', - default: 'cycle' - } - }, - required: [] - }, - form: [ - 'minValue', - 'maxValue', - 'unitTitle', - 'showUnitTitle', - 'majorTicksCount', - 'minorTicks', - 'valueBox', - 'valueInt', - { - key: 'defaultColor', - type: 'color' - }, - { - key: 'colorPlate', - type: 'color' - }, - { - key: 'colorMajorTicks', - type: 'color' - }, - { - key: 'colorMinorTicks', - type: 'color' - }, - { - key: 'colorNeedle', - type: 'color' - }, - { - key: 'colorNeedleEnd', - type: 'color' - }, - { - key: 'colorNeedleShadowUp', - type: 'color' - }, - { - key: 'colorNeedleShadowDown', - type: 'color' - }, - { - key: 'colorValueBoxRect', - type: 'color' - }, - { - key: 'colorValueBoxRectEnd', - type: 'color' - }, - { - key: 'colorValueBoxBackground', - type: 'color' - }, - { - key: 'colorValueBoxShadow', - type: 'color' - }, - { - key: 'highlights', - items: [ - 'highlights[].from', - 'highlights[].to', - { - key: 'highlights[].color', - type: 'color' - } - ] - }, - 'highlightsWidth', - 'showBorder', - { - key: 'numbersFont', - items: [ - 'numbersFont.family', - 'numbersFont.size', - { - key: 'numbersFont.style', - type: 'rc-select', - multiple: false, - items: [ - { - value: 'normal', - label: 'Normal' - }, - { - value: 'italic', - label: 'Italic' - }, - { - value: 'oblique', - label: 'Oblique' - } - ] - }, - { - key: 'numbersFont.weight', - type: 'rc-select', - multiple: false, - items: [ - { - value: 'normal', - label: 'Normal' - }, - { - value: 'bold', - label: 'Bold' - }, - { - value: 'bolder', - label: 'Bolder' - }, - { - value: 'lighter', - label: 'Lighter' - }, - { - value: '100', - label: '100' - }, - { - value: '200', - label: '200' - }, - { - value: '300', - label: '300' - }, - { - value: '400', - label: '400' - }, - { - value: '500', - label: '500' - }, - { - value: '600', - label: '600' - }, - { - value: '700', - label: '700' - }, - { - value: '800', - label: '800' - }, - { - value: '900', - label: '900' - } - ] - }, - { - key: 'numbersFont.color', - type: 'color' - } - ] - }, - { - key: 'titleFont', - items: [ - 'titleFont.family', - 'titleFont.size', - { - key: 'titleFont.style', - type: 'rc-select', - multiple: false, - items: [ - { - value: 'normal', - label: 'Normal' - }, - { - value: 'italic', - label: 'Italic' - }, - { - value: 'oblique', - label: 'Oblique' - } - ] - }, - { - key: 'titleFont.weight', - type: 'rc-select', - multiple: false, - items: [ - { - value: 'normal', - label: 'Normal' - }, - { - value: 'bold', - label: 'Bold' - }, - { - value: 'bolder', - label: 'Bolder' - }, - { - value: 'lighter', - label: 'Lighter' - }, - { - value: '100', - label: '100' - }, - { - value: '200', - label: '200' - }, - { - value: '300', - label: '300' - }, - { - value: '400', - label: '400' - }, - { - value: '500', - label: '500' - }, - { - value: '600', - label: '600' - }, - { - value: '700', - label: '700' - }, - { - value: '800', - label: '800' - }, - { - value: '900', - label: '900' - } - ] - }, - { - key: 'titleFont.color', - type: 'color' - } - ] - }, - { - key: 'unitsFont', - items: [ - 'unitsFont.family', - 'unitsFont.size', - { - key: 'unitsFont.style', - type: 'rc-select', - multiple: false, - items: [ - { - value: 'normal', - label: 'Normal' - }, - { - value: 'italic', - label: 'Italic' - }, - { - value: 'oblique', - label: 'Oblique' - } - ] - }, - { - key: 'unitsFont.weight', - type: 'rc-select', - multiple: false, - items: [ - { - value: 'normal', - label: 'Normal' - }, - { - value: 'bold', - label: 'Bold' - }, - { - value: 'bolder', - label: 'Bolder' - }, - { - value: 'lighter', - label: 'Lighter' - }, - { - value: '100', - label: '100' - }, - { - value: '200', - label: '200' - }, - { - value: '300', - label: '300' - }, - { - value: '400', - label: '400' - }, - { - value: '500', - label: '500' - }, - { - value: '600', - label: '600' - }, - { - value: '700', - label: '700' - }, - { - value: '800', - label: '800' - }, - { - value: '900', - label: '900' - } - ] - }, - { - key: 'unitsFont.color', - type: 'color' - } - ] - }, - { - key: 'valueFont', - items: [ - 'valueFont.family', - 'valueFont.size', - { - key: 'valueFont.style', - type: 'rc-select', - multiple: false, - items: [ - { - value: 'normal', - label: 'Normal' - }, - { - value: 'italic', - label: 'Italic' - }, - { - value: 'oblique', - label: 'Oblique' - } - ] - }, - { - key: 'valueFont.weight', - type: 'rc-select', - multiple: false, - items: [ - { - value: 'normal', - label: 'Normal' - }, - { - value: 'bold', - label: 'Bold' - }, - { - value: 'bolder', - label: 'Bolder' - }, - { - value: 'lighter', - label: 'Lighter' - }, - { - value: '100', - label: '100' - }, - { - value: '200', - label: '200' - }, - { - value: '300', - label: '300' - }, - { - value: '400', - label: '400' - }, - { - value: '500', - label: '500' - }, - { - value: '600', - label: '600' - }, - { - value: '700', - label: '700' - }, - { - value: '800', - label: '800' - }, - { - value: '900', - label: '900' - } - ] - }, - { - key: 'valueFont.color', - type: 'color' - }, - { - key: 'valueFont.shadowColor', - type: 'color' - } - ] - }, - 'animation', - 'animationDuration', - { - key: 'animationRule', - type: 'rc-select', - multiple: false, - items: [ - { - value: 'linear', - label: 'Linear' - }, - { - value: 'quad', - label: 'Quad' - }, - { - value: 'quint', - label: 'Quint' - }, - { - value: 'cycle', - label: 'Cycle' - }, - { - value: 'bounce', - label: 'Bounce' - }, - { - value: 'elastic', - label: 'Elastic' - }, - { - value: 'dequad', - label: 'Dequad' - }, - { - value: 'dequint', - label: 'Dequint' - }, - { - value: 'decycle', - label: 'Decycle' - }, - { - value: 'debounce', - label: 'Debounce' - }, - { - value: 'delastic', - label: 'Delastic' - } - ] - } - ] -}; - export abstract class TbBaseGauge { private gauge: BaseGauge; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-linear-gauge.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-linear-gauge.models.ts index 29e9acf4ed..eeef69401c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-linear-gauge.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-linear-gauge.models.ts @@ -14,9 +14,7 @@ /// limitations under the License. /// -import { JsonSettingsSchema } from '@shared/models/widget.models'; -import { AnalogueGaugeSettings, analogueGaugeSettingsSchema } from '@home/components/widget/lib/analogue-gauge.models'; -import { deepClone } from '@core/utils'; +import { AnalogueGaugeSettings } from '@home/components/widget/lib/analogue-gauge.models'; export interface AnalogueLinearGaugeSettings extends AnalogueGaugeSettings { barStrokeWidth: number; @@ -26,63 +24,3 @@ export interface AnalogueLinearGaugeSettings extends AnalogueGaugeSettings { colorBarProgress: string; colorBarProgressEnd: string; } - -export function getAnalogueLinearGaugeSettingsSchema(): JsonSettingsSchema { - const analogueLinearGaugeSettingsSchema = deepClone(analogueGaugeSettingsSchema); - analogueLinearGaugeSettingsSchema.schema.properties = - {...analogueLinearGaugeSettingsSchema.schema.properties, ...{ - barStrokeWidth: { - title: 'Bar stroke width', - type: 'number', - default: 2.5 - }, - colorBarStroke: { - title: 'Bar stroke color', - type: 'string', - default: null - }, - colorBar: { - title: 'Bar background color', - type: 'string', - default: '#fff' - }, - colorBarEnd: { - title: 'Bar background color - end gradient', - type: 'string', - default: '#ddd' - }, - colorBarProgress: { - title: 'Progress bar color', - type: 'string', - default: null - }, - colorBarProgressEnd: { - title: 'Progress bar color - end gradient', - type: 'string', - default: null - }}}; - analogueLinearGaugeSettingsSchema.form.unshift( - 'barStrokeWidth', - { - key: 'colorBarStroke', - type: 'color' - }, - { - key: 'colorBar', - type: 'color' - }, - { - key: 'colorBarEnd', - type: 'color' - }, - { - key: 'colorBarProgress', - type: 'color' - }, - { - key: 'colorBarProgressEnd', - type: 'color' - } - ); - return analogueLinearGaugeSettingsSchema; -} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-linear-gauge.ts b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-linear-gauge.ts index 3f88ba1f6f..56ac45eada 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-linear-gauge.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-linear-gauge.ts @@ -19,8 +19,7 @@ import { JsonSettingsSchema } from '@shared/models/widget.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { TbAnalogueGauge } from '@home/components/widget/lib/analogue-gauge.models'; import { - AnalogueLinearGaugeSettings, - getAnalogueLinearGaugeSettingsSchema + AnalogueLinearGaugeSettings } from '@home/components/widget/lib/analogue-linear-gauge.models'; import { isDefined } from '@core/utils'; import * as tinycolor_ from 'tinycolor2'; @@ -30,15 +29,9 @@ import BaseGauge = CanvasGauges.BaseGauge; const tinycolor = tinycolor_; -const analogueLinearGaugeSettingsSchemaValue = getAnalogueLinearGaugeSettingsSchema(); - // @dynamic export class TbAnalogueLinearGauge extends TbAnalogueGauge{ - static get settingsSchema(): JsonSettingsSchema { - return analogueLinearGaugeSettingsSchemaValue; - } - constructor(ctx: WidgetContext, canvasId: string) { super(ctx, canvasId); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-radial-gauge.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-radial-gauge.models.ts index 2204572a97..60cdde278a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-radial-gauge.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-radial-gauge.models.ts @@ -14,39 +14,10 @@ /// limitations under the License. /// -import { JsonSettingsSchema } from '@shared/models/widget.models'; -import { AnalogueGaugeSettings, analogueGaugeSettingsSchema } from '@home/components/widget/lib/analogue-gauge.models'; -import { deepClone } from '@core/utils'; +import { AnalogueGaugeSettings } from '@home/components/widget/lib/analogue-gauge.models'; export interface AnalogueRadialGaugeSettings extends AnalogueGaugeSettings { startAngle: number; ticksAngle: number; needleCircleSize: number; } - -export function getAnalogueRadialGaugeSettingsSchema(): JsonSettingsSchema { - const analogueRadialGaugeSettingsSchema = deepClone(analogueGaugeSettingsSchema); - analogueRadialGaugeSettingsSchema.schema.properties = - {...analogueRadialGaugeSettingsSchema.schema.properties, ...{ - startAngle: { - title: 'Start ticks angle', - type: 'number', - default: 45 - }, - ticksAngle: { - title: 'Ticks angle', - type: 'number', - default: 270 - }, - needleCircleSize: { - title: 'Needle circle size', - type: 'number', - default: 10 - }}}; - analogueRadialGaugeSettingsSchema.form.unshift( - 'startAngle', - 'ticksAngle', - 'needleCircleSize' - ); - return analogueRadialGaugeSettingsSchema; -} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-radial-gauge.ts b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-radial-gauge.ts index 297e229467..08e2e71de6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-radial-gauge.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-radial-gauge.ts @@ -16,25 +16,17 @@ import * as CanvasGauges from 'canvas-gauges'; import { - AnalogueRadialGaugeSettings, - getAnalogueRadialGaugeSettingsSchema + AnalogueRadialGaugeSettings } from '@home/components/widget/lib/analogue-radial-gauge.models'; -import { JsonSettingsSchema } from '@shared/models/widget.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { TbAnalogueGauge } from '@home/components/widget/lib/analogue-gauge.models'; import RadialGauge = CanvasGauges.RadialGauge; import RadialGaugeOptions = CanvasGauges.RadialGaugeOptions; import BaseGauge = CanvasGauges.BaseGauge; -const analogueRadialGaugeSettingsSchemaValue = getAnalogueRadialGaugeSettingsSchema(); - // @dynamic export class TbAnalogueRadialGauge extends TbAnalogueGauge{ - static get settingsSchema(): JsonSettingsSchema { - return analogueRadialGaugeSettingsSchemaValue; - } - constructor(ctx: WidgetContext, canvasId: string) { super(ctx, canvasId); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.html index 6f067e88f9..249ff0a32a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.html @@ -50,11 +50,11 @@
widgets.label-widget.x-pos - + widgets.label-widget.y-pos - +
@@ -64,7 +64,7 @@
widgets.label-widget.font-settings - +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.ts index 0c56e66e6f..594d292704 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.ts @@ -20,14 +20,14 @@ import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { TranslateService } from '@ngx-translate/core'; -import { LabelWidgetFont } from '@home/components/widget/lib/settings/cards/label-widget-font.component'; +import { WidgetFont } from '@home/components/widget/lib/settings/common/widget-font.component'; export interface LabelWidgetLabel { pattern: string; x: number; y: number; backgroundColor: string; - font: LabelWidgetFont; + font: WidgetFont; } @Component({ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.html index 55e4a6816d..472978c097 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.html @@ -23,12 +23,13 @@
widgets.label-widget.labels
-
-
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.ts index cde33be48c..e3ea2473b2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.ts @@ -20,11 +20,12 @@ import { AbstractControl, FormArray, FormBuilder, FormGroup, Validators } from ' import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { LabelWidgetLabel } from '@home/components/widget/lib/settings/cards/label-widget-label.component'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; @Component({ selector: 'tb-label-widget-settings', templateUrl: './label-widget-settings.component.html', - styleUrls: ['./label-widget-settings.component.scss', './../widget-settings.scss'] + styleUrls: ['./../widget-settings.scss'] }) export class LabelWidgetSettingsComponent extends WidgetSettingsComponent { @@ -63,8 +64,9 @@ export class LabelWidgetSettingsComponent extends WidgetSettingsComponent { return this.labelWidgetSettingsForm.get('labels') as FormArray; } - public trackByLabel(index: number, labelControl: AbstractControl): number { - return index; + public trackByLabel(index: number, labelControl: AbstractControl): any { + const label: LabelWidgetLabel = labelControl.value; + return label; } public removeLabel(index: number) { @@ -86,7 +88,16 @@ export class LabelWidgetSettingsComponent extends WidgetSettingsComponent { } }; const labelsArray = this.labelWidgetSettingsForm.get('labels') as FormArray; - labelsArray.push(this.fb.control(label, [Validators.required])); + const labelControl = this.fb.control(label, [Validators.required]); + (labelControl as any).new = true; + labelsArray.push(labelControl); this.labelWidgetSettingsForm.updateValueAndValidity(); } + + labelDrop(event: CdkDragDrop) { + const labelsArray = this.labelWidgetSettingsForm.get('labels') as FormArray; + const label = labelsArray.at(event.previousIndex); + labelsArray.removeAt(event.previousIndex); + labelsArray.insert(event.currentIndex, label); + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-font.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.html similarity index 67% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-font.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.html index 1bd94abe6a..c8f4927c38 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-font.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.html @@ -15,43 +15,43 @@ limitations under the License. --> -
+
- widgets.label-widget.font-family + widgets.widget-font.font-family - widgets.label-widget.relative-font-size + {{ sizeTitle }} - widgets.label-widget.font-style + widgets.widget-font.font-style - {{ 'widgets.label-widget.font-style-normal' | translate }} + {{ 'widgets.widget-font.font-style-normal' | translate }} - {{ 'widgets.label-widget.font-style-italic' | translate }} + {{ 'widgets.widget-font.font-style-italic' | translate }} - {{ 'widgets.label-widget.font-style-oblique' | translate }} + {{ 'widgets.widget-font.font-style-oblique' | translate }} - widgets.label-widget.font-weight + widgets.widget-font.font-weight - {{ 'widgets.label-widget.font-weight-normal' | translate }} + {{ 'widgets.widget-font.font-weight-normal' | translate }} - {{ 'widgets.label-widget.font-weight-bold' | translate }} + {{ 'widgets.widget-font.font-weight-bold' | translate }} - {{ 'widgets.label-widget.font-weight-bolder' | translate }} + {{ 'widgets.widget-font.font-weight-bolder' | translate }} - {{ 'widgets.label-widget.font-weight-lighter' | translate }} + {{ 'widgets.widget-font.font-weight-lighter' | translate }} 100 200 @@ -66,6 +66,10 @@ + label="{{ 'widgets.widget-font.color' | translate }}"> + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-font.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.ts similarity index 70% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-font.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.ts index e535f835c0..6600bb25a1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-font.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.ts @@ -21,38 +21,45 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { TranslateService } from '@ngx-translate/core'; -export interface LabelWidgetFont { +export interface WidgetFont { family: string; size: number; style: 'normal' | 'italic' | 'oblique'; weight: 'normal' | 'bold' | 'bolder' | 'lighter' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'; color: string; + shadowColor?: string; } @Component({ - selector: 'tb-label-widget-font', - templateUrl: './label-widget-font.component.html', + selector: 'tb-widget-font', + templateUrl: './widget-font.component.html', styleUrls: [], providers: [ { provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => LabelWidgetFontComponent), + useExisting: forwardRef(() => WidgetFontComponent), multi: true } ] }) -export class LabelWidgetFontComponent extends PageComponent implements OnInit, ControlValueAccessor { +export class WidgetFontComponent extends PageComponent implements OnInit, ControlValueAccessor { @HostBinding('style.display') display = 'block'; @Input() disabled: boolean; - private modelValue: LabelWidgetFont; + @Input() + hasShadowColor = false; + + @Input() + sizeTitle = 'widgets.widget-font.size'; + + private modelValue: WidgetFont; private propagateChange = null; - public labelWidgetFontFormGroup: FormGroup; + public widgetFontFormGroup: FormGroup; constructor(protected store: Store, private translate: TranslateService, @@ -61,14 +68,17 @@ export class LabelWidgetFontComponent extends PageComponent implements OnInit, C } ngOnInit(): void { - this.labelWidgetFontFormGroup = this.fb.group({ + this.widgetFontFormGroup = this.fb.group({ family: [null, []], size: [null, [Validators.min(1)]], style: [null, []], weight: [null, []], color: [null, []] }); - this.labelWidgetFontFormGroup.valueChanges.subscribe(() => { + if (this.hasShadowColor) { + this.widgetFontFormGroup.addControl('shadowColor', this.fb.control(null, [])); + } + this.widgetFontFormGroup.valueChanges.subscribe(() => { this.updateModel(); }); } @@ -83,21 +93,21 @@ export class LabelWidgetFontComponent extends PageComponent implements OnInit, C setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (isDisabled) { - this.labelWidgetFontFormGroup.disable({emitEvent: false}); + this.widgetFontFormGroup.disable({emitEvent: false}); } else { - this.labelWidgetFontFormGroup.enable({emitEvent: false}); + this.widgetFontFormGroup.enable({emitEvent: false}); } } - writeValue(value: LabelWidgetFont): void { + writeValue(value: WidgetFont): void { this.modelValue = value; - this.labelWidgetFontFormGroup.patchValue( + this.widgetFontFormGroup.patchValue( value, {emitEvent: false} ); } private updateModel() { - const value: LabelWidgetFont = this.labelWidgetFontFormGroup.value; + const value: WidgetFont = this.widgetFontFormGroup.value; this.modelValue = value; this.propagateChange(this.modelValue); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-compass-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-compass-widget-settings.component.html new file mode 100644 index 0000000000..2e27effaf6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-compass-widget-settings.component.html @@ -0,0 +1,172 @@ + +
+
+ widgets.gauge.ticks-settings + + widgets.gauge.major-ticks-names + + + {{tickName}} + cancel + + + + + + +
+ + widgets.gauge.minor-ticks-count + + + + +
+ + {{ 'widgets.gauge.show-stroke-ticks' | translate }} + +
+ widgets.gauge.major-ticks-font + +
+
+
+ widgets.gauge.plate-settings + + + + {{ 'widgets.gauge.show-plate-border' | translate }} + +
+ + + + widgets.gauge.border-width + + +
+
+
+ widgets.gauge.needle-settings + + widgets.gauge.needle-circle-size + + +
+ + + + +
+
+
+ widgets.gauge.animation-settings + + + + + {{ 'widgets.gauge.enable-animation' | translate }} + + + + widget-config.advanced-settings + + + +
+ + widgets.gauge.animation-duration + + + + widgets.gauge.animation-rule + + + {{ 'widgets.gauge.animation-linear' | translate }} + + + {{ 'widgets.gauge.animation-quad' | translate }} + + + {{ 'widgets.gauge.animation-quint' | translate }} + + + {{ 'widgets.gauge.animation-cycle' | translate }} + + + {{ 'widgets.gauge.animation-bounce' | translate }} + + + {{ 'widgets.gauge.animation-elastic' | translate }} + + + {{ 'widgets.gauge.animation-dequad' | translate }} + + + {{ 'widgets.gauge.animation-dequint' | translate }} + + + {{ 'widgets.gauge.animation-decycle' | translate }} + + + {{ 'widgets.gauge.animation-debounce' | translate }} + + + {{ 'widgets.gauge.animation-delastic' | translate }} + + + + + widgets.gauge.animation-target + + + {{ 'widgets.gauge.animation-target-needle' | translate }} + + + {{ 'widgets.gauge.animation-target-plate' | translate }} + + + +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-compass-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-compass-widget-settings.component.ts new file mode 100644 index 0000000000..4f110e2e8e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-compass-widget-settings.component.ts @@ -0,0 +1,171 @@ +/// +/// Copyright © 2016-2022 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 { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Component } from '@angular/core'; +import { MatChipInputEvent } from '@angular/material/chips'; +import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; +import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; + +@Component({ + selector: 'tb-analogue-compass-widget-settings', + templateUrl: './analogue-compass-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class AnalogueCompassWidgetSettingsComponent extends WidgetSettingsComponent { + + readonly separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; + + analogueCompassWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + protected fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.analogueCompassWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + majorTicks: [], + minorTicks: 22, + showStrokeTicks: false, + needleCircleSize: 10, + showBorder: true, + borderOuterWidth: 10, + colorPlate: '#222', + colorMajorTicks: '#f5f5f5', + colorMinorTicks: '#ddd', + colorNeedle: '#f08080', + colorNeedleCircle: '#e8e8e8', + colorBorder: '#ccc', + majorTickFont: { + family: 'Roboto', + size: 20, + style: 'normal', + weight: '500', + color: '#ccc' + }, + animation: true, + animationDuration: 500, + animationRule: 'cycle', + animationTarget: 'needle' + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.analogueCompassWidgetSettingsForm = this.fb.group({ + + // Ticks settings + majorTicks: [settings.majorTicks, []], + colorMajorTicks: [settings.colorMajorTicks, []], + minorTicks: [settings.minorTicks, [Validators.min(0)]], + colorMinorTicks: [settings.colorMinorTicks, []], + showStrokeTicks: [settings.showStrokeTicks, []], + majorTickFont: [settings.majorTickFont, []], + + // Plate settings + colorPlate: [settings.colorPlate, []], + showBorder: [settings.showBorder, []], + colorBorder: [settings.colorBorder, []], + borderOuterWidth: [settings.borderOuterWidth, [Validators.min(0)]], + + // Needle settings + needleCircleSize: [settings.needleCircleSize, [Validators.min(0)]], + colorNeedle: [settings.colorNeedle, []], + colorNeedleCircle: [settings.colorNeedleCircle, []], + + // Animation settings + animation: [settings.animation, []], + animationDuration: [settings.animationDuration, [Validators.min(0)]], + animationRule: [settings.animationRule, []], + animationTarget: [settings.animationTarget, []] + }); + } + + protected validatorTriggers(): string[] { + return ['showBorder', 'animation']; + } + + protected updateValidators(emitEvent: boolean) { + const showBorder: boolean = this.analogueCompassWidgetSettingsForm.get('showBorder').value; + const animation: boolean = this.analogueCompassWidgetSettingsForm.get('animation').value; + if (showBorder) { + this.analogueCompassWidgetSettingsForm.get('colorBorder').enable(); + this.analogueCompassWidgetSettingsForm.get('borderOuterWidth').enable(); + } else { + this.analogueCompassWidgetSettingsForm.get('colorBorder').disable(); + this.analogueCompassWidgetSettingsForm.get('borderOuterWidth').disable(); + } + if (animation) { + this.analogueCompassWidgetSettingsForm.get('animationDuration').enable(); + this.analogueCompassWidgetSettingsForm.get('animationRule').enable(); + this.analogueCompassWidgetSettingsForm.get('animationTarget').enable(); + } else { + this.analogueCompassWidgetSettingsForm.get('animationDuration').disable(); + this.analogueCompassWidgetSettingsForm.get('animationRule').disable(); + this.analogueCompassWidgetSettingsForm.get('animationTarget').disable(); + } + this.analogueCompassWidgetSettingsForm.get('colorBorder').updateValueAndValidity({emitEvent}); + this.analogueCompassWidgetSettingsForm.get('borderOuterWidth').updateValueAndValidity({emitEvent}); + this.analogueCompassWidgetSettingsForm.get('animationDuration').updateValueAndValidity({emitEvent}); + this.analogueCompassWidgetSettingsForm.get('animationRule').updateValueAndValidity({emitEvent}); + this.analogueCompassWidgetSettingsForm.get('animationTarget').updateValueAndValidity({emitEvent}); + } + + majorTicksNamesList(): string[] { + return this.analogueCompassWidgetSettingsForm.get('majorTicks').value; + } + + removeMajorTickName(tickName: string): void { + const tickNames: string[] = this.analogueCompassWidgetSettingsForm.get('majorTicks').value; + const index = tickNames.indexOf(tickName); + if (index >= 0) { + tickNames.splice(index, 1); + this.analogueCompassWidgetSettingsForm.get('majorTicks').setValue(tickNames); + this.analogueCompassWidgetSettingsForm.get('majorTicks').markAsDirty(); + } + } + + addMajorTickName(event: MatChipInputEvent): void { + const input = event.input; + const value = event.value; + + const tickNames: string[] = this.analogueCompassWidgetSettingsForm.get('majorTicks').value; + + if ((value || '').trim()) { + tickNames.push(value.trim()); + this.analogueCompassWidgetSettingsForm.get('majorTicks').setValue(tickNames); + this.analogueCompassWidgetSettingsForm.get('majorTicks').markAsDirty(); + } + + if (input) { + input.value = ''; + } + } + + majorTickNameDrop(event: CdkDragDrop): void { + const tickNames: string[] = this.analogueCompassWidgetSettingsForm.get('majorTicks').value; + moveItemInArray(tickNames, event.previousIndex, event.currentIndex); + this.analogueCompassWidgetSettingsForm.get('majorTicks').setValue(tickNames); + this.analogueCompassWidgetSettingsForm.get('majorTicks').markAsDirty(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-gauge-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-gauge-widget-settings.component.html new file mode 100644 index 0000000000..b586c31f90 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-gauge-widget-settings.component.html @@ -0,0 +1,336 @@ + +
+ + + + + + + + + + +
+ widgets.gauge.ticks-settings +
+ + widgets.gauge.min-value + + + + widgets.gauge.max-value + + +
+
+ + widgets.gauge.major-ticks-count + + + + +
+
+ + widgets.gauge.minor-ticks-count + + + + +
+
+ widgets.gauge.tick-numbers-font + +
+
+
+ widgets.gauge.unit-title-settings + + + + + {{ 'widgets.gauge.show-unit-title' | translate }} + + + + widget-config.advanced-settings + + + +
+ + widgets.gauge.unit-title + + +
+ widgets.gauge.title-font + +
+
+
+
+
+
+ widgets.gauge.units-settings +
+ widgets.gauge.units-font + +
+
+
+ widgets.gauge.value-box-settings + + + + + {{ 'widgets.gauge.show-value-box' | translate }} + + + + widget-config.advanced-settings + + + +
+ + widgets.gauge.value-int + + +
+ widgets.gauge.value-font + +
+
+ + + + +
+
+ + + + +
+
+
+
+
+
+ widgets.gauge.plate-settings + + + + {{ 'widgets.gauge.show-plate-border' | translate }} + +
+
+ widgets.gauge.needle-settings +
+ + + + +
+
+ + + + +
+
+
+ widgets.gauge.highlights-settings + + widgets.gauge.highlights-width + + +
+ widgets.gauge.highlights +
+
+
+ + +
+
+
+ widgets.gauge.no-highlights +
+
+ +
+
+
+
+
+ widgets.gauge.animation-settings + + + + + {{ 'widgets.gauge.enable-animation' | translate }} + + + + widget-config.advanced-settings + + + +
+ + widgets.gauge.animation-duration + + + + widgets.gauge.animation-rule + + + {{ 'widgets.gauge.animation-linear' | translate }} + + + {{ 'widgets.gauge.animation-quad' | translate }} + + + {{ 'widgets.gauge.animation-quint' | translate }} + + + {{ 'widgets.gauge.animation-cycle' | translate }} + + + {{ 'widgets.gauge.animation-bounce' | translate }} + + + {{ 'widgets.gauge.animation-elastic' | translate }} + + + {{ 'widgets.gauge.animation-dequad' | translate }} + + + {{ 'widgets.gauge.animation-dequint' | translate }} + + + {{ 'widgets.gauge.animation-decycle' | translate }} + + + {{ 'widgets.gauge.animation-debounce' | translate }} + + + {{ 'widgets.gauge.animation-delastic' | translate }} + + + +
+
+
+
+
+ + +
+ widgets.gauge.radial-gauge-settings +
+ + widgets.gauge.start-ticks-angle + + + + widgets.gauge.ticks-angle + + +
+ + widgets.gauge.needle-circle-size + + +
+
+ + +
+ widgets.gauge.linear-gauge-settings +
+ + widgets.gauge.bar-stroke-width + + + + +
+
+ + + + +
+
+ + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-gauge-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-gauge-widget-settings.component.ts new file mode 100644 index 0000000000..246a268e93 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-gauge-widget-settings.component.ts @@ -0,0 +1,250 @@ +/// +/// Copyright © 2016-2022 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 { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { AbstractControl, FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { GaugeHighlight } from '@home/components/widget/lib/settings/gauge/gauge-highlight.component'; +import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; + +export class AnalogueGaugeWidgetSettingsComponent extends WidgetSettingsComponent { + + analogueGaugeWidgetSettingsForm: FormGroup; + + ctx = { + settingsForm: null + }; + + constructor(protected store: Store, + protected fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.analogueGaugeWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + startAngle: 45, + ticksAngle: 270, + needleCircleSize: 10, + minValue: 0, + maxValue: 100, + showUnitTitle: true, + unitTitle: null, + majorTicksCount: null, + minorTicks: 2, + valueBox: true, + valueInt: 3, + defaultColor: null, + colorPlate: '#fff', + colorMajorTicks: '#444', + colorMinorTicks: '#666', + colorNeedle: null, + colorNeedleEnd: null, + colorNeedleShadowUp: 'rgba(2,255,255,0.2)', + colorNeedleShadowDown: 'rgba(188,143,143,0.45)', + colorValueBoxRect: '#888', + colorValueBoxRectEnd: '#666', + colorValueBoxBackground: '#babab2', + colorValueBoxShadow: 'rgba(0,0,0,1)', + highlights: [], + highlightsWidth: 15, + showBorder: true, + numbersFont: { + family: 'Roboto', + size: 18, + style: 'normal', + weight: '500', + color: null + }, + titleFont: { + family: 'Roboto', + size: 24, + style: 'normal', + weight: '500', + color: '#888' + }, + unitsFont: { + family: 'Roboto', + size: 22, + style: 'normal', + weight: '500', + color: '#888' + }, + valueFont: { + family: 'Roboto', + size: 40, + style: 'normal', + weight: '500', + color: '#444', + shadowColor: 'rgba(0,0,0,0.3)' + }, + animation: true, + animationDuration: 500, + animationRule: 'cycle' + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + const highlightsControls: Array = []; + if (settings.highlights) { + settings.highlights.forEach((highlight) => { + highlightsControls.push(this.fb.control(highlight, [Validators.required])); + }); + } + this.analogueGaugeWidgetSettingsForm = this.fb.group({ + + // Radial gauge settings + startAngle: [settings.startAngle, [Validators.min(0), Validators.max(360)]], + ticksAngle: [settings.ticksAngle, [Validators.min(0), Validators.max(360)]], + needleCircleSize: [settings.needleCircleSize, [Validators.min(0)]], + + defaultColor: [settings.defaultColor, []], + + // Ticks settings + minValue: [settings.minValue, []], + maxValue: [settings.maxValue, []], + majorTicksCount: [settings.majorTicksCount, [Validators.min(0)]], + colorMajorTicks: [settings.colorMajorTicks, []], + minorTicks: [settings.majorTicksCount, [Validators.min(0)]], + colorMinorTicks: [settings.colorMinorTicks, []], + numbersFont: [settings.numbersFont, []], + + // Unit title settings + showUnitTitle: [settings.showUnitTitle, []], + unitTitle: [settings.unitTitle, []], + titleFont: [settings.titleFont, []], + + // Units settings + unitsFont: [settings.unitsFont, []], + + // Value box settings + valueBox: [settings.valueBox, []], + valueInt: [settings.valueInt, [Validators.min(0)]], + valueFont: [settings.valueFont, []], + colorValueBoxRect: [settings.colorValueBoxRect, []], + colorValueBoxRectEnd: [settings.colorValueBoxRectEnd, []], + colorValueBoxBackground: [settings.colorValueBoxBackground, []], + colorValueBoxShadow: [settings.colorValueBoxShadow, []], + + // Plate settings + showBorder: [settings.showBorder, []], + colorPlate: [settings.colorPlate, []], + + // Needle settings + colorNeedle: [settings.colorNeedle, []], + colorNeedleEnd: [settings.colorNeedleEnd, []], + colorNeedleShadowUp: [settings.colorNeedleShadowUp, []], + colorNeedleShadowDown: [settings.colorNeedleShadowDown, []], + + // Highlights settings + highlightsWidth: [settings.highlightsWidth, [Validators.min(0)]], + highlights: this.fb.array(highlightsControls), + + // Animation settings + animation: [settings.animation, []], + animationDuration: [settings.animationDuration, [Validators.min(0)]], + animationRule: [settings.animationRule, []], + }); + this.ctx.settingsForm = this.analogueGaugeWidgetSettingsForm; + } + + protected validatorTriggers(): string[] { + return ['showUnitTitle', 'valueBox', 'animation']; + } + + protected updateValidators(emitEvent: boolean) { + const showUnitTitle: boolean = this.analogueGaugeWidgetSettingsForm.get('showUnitTitle').value; + const valueBox: boolean = this.analogueGaugeWidgetSettingsForm.get('valueBox').value; + const animation: boolean = this.analogueGaugeWidgetSettingsForm.get('animation').value; + if (showUnitTitle) { + this.analogueGaugeWidgetSettingsForm.get('unitTitle').enable(); + this.analogueGaugeWidgetSettingsForm.get('titleFont').enable(); + } else { + this.analogueGaugeWidgetSettingsForm.get('unitTitle').disable(); + this.analogueGaugeWidgetSettingsForm.get('titleFont').disable(); + } + if (valueBox) { + this.analogueGaugeWidgetSettingsForm.get('valueInt').enable(); + this.analogueGaugeWidgetSettingsForm.get('valueFont').enable(); + this.analogueGaugeWidgetSettingsForm.get('colorValueBoxRect').enable(); + this.analogueGaugeWidgetSettingsForm.get('colorValueBoxRectEnd').enable(); + this.analogueGaugeWidgetSettingsForm.get('colorValueBoxBackground').enable(); + this.analogueGaugeWidgetSettingsForm.get('colorValueBoxShadow').enable(); + } else { + this.analogueGaugeWidgetSettingsForm.get('valueInt').disable(); + this.analogueGaugeWidgetSettingsForm.get('valueFont').disable(); + this.analogueGaugeWidgetSettingsForm.get('colorValueBoxRect').disable(); + this.analogueGaugeWidgetSettingsForm.get('colorValueBoxRectEnd').disable(); + this.analogueGaugeWidgetSettingsForm.get('colorValueBoxBackground').disable(); + this.analogueGaugeWidgetSettingsForm.get('colorValueBoxShadow').disable(); + } + if (animation) { + this.analogueGaugeWidgetSettingsForm.get('animationDuration').enable(); + this.analogueGaugeWidgetSettingsForm.get('animationRule').enable(); + } else { + this.analogueGaugeWidgetSettingsForm.get('animationDuration').disable(); + this.analogueGaugeWidgetSettingsForm.get('animationRule').disable(); + } + this.analogueGaugeWidgetSettingsForm.get('unitTitle').updateValueAndValidity({emitEvent}); + this.analogueGaugeWidgetSettingsForm.get('titleFont').updateValueAndValidity({emitEvent}); + this.analogueGaugeWidgetSettingsForm.get('valueInt').updateValueAndValidity({emitEvent}); + this.analogueGaugeWidgetSettingsForm.get('valueFont').updateValueAndValidity({emitEvent}); + this.analogueGaugeWidgetSettingsForm.get('colorValueBoxRect').updateValueAndValidity({emitEvent}); + this.analogueGaugeWidgetSettingsForm.get('colorValueBoxRectEnd').updateValueAndValidity({emitEvent}); + this.analogueGaugeWidgetSettingsForm.get('colorValueBoxBackground').updateValueAndValidity({emitEvent}); + this.analogueGaugeWidgetSettingsForm.get('colorValueBoxShadow').updateValueAndValidity({emitEvent}); + this.analogueGaugeWidgetSettingsForm.get('animationDuration').updateValueAndValidity({emitEvent}); + this.analogueGaugeWidgetSettingsForm.get('animationRule').updateValueAndValidity({emitEvent}); + } + + highlightsFormArray(): FormArray { + return this.analogueGaugeWidgetSettingsForm.get('highlights') as FormArray; + } + + public trackByHighlight(index: number, highlightControl: AbstractControl): any { + const highlight: GaugeHighlight = highlightControl.value; + return highlight; + } + + public removeHighlight(index: number) { + (this.analogueGaugeWidgetSettingsForm.get('highlights') as FormArray).removeAt(index); + } + + public addHighlight() { + const highlight: GaugeHighlight = { + from: null, + to: null, + color: null + }; + const highlightsArray = this.analogueGaugeWidgetSettingsForm.get('highlights') as FormArray; + const highlightControl = this.fb.control(highlight, [Validators.required]); + (highlightControl as any).new = true; + highlightsArray.push(highlightControl); + this.analogueGaugeWidgetSettingsForm.updateValueAndValidity(); + } + + highlightDrop(event: CdkDragDrop) { + const highlightsArray = this.analogueGaugeWidgetSettingsForm.get('highlights') as FormArray; + const highlight = highlightsArray.at(event.previousIndex); + highlightsArray.removeAt(event.previousIndex); + highlightsArray.insert(event.currentIndex, highlight); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-linear-gauge-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-linear-gauge-widget-settings.component.ts new file mode 100644 index 0000000000..d5a424cb6d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-linear-gauge-widget-settings.component.ts @@ -0,0 +1,66 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings } from '@shared/models/widget.models'; +import { FormBuilder, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + AnalogueGaugeWidgetSettingsComponent +} from '@home/components/widget/lib/settings/gauge/analogue-gauge-widget-settings.component'; + +@Component({ + selector: 'tb-analogue-linear-gauge-widget-settings', + templateUrl: './analogue-gauge-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class AnalogueLinearGaugeWidgetSettingsComponent extends AnalogueGaugeWidgetSettingsComponent { + + gaugeType = 'linear'; + + constructor(protected store: Store, + protected fb: FormBuilder) { + super(store, fb); + } + + protected defaultSettings(): WidgetSettings { + const settings = super.defaultSettings(); + settings.barStrokeWidth = 2.5; + settings.colorBarStroke = null; + settings.colorBar = '#fff'; + settings.colorBarEnd = '#ddd'; + settings.colorBarProgress = null; + settings.colorBarProgressEnd = null; + return settings; + } + + protected onSettingsSet(settings: WidgetSettings) { + super.onSettingsSet(settings); + this.analogueGaugeWidgetSettingsForm.addControl('barStrokeWidth', + this.fb.control(settings.barStrokeWidth, [Validators.min(0)])); + this.analogueGaugeWidgetSettingsForm.addControl('colorBarStroke', + this.fb.control(settings.colorBarStroke, [])); + this.analogueGaugeWidgetSettingsForm.addControl('colorBar', + this.fb.control(settings.colorBar, [])); + this.analogueGaugeWidgetSettingsForm.addControl('colorBarEnd', + this.fb.control(settings.colorBarEnd, [])); + this.analogueGaugeWidgetSettingsForm.addControl('colorBarProgress', + this.fb.control(settings.colorBarProgress, [])); + this.analogueGaugeWidgetSettingsForm.addControl('colorBarProgressEnd', + this.fb.control(settings.colorBarProgressEnd, [])); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-radial-gauge-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-radial-gauge-widget-settings.component.ts new file mode 100644 index 0000000000..0641fb5ab3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-radial-gauge-widget-settings.component.ts @@ -0,0 +1,57 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings } from '@shared/models/widget.models'; +import { FormBuilder, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + AnalogueGaugeWidgetSettingsComponent +} from '@home/components/widget/lib/settings/gauge/analogue-gauge-widget-settings.component'; + +@Component({ + selector: 'tb-analogue-radial-gauge-widget-settings', + templateUrl: './analogue-gauge-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class AnalogueRadialGaugeWidgetSettingsComponent extends AnalogueGaugeWidgetSettingsComponent { + + gaugeType = 'radial'; + + constructor(protected store: Store, + protected fb: FormBuilder) { + super(store, fb); + } + + protected defaultSettings(): WidgetSettings { + const settings = super.defaultSettings(); + settings.startAngle = 45; + settings.ticksAngle = 270; + settings.needleCircleSize = 10; + return settings; + } + + protected onSettingsSet(settings: WidgetSettings) { + super.onSettingsSet(settings); + this.analogueGaugeWidgetSettingsForm.addControl('startAngle', + this.fb.control(settings.startAngle, [Validators.min(0), Validators.max(360)])); + this.analogueGaugeWidgetSettingsForm.addControl('ticksAngle', + this.fb.control(settings.ticksAngle, [Validators.min(0), Validators.max(360)])); + this.analogueGaugeWidgetSettingsForm.addControl('needleCircleSize', + this.fb.control(settings.needleCircleSize, [Validators.min(0)])); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/gauge-highlight.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/gauge-highlight.component.html new file mode 100644 index 0000000000..3e1ffde7a3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/gauge-highlight.component.html @@ -0,0 +1,60 @@ + + + +
+ +
+
{{ highlightRangeText() }}
+
+
+
+
+
+ + +
+
+ +
+ +
+
+ + widgets.gauge.highlight-from + + + + widgets.gauge.highlight-to + + +
+ + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/gauge-highlight.component.scss similarity index 61% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/gauge-highlight.component.scss index 563f2ae465..bbed5bf6eb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/gauge-highlight.component.scss @@ -13,20 +13,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import '../../../../../../../../scss/constants'; - :host { - .tb-label-widget-labels { - overflow-y: auto; - &.mat-padding { - padding: 8px; - @media #{$mat-gt-sm} { - padding: 16px; + display: block; + .mat-expansion-panel { + box-shadow: none; + &.gauge-highlight { + border: 1px groove rgba(0, 0, 0, .25); + .mat-expansion-panel-header { + padding: 0 24px 0 8px; + &.mat-expanded { + height: 48px; + } } } } +} - .tb-prompt{ - margin: 30px 0; +:host ::ng-deep { + .mat-expansion-panel { + &.gauge-highlight { + .mat-expansion-panel-body { + padding: 0 8px 8px; + } + } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/gauge-highlight.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/gauge-highlight.component.ts new file mode 100644 index 0000000000..697600562e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/gauge-highlight.component.ts @@ -0,0 +1,116 @@ +/// +/// Copyright © 2016-2022 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 { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { isNumber } from '@core/utils'; + +export interface GaugeHighlight { + from: number; + to: number; + color: string; +} + +@Component({ + selector: 'tb-gauge-highlight', + templateUrl: './gauge-highlight.component.html', + styleUrls: ['./gauge-highlight.component.scss', './../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => GaugeHighlightComponent), + multi: true + } + ] +}) +export class GaugeHighlightComponent extends PageComponent implements OnInit, ControlValueAccessor { + + @Input() + disabled: boolean; + + @Input() + expanded = false; + + @Output() + removeHighlight = new EventEmitter(); + + private modelValue: GaugeHighlight; + + private propagateChange = null; + + public gaugeHighlightFormGroup: FormGroup; + + constructor(protected store: Store, + private translate: TranslateService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.gaugeHighlightFormGroup = this.fb.group({ + from: [null, []], + to: [null, []], + color: [null, []] + }); + this.gaugeHighlightFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.gaugeHighlightFormGroup.disable({emitEvent: false}); + } else { + this.gaugeHighlightFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: GaugeHighlight): void { + this.modelValue = value; + this.gaugeHighlightFormGroup.patchValue( + value, {emitEvent: false} + ); + } + + highlightRangeText(): string { + const value: GaugeHighlight = this.gaugeHighlightFormGroup.value; + const from = isNumber(value.from) ? value.from : 0; + const to = isNumber(value.to) ? value.to : 0; + return `${from} - ${to}`; + } + + private updateModel() { + const value: GaugeHighlight = this.gaugeHighlightFormGroup.value; + this.modelValue = value; + if (this.gaugeHighlightFormGroup.valid) { + this.propagateChange(this.modelValue); + } else { + this.propagateChange(null); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index 14e5cc21bf..8e0f2b687d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -32,7 +32,7 @@ import { import { MarkdownWidgetSettingsComponent } from '@home/components/widget/lib/settings/cards/markdown-widget-settings.component'; -import { LabelWidgetFontComponent } from '@home/components/widget/lib/settings/cards/label-widget-font.component'; +import { WidgetFontComponent } from '@home/components/widget/lib/settings/common/widget-font.component'; import { LabelWidgetLabelComponent } from '@home/components/widget/lib/settings/cards/label-widget-label.component'; import { LabelWidgetSettingsComponent } from '@home/components/widget/lib/settings/cards/label-widget-settings.component'; import { @@ -59,6 +59,16 @@ import { import { AlarmsTableKeySettingsComponent } from '@home/components/widget/lib/settings/alarm/alarms-table-key-settings.component'; +import { GaugeHighlightComponent } from '@home/components/widget/lib/settings/gauge/gauge-highlight.component'; +import { + AnalogueRadialGaugeWidgetSettingsComponent +} from '@home/components/widget/lib/settings/gauge/analogue-radial-gauge-widget-settings.component'; +import { + AnalogueLinearGaugeWidgetSettingsComponent +} from '@home/components/widget/lib/settings/gauge/analogue-linear-gauge-widget-settings.component'; +import { + AnalogueCompassWidgetSettingsComponent +} from '@home/components/widget/lib/settings/gauge/analogue-compass-widget-settings.component'; @NgModule({ declarations: [ @@ -67,7 +77,7 @@ import { TimeseriesTableKeySettingsComponent, TimeseriesTableLatestKeySettingsComponent, MarkdownWidgetSettingsComponent, - LabelWidgetFontComponent, + WidgetFontComponent, LabelWidgetLabelComponent, LabelWidgetSettingsComponent, SimpleCardWidgetSettingsComponent, @@ -77,7 +87,11 @@ import { EntitiesTableWidgetSettingsComponent, EntitiesTableKeySettingsComponent, AlarmsTableWidgetSettingsComponent, - AlarmsTableKeySettingsComponent + AlarmsTableKeySettingsComponent, + GaugeHighlightComponent, + AnalogueRadialGaugeWidgetSettingsComponent, + AnalogueLinearGaugeWidgetSettingsComponent, + AnalogueCompassWidgetSettingsComponent ], imports: [ CommonModule, @@ -90,7 +104,7 @@ import { TimeseriesTableKeySettingsComponent, TimeseriesTableLatestKeySettingsComponent, MarkdownWidgetSettingsComponent, - LabelWidgetFontComponent, + WidgetFontComponent, LabelWidgetLabelComponent, LabelWidgetSettingsComponent, SimpleCardWidgetSettingsComponent, @@ -100,7 +114,11 @@ import { EntitiesTableWidgetSettingsComponent, EntitiesTableKeySettingsComponent, AlarmsTableWidgetSettingsComponent, - AlarmsTableKeySettingsComponent + AlarmsTableKeySettingsComponent, + GaugeHighlightComponent, + AnalogueRadialGaugeWidgetSettingsComponent, + AnalogueLinearGaugeWidgetSettingsComponent, + AnalogueCompassWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -120,5 +138,8 @@ export const widgetSettingsComponentsMap: {[key: string]: Type
- - - + + class="tb-datasource-list-item tb-draggable" cdkDrag + [cdkDragDisabled]="disabled">
+ label="{{ 'widgets.label-widget.background-color' | translate }}" openOnInput colorClearButton>
widgets.label-widget.font-settings diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html new file mode 100644 index 0000000000..d4e1e669f6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html @@ -0,0 +1,76 @@ + +
+ + widgets.value-source.value-source + + + {{ 'widgets.value-source.predefined-value' | translate }} + + + {{ 'widgets.value-source.entity-attribute' | translate }} + + + + + widgets.value-source.value + + +
+ + {{ 'widgets.value-source.source-entity-alias' | translate }} + + + + + + + + + + {{ 'widgets.value-source.source-entity-attribute' | translate }} + + + + + + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.ts new file mode 100644 index 0000000000..144904ca41 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.ts @@ -0,0 +1,255 @@ +/// +/// Copyright © 2016-2022 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 { Component, ElementRef, forwardRef, HostBinding, Input, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { IAliasController } from '@core/api/widget-api.models'; +import { Observable, of } from 'rxjs'; +import { catchError, map, mergeMap, publishReplay, refCount, tap } from 'rxjs/operators'; +import { DataKey } from '@shared/models/widget.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { EntityService } from '@core/http/entity.service'; + +export declare type ValueSource = 'predefinedValue' | 'entityAttribute'; + +export interface ValueSourceProperty { + valueSource: ValueSource; + entityAlias?: string; + attribute?: string; + value?: number; +} + +@Component({ + selector: 'tb-value-source', + templateUrl: './value-source.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ValueSourceComponent), + multi: true + } + ] +}) +export class ValueSourceComponent extends PageComponent implements OnInit, ControlValueAccessor { + + @HostBinding('style.display') display = 'block'; + + @ViewChild('entityAliasInput') entityAliasInput: ElementRef; + + @ViewChild('keyInput') keyInput: ElementRef; + + @Input() + disabled: boolean; + + @Input() + aliasController: IAliasController; + + private modelValue: ValueSourceProperty; + + private propagateChange = null; + + public valueSourceFormGroup: FormGroup; + + filteredEntityAliases: Observable>; + aliasSearchText = ''; + + filteredKeys: Observable>; + keySearchText = ''; + + private latestKeySearchResult: Array = null; + private keysFetchObservable$: Observable> = null; + + private entityAliasList: Array = []; + + constructor(protected store: Store, + private translate: TranslateService, + private entityService: EntityService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.valueSourceFormGroup = this.fb.group({ + valueSource: ['predefinedValue', []], + entityAlias: [null, []], + attribute: [null, []], + value: [null, []] + }); + this.valueSourceFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + this.valueSourceFormGroup.get('valueSource').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.updateValidators(false); + + this.filteredEntityAliases = this.valueSourceFormGroup.get('entityAlias').valueChanges + .pipe( + tap(() => { + this.latestKeySearchResult = null; + this.keysFetchObservable$ = null; + this.valueSourceFormGroup.get('attribute').setValue(this.valueSourceFormGroup.get('attribute').value); + }), + map(value => value ? value : ''), + mergeMap(name => this.fetchEntityAliases(name) ) + ); + + this.filteredKeys = this.valueSourceFormGroup.get('attribute').valueChanges + .pipe( + map(value => value ? value : ''), + mergeMap(name => this.fetchKeys(name) ) + ); + + if (this.aliasController) { + const entityAliases = this.aliasController.getEntityAliases(); + for (const aliasId of Object.keys(entityAliases)) { + this.entityAliasList.push(entityAliases[aliasId].alias); + } + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.valueSourceFormGroup.disable({emitEvent: false}); + } else { + this.valueSourceFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: ValueSourceProperty): void { + this.modelValue = value; + this.valueSourceFormGroup.patchValue( + value, {emitEvent: false} + ); + this.updateValidators(false); + } + + clearEntityAlias() { + this.valueSourceFormGroup.get('entityAlias').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.entityAliasInput.nativeElement.blur(); + this.entityAliasInput.nativeElement.focus(); + }, 0); + } + + clearKey() { + this.valueSourceFormGroup.get('attribute').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.keyInput.nativeElement.blur(); + this.keyInput.nativeElement.focus(); + }, 0); + } + + private fetchEntityAliases(searchText?: string): Observable> { + this.aliasSearchText = searchText; + let result = this.entityAliasList; + if (searchText && searchText.length) { + result = this.entityAliasList.filter((entityAlias) => entityAlias.toLowerCase().includes(searchText.toLowerCase())); + } + return of(result); + } + + private fetchKeys(searchText?: string): Observable> { + if (this.keySearchText !== searchText || this.latestKeySearchResult === null) { + this.keySearchText = searchText; + const dataKeyFilter = this.createKeyFilter(this.keySearchText); + return this.getKeys().pipe( + map(name => name.filter(dataKeyFilter)), + tap(res => this.latestKeySearchResult = res) + ); + } + return of(this.latestKeySearchResult); + } + + private getKeys() { + if (this.keysFetchObservable$ === null) { + let fetchObservable: Observable>; + let entityAliasId: string; + const entityAlias: string = this.valueSourceFormGroup.get('entityAlias').value; + if (entityAlias && this.aliasController) { + entityAliasId = this.aliasController.getEntityAliasId(entityAlias); + } + if (entityAliasId) { + const dataKeyTypes = [DataKeyType.attribute]; + fetchObservable = this.fetchEntityKeys(entityAliasId, dataKeyTypes); + } else { + fetchObservable = of([]); + } + this.keysFetchObservable$ = fetchObservable.pipe( + map((dataKeys) => dataKeys.map((dataKey) => dataKey.name)), + publishReplay(1), + refCount() + ); + } + return this.keysFetchObservable$; + } + + private fetchEntityKeys(entityAliasId: string, dataKeyTypes: Array): Observable> { + return this.aliasController.getAliasInfo(entityAliasId).pipe( + mergeMap((aliasInfo) => { + return this.entityService.getEntityKeysByEntityFilter( + aliasInfo.entityFilter, + dataKeyTypes, + {ignoreLoading: true, ignoreErrors: true} + ).pipe( + catchError(() => of([])) + ); + }), + catchError(() => of([] as Array)) + ); + } + + private createKeyFilter(query: string): (key: string) => boolean { + const lowercaseQuery = query.toLowerCase(); + return key => key.toLowerCase().startsWith(lowercaseQuery); + } + + private updateModel() { + const value: ValueSourceProperty = this.valueSourceFormGroup.value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } + + private updateValidators(emitEvent?: boolean): void { + const valueSource: ValueSource = this.valueSourceFormGroup.get('valueSource').value; + if (valueSource === 'predefinedValue') { + this.valueSourceFormGroup.get('entityAlias').disable({emitEvent}); + this.valueSourceFormGroup.get('attribute').disable({emitEvent}); + this.valueSourceFormGroup.get('value').enable({emitEvent}); + } else if (valueSource === 'entityAttribute') { + this.valueSourceFormGroup.get('entityAlias').enable({emitEvent}); + this.valueSourceFormGroup.get('attribute').enable({emitEvent}); + this.valueSourceFormGroup.get('value').disable({emitEvent}); + } + this.valueSourceFormGroup.get('entityAlias').updateValueAndValidity({emitEvent: false}); + this.valueSourceFormGroup.get('attribute').updateValueAndValidity({emitEvent: false}); + this.valueSourceFormGroup.get('value').updateValueAndValidity({emitEvent: false}); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.html index c8f4927c38..3cc8899bcc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-font.component.html @@ -66,10 +66,10 @@ + label="{{ 'widgets.widget-font.color' | translate }}" openOnInput colorClearButton> + label="{{ 'widgets.widget-font.shadow-color' | translate }}" openOnInput colorClearButton>
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-compass-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-compass-widget-settings.component.html index 2e27effaf6..c9fc848e95 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-compass-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-compass-widget-settings.component.html @@ -39,7 +39,7 @@ + label="{{ 'widgets.gauge.major-ticks-color' | translate }}" openOnInput colorClearButton>
@@ -48,7 +48,7 @@ + label="{{ 'widgets.gauge.minor-ticks-color' | translate }}" openOnInput colorClearButton>
@@ -63,7 +63,7 @@ widgets.gauge.plate-settings + label="{{ 'widgets.gauge.plate-color' | translate }}" openOnInput colorClearButton> {{ 'widgets.gauge.show-plate-border' | translate }} @@ -71,7 +71,7 @@
+ label="{{ 'widgets.gauge.border-color' | translate }}" openOnInput colorClearButton> widgets.gauge.border-width @@ -88,11 +88,11 @@
+ label="{{ 'widgets.gauge.needle-color' | translate }}" openOnInput colorClearButton> + label="{{ 'widgets.gauge.needle-circle-color' | translate }}" openOnInput colorClearButton>
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-gauge-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-gauge-widget-settings.component.html index b586c31f90..f00e3d130d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-gauge-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-gauge-widget-settings.component.html @@ -26,7 +26,7 @@ + label="{{ 'widgets.gauge.default-color' | translate }}" openOnInput colorClearButton>
widgets.gauge.ticks-settings @@ -47,7 +47,7 @@ + label="{{ 'widgets.gauge.major-ticks-color' | translate }}" openOnInput colorClearButton>
@@ -57,7 +57,7 @@ + label="{{ 'widgets.gauge.minor-ticks-color' | translate }}" openOnInput colorClearButton>
@@ -127,21 +127,21 @@
+ label="{{ 'widgets.gauge.value-box-rect-stroke-color' | translate }}" openOnInput colorClearButton> + label="{{ 'widgets.gauge.value-box-rect-stroke-color-end' | translate }}" openOnInput colorClearButton>
+ label="{{ 'widgets.gauge.value-box-background-color' | translate }}" openOnInput colorClearButton> + label="{{ 'widgets.gauge.value-box-shadow-color' | translate }}" openOnInput colorClearButton>
@@ -152,7 +152,7 @@ widgets.gauge.plate-settings + label="{{ 'widgets.gauge.plate-color' | translate }}" openOnInput colorClearButton> {{ 'widgets.gauge.show-plate-border' | translate }} @@ -163,21 +163,21 @@
+ label="{{ 'widgets.gauge.needle-color' | translate }}" openOnInput colorClearButton> + label="{{ 'widgets.gauge.needle-color-end' | translate }}" openOnInput colorClearButton>
+ label="{{ 'widgets.gauge.needle-color-shadow-up' | translate }}" openOnInput colorClearButton> + label="{{ 'widgets.gauge.needle-color-shadow-down' | translate }}" openOnInput colorClearButton>
@@ -309,27 +309,27 @@ + label="{{ 'widgets.gauge.bar-stroke-color' | translate }}" openOnInput colorClearButton>
+ label="{{ 'widgets.gauge.bar-background-color' | translate }}" openOnInput colorClearButton> + label="{{ 'widgets.gauge.bar-background-color-end' | translate }}" openOnInput colorClearButton>
+ label="{{ 'widgets.gauge.progress-bar-color' | translate }}" openOnInput colorClearButton> + label="{{ 'widgets.gauge.progress-bar-color-end' | translate }}" openOnInput colorClearButton>
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html new file mode 100644 index 0000000000..7ed47b81f0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html @@ -0,0 +1,378 @@ + +
+
+ widgets.gauge.common-settings +
+ + widgets.gauge.min-value + + + + widgets.gauge.max-value + + +
+ + widgets.gauge.gauge-type + + + {{ 'widgets.gauge.gauge-type-arc' | translate }} + + + {{ 'widgets.gauge.gauge-type-donut' | translate }} + + + {{ 'widgets.gauge.gauge-type-horizontal-bar' | translate }} + + + {{ 'widgets.gauge.gauge-type-vertical-bar' | translate }} + + + + + widgets.gauge.donut-start-angle + + + + +
+
+ widgets.gauge.bar-settings + + widgets.gauge.relative-bar-width + + + + widgets.gauge.neon-glow-brightness + + +
+ + widgets.gauge.stripes-thickness + + + + {{ 'widgets.gauge.rounded-line-cap' | translate }} + +
+
+ widgets.gauge.bar-color-settings + + + + {{ 'widgets.gauge.use-precise-level-color-values' | translate }} + +
+ widgets.gauge.bar-colors +
+
+
+
+ drag_handle +
+ + + +
+
+
+ widgets.gauge.no-bar-colors +
+
+ +
+
+
+
+ widgets.gauge.fixed-level-colors +
+
+
+ + +
+
+
+ widgets.gauge.no-bar-colors +
+
+ +
+
+
+
+
+
+ widgets.gauge.gauge-title-settings + + + + + {{ 'widgets.gauge.show-gauge-title' | translate }} + + + + widget-config.advanced-settings + + + +
+ + widgets.gauge.gauge-title + + +
+ widgets.gauge.gauge-title-font + +
+
+
+
+
+
+ widgets.gauge.unit-title-and-timestamp-settings +
+ + {{ 'widgets.gauge.show-unit-title' | translate }} + + + widgets.gauge.unit-title + + +
+
+ + {{ 'widgets.gauge.show-timestamp' | translate }} + + + widgets.gauge.timestamp-format + + +
+ + + + widget-config.advanced-settings + + + +
+ widgets.gauge.label-font + +
+
+
+
+
+ widgets.gauge.value-settings + + + + + {{ 'widgets.gauge.show-value' | translate }} + + + + widget-config.advanced-settings + + + +
+ widgets.gauge.value-font + +
+
+
+
+
+ widgets.gauge.min-max-settings + + + + + {{ 'widgets.gauge.show-min-max' | translate }} + + + + widget-config.advanced-settings + + + +
+ widgets.gauge.min-max-font + +
+
+
+
+
+ widgets.gauge.ticks-settings + + + + + {{ 'widgets.gauge.show-ticks' | translate }} + + + + widget-config.advanced-settings + + + +
+ + widgets.gauge.tick-width + + + + +
+ widgets.gauge.tick-values +
+
+
+ + + +
+
+
+ widgets.gauge.no-tick-values +
+
+ +
+
+
+
+
+
+
+
+ widgets.gauge.animation-settings + + + + + {{ 'widgets.gauge.enable-animation' | translate }} + + + + widget-config.advanced-settings + + + +
+ + widgets.gauge.animation-duration + + + + widgets.gauge.animation-rule + + + {{ 'widgets.gauge.animation-linear' | translate }} + + + {{ 'widgets.gauge.animation-quad' | translate }} + + + {{ 'widgets.gauge.animation-quint' | translate }} + + + {{ 'widgets.gauge.animation-cycle' | translate }} + + + {{ 'widgets.gauge.animation-bounce' | translate }} + + + {{ 'widgets.gauge.animation-elastic' | translate }} + + + {{ 'widgets.gauge.animation-dequad' | translate }} + + + {{ 'widgets.gauge.animation-dequint' | translate }} + + + {{ 'widgets.gauge.animation-decycle' | translate }} + + + {{ 'widgets.gauge.animation-debounce' | translate }} + + + {{ 'widgets.gauge.animation-delastic' | translate }} + + + +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.ts new file mode 100644 index 0000000000..40958f8965 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.ts @@ -0,0 +1,373 @@ +/// +/// Copyright © 2016-2022 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 { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { Component } from '@angular/core'; +import { AbstractControl, FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { GaugeType } from '@home/components/widget/lib/canvas-digital-gauge'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { GaugeHighlight } from '@home/components/widget/lib/settings/gauge/gauge-highlight.component'; +import { + FixedColorLevel, + fixedColorLevelValidator +} from '@home/components/widget/lib/settings/gauge/fixed-color-level.component'; +import { ValueSourceProperty } from '@home/components/widget/lib/settings/common/value-source.component'; + +@Component({ + selector: 'tb-digital-gauge-widget-settings', + templateUrl: './digital-gauge-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class DigitalGaugeWidgetSettingsComponent extends WidgetSettingsComponent { + + digitalGaugeWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + protected fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.digitalGaugeWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + minValue: 0, + maxValue: 100, + gaugeType: 'arc', + donutStartAngle: 90, + neonGlowBrightness: 0, + dashThickness: 0, + roundedLineCap: false, + title: null, + showTitle: false, + unitTitle: null, + showUnitTitle: false, + showTimestamp: false, + timestampFormat: 'yyyy-MM-dd HH:mm:ss', + showValue: true, + showMinMax: true, + gaugeWidthScale: 0.75, + defaultColor: null, + gaugeColor: null, + useFixedLevelColor: false, + levelColors: [], + fixedLevelColors: [], + showTicks: false, + tickWidth: 4, + colorTicks: '#666', + ticksValue: [], + animation: true, + animationDuration: 500, + animationRule: 'linear', + titleFont: { + family: 'Roboto', + size: 12, + style: 'normal', + weight: '500', + color: null + }, + labelFont: { + family: 'Roboto', + size: 8, + style: 'normal', + weight: '500', + color: null + }, + valueFont: { + family: 'Roboto', + size: 18, + style: 'normal', + weight: '500', + color: null + }, + minMaxFont: { + family: 'Roboto', + size: 10, + style: 'normal', + weight: '500', + color: null + } + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + + const levelColorsControls: Array = []; + const fixedLevelColorsControls: Array = []; + const ticksValueControls: Array = []; + if (settings.levelColors) { + settings.levelColors.forEach((levelColor) => { + levelColorsControls.push(this.fb.control(levelColor, [Validators.required])); + }); + } + if (settings.fixedLevelColors) { + settings.fixedLevelColors.forEach((fixedLevelColor) => { + fixedLevelColorsControls.push(this.fb.control(fixedLevelColor, [fixedColorLevelValidator])); + }); + } + if (settings.ticksValue) { + settings.ticksValue.forEach((tickValue) => { + ticksValueControls.push(this.fb.control(tickValue, [Validators.required])); + }); + } + + this.digitalGaugeWidgetSettingsForm = this.fb.group({ + + // Common gauge settings + minValue: [settings.minValue, []], + maxValue: [settings.maxValue, []], + gaugeType: [settings.gaugeType, []], + donutStartAngle: [settings.donutStartAngle, []], + defaultColor: [settings.defaultColor, []], + + // Gauge bar settings + gaugeWidthScale: [settings.gaugeWidthScale, [Validators.min(0)]], + neonGlowBrightness: [settings.neonGlowBrightness, [Validators.min(0), Validators.max(100)]], + dashThickness: [settings.dashThickness, [Validators.min(0)]], + roundedLineCap: [settings.roundedLineCap, []], + + // Gauge bar colors settings + gaugeColor: [settings.gaugeColor, []], + useFixedLevelColor: [settings.useFixedLevelColor, []], + levelColors: this.fb.array(levelColorsControls), + fixedLevelColors: this.fb.array(fixedLevelColorsControls), + + // Title settings + showTitle: [settings.showTitle, []], + title: [settings.title, []], + titleFont: [settings.titleFont, []], + + // Unit title/timestamp settings + showUnitTitle: [settings.showUnitTitle, []], + unitTitle: [settings.unitTitle, []], + showTimestamp: [settings.showTimestamp, []], + timestampFormat: [settings.timestampFormat, []], + labelFont: [settings.labelFont, []], + + // Value settings + showValue: [settings.showValue, []], + valueFont: [settings.valueFont, []], + + // Min/max labels settings + showMinMax: [settings.showMinMax, []], + minMaxFont: [settings.minMaxFont, []], + + // Ticks settings + showTicks: [settings.showTicks, []], + tickWidth: [settings.tickWidth, [Validators.min(0)]], + colorTicks: [settings.colorTicks, []], + ticksValue: this.fb.array(ticksValueControls), + + // Animation settings + animation: [settings.animation, []], + animationDuration: [settings.animationDuration, [Validators.min(0)]], + animationRule: [settings.animationRule, []] + + }); + } + + protected validatorTriggers(): string[] { + return ['gaugeType', 'showTitle', 'showUnitTitle', 'showValue', 'showMinMax', 'showTimestamp', 'useFixedLevelColor', 'showTicks', 'animation']; + } + + protected updateValidators(emitEvent: boolean) { + const gaugeType: GaugeType = this.digitalGaugeWidgetSettingsForm.get('gaugeType').value; + const showTitle: boolean = this.digitalGaugeWidgetSettingsForm.get('showTitle').value; + const showUnitTitle: boolean = this.digitalGaugeWidgetSettingsForm.get('showUnitTitle').value; + const showValue: boolean = this.digitalGaugeWidgetSettingsForm.get('showValue').value; + const showMinMax: boolean = this.digitalGaugeWidgetSettingsForm.get('showMinMax').value; + const showTimestamp: boolean = this.digitalGaugeWidgetSettingsForm.get('showTimestamp').value; + const useFixedLevelColor: boolean = this.digitalGaugeWidgetSettingsForm.get('useFixedLevelColor').value; + const showTicks: boolean = this.digitalGaugeWidgetSettingsForm.get('showTicks').value; + const animation: boolean = this.digitalGaugeWidgetSettingsForm.get('animation').value; + + if (gaugeType === 'donut') { + this.digitalGaugeWidgetSettingsForm.get('donutStartAngle').enable(); + } else { + this.digitalGaugeWidgetSettingsForm.get('donutStartAngle').disable(); + } + if (showTitle) { + this.digitalGaugeWidgetSettingsForm.get('title').enable(); + this.digitalGaugeWidgetSettingsForm.get('titleFont').enable(); + } else { + this.digitalGaugeWidgetSettingsForm.get('title').disable(); + this.digitalGaugeWidgetSettingsForm.get('titleFont').disable(); + } + if (showUnitTitle) { + this.digitalGaugeWidgetSettingsForm.get('unitTitle').enable(); + } else { + this.digitalGaugeWidgetSettingsForm.get('unitTitle').disable(); + } + if (showTimestamp) { + this.digitalGaugeWidgetSettingsForm.get('timestampFormat').enable(); + } else { + this.digitalGaugeWidgetSettingsForm.get('timestampFormat').disable(); + } + if (showUnitTitle || showTimestamp) { + this.digitalGaugeWidgetSettingsForm.get('labelFont').enable(); + } else { + this.digitalGaugeWidgetSettingsForm.get('labelFont').disable(); + } + if (showValue) { + this.digitalGaugeWidgetSettingsForm.get('valueFont').enable(); + } else { + this.digitalGaugeWidgetSettingsForm.get('valueFont').disable(); + } + if (showMinMax) { + this.digitalGaugeWidgetSettingsForm.get('minMaxFont').enable(); + } else { + this.digitalGaugeWidgetSettingsForm.get('minMaxFont').disable(); + } + if (useFixedLevelColor) { + this.digitalGaugeWidgetSettingsForm.get('fixedLevelColors').enable(); + this.digitalGaugeWidgetSettingsForm.get('levelColors').disable(); + } else { + this.digitalGaugeWidgetSettingsForm.get('fixedLevelColors').disable(); + this.digitalGaugeWidgetSettingsForm.get('levelColors').enable(); + } + if (showTicks) { + this.digitalGaugeWidgetSettingsForm.get('tickWidth').enable(); + this.digitalGaugeWidgetSettingsForm.get('colorTicks').enable(); + this.digitalGaugeWidgetSettingsForm.get('ticksValue').enable(); + } else { + this.digitalGaugeWidgetSettingsForm.get('tickWidth').disable(); + this.digitalGaugeWidgetSettingsForm.get('colorTicks').disable(); + this.digitalGaugeWidgetSettingsForm.get('ticksValue').disable(); + } + if (animation) { + this.digitalGaugeWidgetSettingsForm.get('animationDuration').enable(); + this.digitalGaugeWidgetSettingsForm.get('animationRule').enable(); + } else { + this.digitalGaugeWidgetSettingsForm.get('animationDuration').disable(); + this.digitalGaugeWidgetSettingsForm.get('animationRule').disable(); + } + this.digitalGaugeWidgetSettingsForm.get('donutStartAngle').updateValueAndValidity({emitEvent}); + this.digitalGaugeWidgetSettingsForm.get('title').updateValueAndValidity({emitEvent}); + this.digitalGaugeWidgetSettingsForm.get('titleFont').updateValueAndValidity({emitEvent}); + this.digitalGaugeWidgetSettingsForm.get('unitTitle').updateValueAndValidity({emitEvent}); + this.digitalGaugeWidgetSettingsForm.get('timestampFormat').updateValueAndValidity({emitEvent}); + this.digitalGaugeWidgetSettingsForm.get('labelFont').updateValueAndValidity({emitEvent}); + this.digitalGaugeWidgetSettingsForm.get('valueFont').updateValueAndValidity({emitEvent}); + this.digitalGaugeWidgetSettingsForm.get('minMaxFont').updateValueAndValidity({emitEvent}); + this.digitalGaugeWidgetSettingsForm.get('fixedLevelColors').updateValueAndValidity({emitEvent}); + this.digitalGaugeWidgetSettingsForm.get('levelColors').updateValueAndValidity({emitEvent}); + this.digitalGaugeWidgetSettingsForm.get('tickWidth').updateValueAndValidity({emitEvent}); + this.digitalGaugeWidgetSettingsForm.get('colorTicks').updateValueAndValidity({emitEvent}); + this.digitalGaugeWidgetSettingsForm.get('ticksValue').updateValueAndValidity({emitEvent}); + this.digitalGaugeWidgetSettingsForm.get('animationDuration').updateValueAndValidity({emitEvent}); + this.digitalGaugeWidgetSettingsForm.get('animationRule').updateValueAndValidity({emitEvent}); + } + + levelColorsFormArray(): FormArray { + return this.digitalGaugeWidgetSettingsForm.get('levelColors') as FormArray; + } + + public trackByLevelColor(index: number, levelColorControl: AbstractControl): any { + return index; + } + + public removeLevelColor(index: number) { + (this.digitalGaugeWidgetSettingsForm.get('levelColors') as FormArray).removeAt(index); + } + + public addLevelColor() { + const levelColorsArray = this.digitalGaugeWidgetSettingsForm.get('levelColors') as FormArray; + const levelColorControl = this.fb.control(null, []); + levelColorsArray.push(levelColorControl); + this.digitalGaugeWidgetSettingsForm.updateValueAndValidity(); + } + + levelColorDrop(event: CdkDragDrop) { + const levelColorsArray = this.digitalGaugeWidgetSettingsForm.get('levelColors') as FormArray; + const levelColor = levelColorsArray.at(event.previousIndex); + levelColorsArray.removeAt(event.previousIndex); + levelColorsArray.insert(event.currentIndex, levelColor); + } + + fixedLevelColorFormArray(): FormArray { + return this.digitalGaugeWidgetSettingsForm.get('fixedLevelColors') as FormArray; + } + + public trackByFixedLevelColor(index: number, fixedLevelColorControl: AbstractControl): any { + return fixedLevelColorControl; + } + + public removeFixedLevelColor(index: number) { + (this.digitalGaugeWidgetSettingsForm.get('fixedLevelColors') as FormArray).removeAt(index); + } + + public addFixedLevelColor() { + const fixedLevelColor: FixedColorLevel = { + from: { + valueSource: 'predefinedValue' + }, + to: { + valueSource: 'predefinedValue' + }, + color: null + }; + const fixedLevelColorsArray = this.digitalGaugeWidgetSettingsForm.get('fixedLevelColors') as FormArray; + const fixedLevelColorControl = this.fb.control(fixedLevelColor, [fixedColorLevelValidator]); + (fixedLevelColorControl as any).new = true; + fixedLevelColorsArray.push(fixedLevelColorControl); + this.digitalGaugeWidgetSettingsForm.updateValueAndValidity(); + if (!this.digitalGaugeWidgetSettingsForm.valid) { + this.onSettingsChanged(this.digitalGaugeWidgetSettingsForm.value); + } + } + + fixedLevelColorDrop(event: CdkDragDrop) { + const fixedLevelColorsArray = this.digitalGaugeWidgetSettingsForm.get('fixedLevelColors') as FormArray; + const fixedLevelColor = fixedLevelColorsArray.at(event.previousIndex); + fixedLevelColorsArray.removeAt(event.previousIndex); + fixedLevelColorsArray.insert(event.currentIndex, fixedLevelColor); + } + + tickValuesFormArray(): FormArray { + return this.digitalGaugeWidgetSettingsForm.get('ticksValue') as FormArray; + } + + public trackByTickValue(index: number, tickValueControl: AbstractControl): any { + return tickValueControl; + } + + public removeTickValue(index: number) { + (this.digitalGaugeWidgetSettingsForm.get('ticksValue') as FormArray).removeAt(index); + } + + public addTickValue() { + const tickValue: ValueSourceProperty = { + valueSource: 'predefinedValue' + }; + const tickValuesArray = this.digitalGaugeWidgetSettingsForm.get('ticksValue') as FormArray; + const tickValueControl = this.fb.control(tickValue, []); + (tickValueControl as any).new = true; + tickValuesArray.push(tickValueControl); + this.digitalGaugeWidgetSettingsForm.updateValueAndValidity(); + } + + tickValueDrop(event: CdkDragDrop) { + const tickValuesArray = this.digitalGaugeWidgetSettingsForm.get('ticksValue') as FormArray; + const tickValue = tickValuesArray.at(event.previousIndex); + tickValuesArray.removeAt(event.previousIndex); + tickValuesArray.insert(event.currentIndex, tickValue); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/fixed-color-level.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/fixed-color-level.component.html new file mode 100644 index 0000000000..01fec01d3a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/fixed-color-level.component.html @@ -0,0 +1,59 @@ + + + +
+ +
+
{{ fixedColorLevelRangeText() }}
+
+
+
+
+
+ + +
+
+ +
+ +
+
+ widgets.gauge.from + +
+
+ widgets.gauge.to + +
+ + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/fixed-color-level.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/fixed-color-level.component.scss new file mode 100644 index 0000000000..16c23f47c2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/fixed-color-level.component.scss @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2022 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. + */ +:host { + display: block; + .mat-expansion-panel { + box-shadow: none; + &.fixed-color-level { + border: 1px groove rgba(0, 0, 0, .25); + .mat-expansion-panel-header { + padding: 0 24px 0 8px; + &.mat-expanded { + height: 48px; + } + } + } + } +} + +:host ::ng-deep { + .mat-expansion-panel { + &.fixed-color-level { + .mat-expansion-panel-body { + padding: 0 8px 8px; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/fixed-color-level.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/fixed-color-level.component.ts new file mode 100644 index 0000000000..f289b235b4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/fixed-color-level.component.ts @@ -0,0 +1,147 @@ +/// +/// Copyright © 2016-2022 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 { ValueSourceProperty } from '@home/components/widget/lib/settings/common/value-source.component'; +import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormBuilder, + FormGroup, + NG_VALUE_ACCESSOR, ValidationErrors, + Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { isNumber } from '@core/utils'; +import { IAliasController } from '@core/api/widget-api.models'; + +export interface FixedColorLevel { + from?: ValueSourceProperty; + to?: ValueSourceProperty; + color: string; +} + +export function fixedColorLevelValidator(control: AbstractControl): ValidationErrors | null { + const fixedColorLevel: FixedColorLevel = control.value; + if (!fixedColorLevel || !fixedColorLevel.color) { + return { + fixedColorLevel: true + }; + } + return null; +} + +@Component({ + selector: 'tb-fixed-color-level', + templateUrl: './fixed-color-level.component.html', + styleUrls: ['./fixed-color-level.component.scss', './../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FixedColorLevelComponent), + multi: true + } + ] +}) +export class FixedColorLevelComponent extends PageComponent implements OnInit, ControlValueAccessor { + + @Input() + disabled: boolean; + + @Input() + expanded = false; + + @Input() + aliasController: IAliasController; + + @Output() + removeFixedColorLevel = new EventEmitter(); + + private modelValue: FixedColorLevel; + + private propagateChange = null; + + public fixedColorLevelFormGroup: FormGroup; + + constructor(protected store: Store, + private translate: TranslateService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.fixedColorLevelFormGroup = this.fb.group({ + from: [null, []], + to: [null, []], + color: [null, [Validators.required]] + }); + this.fixedColorLevelFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.fixedColorLevelFormGroup.disable({emitEvent: false}); + } else { + this.fixedColorLevelFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: FixedColorLevel): void { + this.modelValue = value; + this.fixedColorLevelFormGroup.patchValue( + value, {emitEvent: false} + ); + } + + fixedColorLevelRangeText(): string { + const value: FixedColorLevel = this.fixedColorLevelFormGroup.value; + const from = this.valueSourcePropertyText(value?.from); + const to = this.valueSourcePropertyText(value?.to); + return `${from} - ${to}`; + } + + private valueSourcePropertyText(source?: ValueSourceProperty): string { + if (source) { + if (source.valueSource === 'predefinedValue') { + return `${isNumber(source.value) ? source.value : 0}`; + } else if (source.valueSource === 'entityAttribute') { + const alias = source.entityAlias || 'Undefined'; + const key = source.attribute || 'Undefined'; + return `${alias}.${key}`; + } + } + return 'Undefined'; + } + + private updateModel() { + const value: FixedColorLevel = this.fixedColorLevelFormGroup.value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/gauge-highlight.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/gauge-highlight.component.html index 3e1ffde7a3..80e4256e86 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/gauge-highlight.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/gauge-highlight.component.html @@ -52,7 +52,7 @@ + label="{{ 'widgets.gauge.highlight-color' | translate }}" openOnInput colorClearButton> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.html new file mode 100644 index 0000000000..6c0c87e076 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.html @@ -0,0 +1,44 @@ + + + +
+ +
+
{{ tickValueText() }}
+
+
+ + +
+
+ +
+ +
+ +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.scss new file mode 100644 index 0000000000..002203e3d3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.scss @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2022 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. + */ +:host { + display: block; + .mat-expansion-panel { + box-shadow: none; + &.tick-value { + border: 1px groove rgba(0, 0, 0, .25); + .mat-expansion-panel-header { + padding: 0 24px 0 8px; + &.mat-expanded { + height: 48px; + } + } + } + } +} + +:host ::ng-deep { + .mat-expansion-panel { + &.tick-value { + .mat-expansion-panel-body { + padding: 0 8px 8px; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.ts new file mode 100644 index 0000000000..7bf02ac9d0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.ts @@ -0,0 +1,120 @@ +/// +/// Copyright © 2016-2022 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 { ValueSourceProperty } from '@home/components/widget/lib/settings/common/value-source.component'; +import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { isNumber } from '@core/utils'; +import { IAliasController } from '@core/api/widget-api.models'; + +@Component({ + selector: 'tb-tick-value', + templateUrl: './tick-value.component.html', + styleUrls: ['./tick-value.component.scss', './../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TickValueComponent), + multi: true + } + ] +}) +export class TickValueComponent extends PageComponent implements OnInit, ControlValueAccessor { + + @Input() + disabled: boolean; + + @Input() + expanded = false; + + @Input() + aliasController: IAliasController; + + @Output() + removeTickValue = new EventEmitter(); + + private modelValue: ValueSourceProperty; + + private propagateChange = null; + + public tickValueFormGroup: FormGroup; + + constructor(protected store: Store, + private translate: TranslateService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.tickValueFormGroup = this.fb.group({ + tickValue: [null, []] + }); + this.tickValueFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.tickValueFormGroup.disable({emitEvent: false}); + } else { + this.tickValueFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: ValueSourceProperty): void { + this.modelValue = value; + this.tickValueFormGroup.patchValue( + {tickValue: value}, {emitEvent: false} + ); + } + + tickValueText(): string { + const value: ValueSourceProperty = this.tickValueFormGroup.get('tickValue').value; + return this.valueSourcePropertyText(value); + } + + private valueSourcePropertyText(source?: ValueSourceProperty): string { + if (source) { + if (source.valueSource === 'predefinedValue') { + return `${isNumber(source.value) ? source.value : 0}`; + } else if (source.valueSource === 'entityAttribute') { + const alias = source.entityAlias || 'Undefined'; + const key = source.attribute || 'Undefined'; + return `${alias}.${key}`; + } + } + return 'Undefined'; + } + + private updateModel() { + const value: ValueSourceProperty = this.tickValueFormGroup.get('tickValue').value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index 8e0f2b687d..a90bc9edf4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -69,6 +69,12 @@ import { import { AnalogueCompassWidgetSettingsComponent } from '@home/components/widget/lib/settings/gauge/analogue-compass-widget-settings.component'; +import { + DigitalGaugeWidgetSettingsComponent +} from '@home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component'; +import { ValueSourceComponent } from '@home/components/widget/lib/settings/common/value-source.component'; +import { FixedColorLevelComponent } from '@home/components/widget/lib/settings/gauge/fixed-color-level.component'; +import { TickValueComponent } from '@home/components/widget/lib/settings/gauge/tick-value.component'; @NgModule({ declarations: [ @@ -91,7 +97,11 @@ import { GaugeHighlightComponent, AnalogueRadialGaugeWidgetSettingsComponent, AnalogueLinearGaugeWidgetSettingsComponent, - AnalogueCompassWidgetSettingsComponent + AnalogueCompassWidgetSettingsComponent, + DigitalGaugeWidgetSettingsComponent, + ValueSourceComponent, + FixedColorLevelComponent, + TickValueComponent ], imports: [ CommonModule, @@ -118,7 +128,11 @@ import { GaugeHighlightComponent, AnalogueRadialGaugeWidgetSettingsComponent, AnalogueLinearGaugeWidgetSettingsComponent, - AnalogueCompassWidgetSettingsComponent + AnalogueCompassWidgetSettingsComponent, + DigitalGaugeWidgetSettingsComponent, + ValueSourceComponent, + FixedColorLevelComponent, + TickValueComponent ] }) export class WidgetSettingsModule { @@ -141,5 +155,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type .mat-expansion-panel-content { + > .mat-expansion-panel-body { + padding: 0; + } } .mat-checkbox-layout { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index 256aa56746..3b764f0cf0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -500,6 +500,7 @@ fxLayout="column"> diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss index d714420909..d8f18a6fc5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss @@ -139,8 +139,10 @@ .mat-expansion-panel-header-description { align-items: center; } - .mat-expansion-panel-body{ - padding: 0; + > .mat-expansion-panel-content { + > .mat-expansion-panel-body { + padding: 0; + } } .tb-json-object-panel, .tb-css-content-panel { margin: 0 0 8px; diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.ts index 15919447ea..c5fed3ff05 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.ts @@ -44,6 +44,7 @@ import { IWidgetSettingsComponent, Widget, WidgetSettings } from '@shared/models import { widgetSettingsComponentsMap } from '@home/components/widget/lib/settings/widget-settings.module'; import { Dashboard } from '@shared/models/dashboard.models'; import { WidgetService } from '@core/http/widget.service'; +import { IAliasController } from '@core/api/widget-api.models'; @Component({ selector: 'tb-widget-settings', @@ -64,6 +65,9 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On @Input() disabled: boolean; + @Input() + aliasController: IAliasController; + @Input() dashboard: Dashboard; @@ -143,6 +147,7 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On this.changeSubscription = null; } if (this.definedSettingsComponent) { + this.definedSettingsComponent.aliasController = this.aliasController; this.definedSettingsComponent.dashboard = this.dashboard; this.definedSettingsComponent.widget = this.widget; this.definedSettingsComponent.functionScopeVariables = this.widgetService.getWidgetScopeVariables(); @@ -197,6 +202,7 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On const factory = this.cfr.resolveComponentFactory(componentType); this.definedSettingsComponentRef = this.definedSettingsContainer.createComponent(factory); this.definedSettingsComponent = this.definedSettingsComponentRef.instance; + this.definedSettingsComponent.aliasController = this.aliasController; this.definedSettingsComponent.dashboard = this.dashboard; this.definedSettingsComponent.widget = this.widget; this.definedSettingsComponent.functionScopeVariables = this.widgetService.getWidgetScopeVariables(); diff --git a/ui-ngx/src/app/shared/components/color-input.component.html b/ui-ngx/src/app/shared/components/color-input.component.html index 16b6297a24..765178022b 100644 --- a/ui-ngx/src/app/shared/components/color-input.component.html +++ b/ui-ngx/src/app/shared/components/color-input.component.html @@ -23,7 +23,7 @@
- + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts new file mode 100644 index 0000000000..8accd374a6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts @@ -0,0 +1,411 @@ +/// +/// Copyright © 2016-2022 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 { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { ChartType, TbFlotSettings } from '@home/components/widget/lib/flot-widget.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { ComparisonDuration } from '@shared/models/time/time.models'; +import { WidgetService } from '@core/http/widget.service'; +import { fixedColorLevelValidator } from '@home/components/widget/lib/settings/gauge/fixed-color-level.component'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { + LabelDataKey, + labelDataKeyValidator +} from '@home/components/widget/lib/settings/chart/label-data-key.component'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; + +export function flotDefaultSettings(chartType: ChartType): Partial { + const settings: Partial = { + stack: false, + fontColor: '#545454', + fontSize: 10, + showTooltip: true, + tooltipIndividual: false, + tooltipCumulative: false, + hideZeros: false, + tooltipValueFormatter: '', + grid: { + verticalLines: true, + horizontalLines: true, + outlineWidth: 1, + color: '#545454', + backgroundColor: null, + tickColor: '#DDDDDD' + }, + xaxis: { + title: null, + showLabels: true, + color: null + }, + yaxis: { + min: null, + max: null, + title: null, + showLabels: true, + color: null, + tickSize: null, + tickDecimals: 0, + ticksFormatter: '' + } + }; + if (chartType === 'graph') { + settings.smoothLines = false; + settings.shadowSize = 4; + } + if (chartType === 'bar') { + settings.defaultBarWidth = 600; + settings.barAlignment = 'left'; + } + if (chartType === 'graph' || chartType === 'bar') { + settings.thresholdsLineWidth = null; + settings.comparisonEnabled = false; + settings.timeForComparison = 'previousInterval'; + settings.comparisonCustomIntervalValue = 7200000; + settings.xaxisSecond = { + title: null, + axisPosition: 'top', + showLabels: true + }; + settings.customLegendEnabled = false; + settings.dataKeysListForLabels = []; + } + return settings; +} + +@Component({ + selector: 'tb-flot-widget-settings', + templateUrl: './flot-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FlotWidgetSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => FlotWidgetSettingsComponent), + multi: true, + } + ] +}) +export class FlotWidgetSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { + + @Input() + disabled: boolean; + + @Input() + chartType: ChartType; + + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + + private modelValue: TbFlotSettings; + + private propagateChange = null; + + public flotSettingsFormGroup: FormGroup; + + constructor(protected store: Store, + private translate: TranslateService, + private widgetService: WidgetService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.flotSettingsFormGroup = this.fb.group({ + + // Common settings + + stack: [false, []], + fontSize: [10, [Validators.min(0)]], + fontColor: ['#545454', []], + + // Tooltip settings + + showTooltip: [true, []], + tooltipIndividual: [false, []], + tooltipCumulative: [false, []], + hideZeros: [false, []], + tooltipValueFormatter: ['', []], + + // Grid settings + + grid: this.fb.group({ + verticalLines: [true, []], + horizontalLines: [true, []], + outlineWidth: [1, [Validators.min(0)]], + color: ['#545454', []], + backgroundColor: [null, []], + tickColor: ['#DDDDDD', []] + }), + + // X axis settings + + xaxis: this.fb.group({ + title: [null, []], + + // --> X axis tick labels settings + + showLabels: [true, []], + color: [null, []], + }), + + // Y axis settings + + yaxis: this.fb.group({ + min: [null, []], + max: [null, []], + title: [null, []], + + // --> Y axis tick labels settings + + showLabels: [true, []], + color: [null, []], + tickSize: [null, [Validators.min(0)]], + tickDecimals: [0, [Validators.min(0)]], + ticksFormatter: ['', []] + }) + }); + if (this.chartType === 'graph') { + // Common settings + this.flotSettingsFormGroup.addControl('shadowSize', this.fb.control(4, [Validators.min(0)])); + this.flotSettingsFormGroup.addControl('smoothLines', this.fb.control(false, [])); + } else if (this.chartType === 'bar') { + // Common settings + this.flotSettingsFormGroup.addControl('defaultBarWidth', this.fb.control(600, [Validators.min(0)])); + this.flotSettingsFormGroup.addControl('barAlignment', this.fb.control('left', [])); + } + if (this.chartType === 'graph' || this.chartType === 'bar') { + // Common settings + this.flotSettingsFormGroup.addControl('thresholdsLineWidth', this.fb.control(null, [Validators.min(0)])); + + // Comparison settings + + this.flotSettingsFormGroup.addControl('comparisonEnabled', this.fb.control(false, [])); + this.flotSettingsFormGroup.addControl('timeForComparison', this.fb.control('previousInterval', [])); + this.flotSettingsFormGroup.addControl('comparisonCustomIntervalValue', this.fb.control(7200000, [Validators.min(0)])); + + // --> Comparison X axis settings + + this.flotSettingsFormGroup.addControl('xaxisSecond', this.fb.group({ + axisPosition: ['top', []], + title: [null, []], + showLabels: [true, []] + })); + + // Custom legend settings + + this.flotSettingsFormGroup.addControl('customLegendEnabled', this.fb.control(false, [])); + this.flotSettingsFormGroup.addControl('dataKeysListForLabels', this.fb.control(this.fb.array([]), [])); + } + + this.flotSettingsFormGroup.get('showTooltip').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + + this.flotSettingsFormGroup.get('xaxis.showLabels').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + + this.flotSettingsFormGroup.get('yaxis.showLabels').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + + if (this.chartType === 'graph' || this.chartType === 'bar') { + this.flotSettingsFormGroup.get('comparisonEnabled').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.flotSettingsFormGroup.get('timeForComparison').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.flotSettingsFormGroup.get('customLegendEnabled').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + } + + this.flotSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + + this.updateValidators(false); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.flotSettingsFormGroup.disable({emitEvent: false}); + } else { + this.flotSettingsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: TbFlotSettings): void { + const dataKeysListForLabels = value?.dataKeysListForLabels; + this.modelValue = value; + this.flotSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + const dataKeysListForLabelsControls: Array = []; + if (dataKeysListForLabels && dataKeysListForLabels.length) { + dataKeysListForLabels.forEach((labelDataKey) => { + dataKeysListForLabelsControls.push(this.fb.control(labelDataKey, [labelDataKeyValidator])); + }); + } + this.flotSettingsFormGroup.setControl('dataKeysListForLabels', this.fb.array(dataKeysListForLabelsControls), {emitEvent: false}); + this.updateValidators(false); + } + + validate(c: FormControl) { + return (this.flotSettingsFormGroup.valid) ? null : { + flotSettings: { + valid: false, + }, + }; + } + + private updateModel() { + const value: TbFlotSettings = this.flotSettingsFormGroup.value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } + + private updateValidators(emitEvent?: boolean): void { + const showTooltip: boolean = this.flotSettingsFormGroup.get('showTooltip').value; + const xaxisShowLabels: boolean = this.flotSettingsFormGroup.get('xaxis.showLabels').value; + const yaxisShowLabels: boolean = this.flotSettingsFormGroup.get('yaxis.showLabels').value; + + if (showTooltip) { + this.flotSettingsFormGroup.get('tooltipIndividual').enable({emitEvent}); + this.flotSettingsFormGroup.get('tooltipCumulative').enable({emitEvent}); + this.flotSettingsFormGroup.get('hideZeros').enable({emitEvent}); + this.flotSettingsFormGroup.get('tooltipValueFormatter').enable({emitEvent}); + } else { + this.flotSettingsFormGroup.get('tooltipIndividual').disable({emitEvent}); + this.flotSettingsFormGroup.get('tooltipCumulative').disable({emitEvent}); + this.flotSettingsFormGroup.get('hideZeros').disable({emitEvent}); + this.flotSettingsFormGroup.get('tooltipValueFormatter').disable({emitEvent}); + } + + if (xaxisShowLabels) { + this.flotSettingsFormGroup.get('xaxis.color').enable({emitEvent}); + } else { + this.flotSettingsFormGroup.get('xaxis.color').disable({emitEvent}); + } + + if (yaxisShowLabels) { + this.flotSettingsFormGroup.get('yaxis.color').enable({emitEvent}); + this.flotSettingsFormGroup.get('yaxis.tickSize').enable({emitEvent}); + this.flotSettingsFormGroup.get('yaxis.tickDecimals').enable({emitEvent}); + this.flotSettingsFormGroup.get('yaxis.ticksFormatter').enable({emitEvent}); + } else { + this.flotSettingsFormGroup.get('yaxis.color').disable({emitEvent}); + this.flotSettingsFormGroup.get('yaxis.tickSize').disable({emitEvent}); + this.flotSettingsFormGroup.get('yaxis.tickDecimals').disable({emitEvent}); + this.flotSettingsFormGroup.get('yaxis.ticksFormatter').disable({emitEvent}); + } + + this.flotSettingsFormGroup.get('tooltipIndividual').updateValueAndValidity({emitEvent: false}); + this.flotSettingsFormGroup.get('tooltipCumulative').updateValueAndValidity({emitEvent: false}); + this.flotSettingsFormGroup.get('hideZeros').updateValueAndValidity({emitEvent: false}); + this.flotSettingsFormGroup.get('tooltipValueFormatter').updateValueAndValidity({emitEvent: false}); + this.flotSettingsFormGroup.get('xaxis.color').updateValueAndValidity({emitEvent: false}); + this.flotSettingsFormGroup.get('yaxis.color').updateValueAndValidity({emitEvent: false}); + this.flotSettingsFormGroup.get('yaxis.tickSize').updateValueAndValidity({emitEvent: false}); + this.flotSettingsFormGroup.get('yaxis.tickDecimals').updateValueAndValidity({emitEvent: false}); + this.flotSettingsFormGroup.get('yaxis.ticksFormatter').updateValueAndValidity({emitEvent: false}); + + if (this.chartType === 'graph' || this.chartType === 'bar') { + const comparisonEnabled: boolean = this.flotSettingsFormGroup.get('comparisonEnabled').value; + const timeForComparison: ComparisonDuration = this.flotSettingsFormGroup.get('timeForComparison').value; + const customLegendEnabled: boolean = this.flotSettingsFormGroup.get('customLegendEnabled').value; + if (comparisonEnabled) { + this.flotSettingsFormGroup.get('timeForComparison').enable({emitEvent: false}); + if (timeForComparison === 'customInterval') { + this.flotSettingsFormGroup.get('comparisonCustomIntervalValue').enable({emitEvent}); + } else { + this.flotSettingsFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent}); + } + } else { + this.flotSettingsFormGroup.get('timeForComparison').disable({emitEvent: false}); + this.flotSettingsFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent}); + } + if (customLegendEnabled) { + this.flotSettingsFormGroup.get('dataKeysListForLabels').enable({emitEvent}); + } else { + this.flotSettingsFormGroup.get('dataKeysListForLabels').disable({emitEvent}); + } + + this.flotSettingsFormGroup.get('timeForComparison').updateValueAndValidity({emitEvent: false}); + this.flotSettingsFormGroup.get('comparisonCustomIntervalValue').updateValueAndValidity({emitEvent: false}); + this.flotSettingsFormGroup.get('dataKeysListForLabels').updateValueAndValidity({emitEvent: false}); + } + } + + dataKeysListForLabelsFormArray(): FormArray { + return this.flotSettingsFormGroup.get('dataKeysListForLabels') as FormArray; + } + + public trackByLabelDataKey(index: number, labelDataKeyControl: AbstractControl): any { + return labelDataKeyControl; + } + + public removeLabelDataKey(index: number) { + (this.flotSettingsFormGroup.get('dataKeysListForLabels') as FormArray).removeAt(index); + } + + public addLabelDataKey() { + const labelDataKey: LabelDataKey = { + name: null, + type: DataKeyType.attribute + }; + const dataKeysListForLabelsArray = this.flotSettingsFormGroup.get('dataKeysListForLabels') as FormArray; + const labelDataKeyControl = this.fb.control(labelDataKey, [labelDataKeyValidator]); + (labelDataKeyControl as any).new = true; + dataKeysListForLabelsArray.push(labelDataKeyControl); + this.flotSettingsFormGroup.updateValueAndValidity(); + } + + labelDataKeyDrop(event: CdkDragDrop) { + const dataKeysListForLabelsArray = this.flotSettingsFormGroup.get('dataKeysListForLabels') as FormArray; + const labelDataKey = dataKeysListForLabelsArray.at(event.previousIndex); + dataKeysListForLabelsArray.removeAt(event.previousIndex); + dataKeysListForLabelsArray.insert(event.currentIndex, labelDataKey); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/label-data-key.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/label-data-key.component.html new file mode 100644 index 0000000000..50dc16d6fb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/label-data-key.component.html @@ -0,0 +1,63 @@ + + + +
+ +
+ {{ labelDataKeyText() }} +
+
+ + +
+
+ +
+ +
+
+ + widgets.chart.key-name + + + {{ 'widgets.chart.key-name-required' | translate }} + + + + widgets.chart.key-type + + + {{ 'widgets.chart.key-type-attribute' | translate }} + + + {{ 'widgets.chart.key-type-timeseries' | translate }} + + + +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/label-data-key.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/label-data-key.component.scss new file mode 100644 index 0000000000..d82e4b34b9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/label-data-key.component.scss @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2022 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. + */ +:host { + display: block; + .mat-expansion-panel { + box-shadow: none; + &.label-data-key { + border: 1px groove rgba(0, 0, 0, .25); + .mat-expansion-panel-header { + padding: 0 24px 0 8px; + &.mat-expanded { + height: 48px; + } + } + } + } +} + +:host ::ng-deep { + .mat-expansion-panel { + &.label-data-key { + .mat-expansion-panel-body { + padding: 0 8px 8px; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/label-data-key.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/label-data-key.component.ts new file mode 100644 index 0000000000..940c135c1e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/label-data-key.component.ts @@ -0,0 +1,132 @@ +/// +/// Copyright © 2016-2022 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 { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormBuilder, + FormGroup, + NG_VALUE_ACCESSOR, ValidationErrors, + Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; + +export interface LabelDataKey { + name: string; + type: DataKeyType; +} + +export function labelDataKeyValidator(control: AbstractControl): ValidationErrors | null { + const labelDataKey: LabelDataKey = control.value; + if (!labelDataKey || !labelDataKey.name) { + return { + labelDataKey: true + }; + } + return null; +} + +@Component({ + selector: 'tb-label-data-key', + templateUrl: './label-data-key.component.html', + styleUrls: ['./label-data-key.component.scss', './../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => LabelDataKeyComponent), + multi: true + } + ] +}) +export class LabelDataKeyComponent extends PageComponent implements OnInit, ControlValueAccessor { + + @Input() + disabled: boolean; + + @Input() + expanded = false; + + @Output() + removeLabelDataKey = new EventEmitter(); + + private modelValue: LabelDataKey; + + private propagateChange = null; + + public labelDataKeyFormGroup: FormGroup; + + constructor(protected store: Store, + private translate: TranslateService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.labelDataKeyFormGroup = this.fb.group({ + name: [null, [Validators.required]], + type: [DataKeyType.attribute, [Validators.required]] + }); + this.labelDataKeyFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.labelDataKeyFormGroup.disable({emitEvent: false}); + } else { + this.labelDataKeyFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: LabelDataKey): void { + this.modelValue = value; + this.labelDataKeyFormGroup.patchValue( + value, {emitEvent: false} + ); + } + + labelDataKeyText(): string { + const name: string = this.labelDataKeyFormGroup.get('name').value || ''; + const type: DataKeyType = this.labelDataKeyFormGroup.get('type').value; + let typeStr: string; + if (type === DataKeyType.attribute) { + typeStr = this.translate.instant('widgets.chart.key-type-attribute'); + } else if (type === DataKeyType.timeseries) { + typeStr = this.translate.instant('widgets.chart.key-type-timeseries'); + } + return `${name} (${typeStr})`; + } + + private updateModel() { + const value: LabelDataKey = this.labelDataKeyFormGroup.value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index a90bc9edf4..8ee227e253 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -75,6 +75,14 @@ import { import { ValueSourceComponent } from '@home/components/widget/lib/settings/common/value-source.component'; import { FixedColorLevelComponent } from '@home/components/widget/lib/settings/gauge/fixed-color-level.component'; import { TickValueComponent } from '@home/components/widget/lib/settings/gauge/tick-value.component'; +import { FlotWidgetSettingsComponent } from '@home/components/widget/lib/settings/chart/flot-widget-settings.component'; +import { + FlotLineWidgetSettingsComponent +} from '@home/components/widget/lib/settings/chart/flot-line-widget-settings.component'; +import { LabelDataKeyComponent } from '@home/components/widget/lib/settings/chart/label-data-key.component'; +import { + FlotBarWidgetSettingsComponent +} from '@home/components/widget/lib/settings/chart/flot-bar-widget-settings.component'; @NgModule({ declarations: [ @@ -101,7 +109,11 @@ import { TickValueComponent } from '@home/components/widget/lib/settings/gauge/t DigitalGaugeWidgetSettingsComponent, ValueSourceComponent, FixedColorLevelComponent, - TickValueComponent + TickValueComponent, + FlotWidgetSettingsComponent, + LabelDataKeyComponent, + FlotLineWidgetSettingsComponent, + FlotBarWidgetSettingsComponent ], imports: [ CommonModule, @@ -132,7 +144,11 @@ import { TickValueComponent } from '@home/components/widget/lib/settings/gauge/t DigitalGaugeWidgetSettingsComponent, ValueSourceComponent, FixedColorLevelComponent, - TickValueComponent + TickValueComponent, + FlotWidgetSettingsComponent, + LabelDataKeyComponent, + FlotLineWidgetSettingsComponent, + FlotBarWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -156,5 +172,7 @@ export const widgetSettingsComponentsMap: {[key: string]: Type { - this.onSettingsChanged(updated); + this.settingsForm().valueChanges.subscribe((updated: any) => { + this.onSettingsChanged(this.prepareOutputSettings(updated)); }); } @@ -713,7 +713,7 @@ export abstract class WidgetSettingsComponent extends PageComponent implements protected onSettingsChanged(updated: WidgetSettings) { this.settingsValue = updated; if (this.validateSettings()) { - this.settingsChangedEmitter.emit(this.prepareOutputSettings(updated)); + this.settingsChangedEmitter.emit(updated); } else { this.settingsChangedEmitter.emit(null); } @@ -723,7 +723,7 @@ export abstract class WidgetSettingsComponent extends PageComponent implements return settings; } - protected prepareOutputSettings(settings: WidgetSettings): WidgetSettings { + protected prepareOutputSettings(settings: any): WidgetSettings { return settings; } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index c0e6c3b801..d4d47f3bfd 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3182,13 +3182,75 @@ "invalid-widget-type-file-error": "Unable to import widget type: Invalid widget type data structure." }, "widgets": { + "chart": { + "common-settings": "Common settings", + "enable-stacking-mode": "Enable stacking mode", + "line-shadow-size": "Line shadow size", + "display-smooth-lines": "Display smooth (curved) lines", + "default-bar-width": "Default bar width for non-aggregated data (milliseconds)", + "bar-alignment": "Bar alignment", + "bar-alignment-left": "Left", + "bar-alignment-right": "Right", + "bar-alignment-center": "Center", + "default-font-size": "Default font size", + "default-font-color": "Default font color", + "thresholds-line-width": "Default line width for all thresholds", + "tooltip-settings": "Tooltip settings", + "show-tooltip": "Show tooltip", + "hover-individual-points": "Hover individual points", + "show-cumulative-values": "Show cumulative values in stacking mode", + "hide-zero-false-values": "Hide zero/false values from tooltip", + "tooltip-value-format-function": "Tooltip value format function", + "grid-settings": "Grid settings", + "show-vertical-lines": "Show vertical lines", + "show-horizontal-lines": "Show horizontal lines", + "grid-outline-border-width": "Grid outline/border width (px)", + "primary-color": "Primary color", + "background-color": "Background color", + "ticks-color": "Ticks color", + "xaxis-settings": "X axis settings", + "axis-title": "Axis title", + "xaxis-tick-labels-settings": "X axis tick labels settings", + "show-tick-labels": "Show axis tick labels", + "yaxis-settings": "Y axis settings", + "min-scale-value": "Minimum value on the scale", + "max-scale-value": "Maximum value on the scale", + "yaxis-tick-labels-settings": "Y axis tick labels settings", + "tick-step-size": "Step size between ticks", + "number-of-decimals": "The number of decimals to display", + "ticks-formatter-function": "Ticks formatter function", + "comparison-settings": "Comparison settings", + "enable-comparison": "Enable comparison", + "time-for-comparison": "Comparison period", + "time-for-comparison-previous-interval": "Previous interval (default)", + "time-for-comparison-days": "Day ago", + "time-for-comparison-weeks": "Week ago", + "time-for-comparison-months": "Month ago", + "time-for-comparison-years": "Year ago", + "time-for-comparison-custom-interval": "Custom interval", + "custom-interval-value": "Custom interval value (ms)", + "comparison-x-axis-settings": "Comparison X axis settings", + "axis-position": "Axis position", + "axis-position-top": "Top (default)", + "axis-position-bottom": "Bottom", + "custom-legend-settings": "Custom legend settings", + "enable-custom-legend": "Enable custom legend (this will allow you to use attribute/timeseries values in key labels)", + "key-name": "Key name", + "key-name-required": "Key name is required", + "key-type": "Key type", + "key-type-attribute": "Attribute", + "key-type-timeseries": "Timeseries", + "label-keys-list": "Keys list to use in labels", + "no-label-keys": "No keys configured", + "add-label-key": "Add new key" + }, "dashboard-state": { - "dashboard-state-settings": "Dashboard state settings", - "dashboard-state": "Dashboard state id", - "autofill-state-layout": "Autofill state layout height by default", - "default-margin": "Default widgets margin", - "default-background-color": "Default background color", - "sync-parent-state-params": "Sync state params with parent dashboard" + "dashboard-state-settings": "Dashboard state settings", + "dashboard-state": "Dashboard state id", + "autofill-state-layout": "Autofill state layout height by default", + "default-margin": "Default widgets margin", + "default-background-color": "Default background color", + "sync-parent-state-params": "Sync state params with parent dashboard" }, "date-range-navigator": { "localizationMap": { @@ -3248,137 +3310,137 @@ } }, "entities-hierarchy": { - "hierarchy-data-settings": "Hierarchy data settings", - "relations-query-function": "Node relations query function", - "has-children-function": "Node has children function", - "node-state-settings": "Node state settings", - "node-opened-function": "Default node opened function", - "node-disabled-function": "Node disabled function", - "display-settings": "Display settings", - "node-icon-function": "Node icon function", - "node-text-function": "Node text function", - "sort-settings": "Sort settings", - "nodes-sort-function": "Nodes sort function" + "hierarchy-data-settings": "Hierarchy data settings", + "relations-query-function": "Node relations query function", + "has-children-function": "Node has children function", + "node-state-settings": "Node state settings", + "node-opened-function": "Default node opened function", + "node-disabled-function": "Node disabled function", + "display-settings": "Display settings", + "node-icon-function": "Node icon function", + "node-text-function": "Node text function", + "sort-settings": "Sort settings", + "nodes-sort-function": "Nodes sort function" }, "gauge": { - "default-color": "Default color", - "radial-gauge-settings": "Radial gauge settings", - "ticks-settings": "Ticks settings", - "min-value": "Minimum value", - "max-value": "Maximum value", - "start-ticks-angle": "Start ticks angle", - "ticks-angle": "Ticks angle", - "major-ticks-count": "Major ticks count", - "major-ticks-color": "Major ticks color", - "minor-ticks-count": "Minor ticks count", - "minor-ticks-color": "Minor ticks color", - "tick-numbers-font": "Tick numbers font", - "unit-title-settings": "Unit title settings", - "show-unit-title": "Show unit title", - "unit-title": "Unit title", - "title-font": "Title text font", - "units-settings": "Units settings", - "units-font": "Units text font", - "value-box-settings": "Value box settings", - "show-value-box": "Show value box", - "value-int": "Digits count for integer part of value", - "value-font": "Value text font", - "value-box-rect-stroke-color": "Value box rectangle stroke color", - "value-box-rect-stroke-color-end": "Value box rectangle stroke color - end gradient", - "value-box-background-color": "Value box background color", - "value-box-shadow-color": "Value box shadow color", - "plate-settings": "Plate settings", - "show-plate-border": "Show plate border", - "plate-color": "Plate color", - "needle-settings": "Needle settings", - "needle-circle-size": "Needle circle size", - "needle-color": "Needle color", - "needle-color-end": "Needle color - end gradient", - "needle-color-shadow-up": "Upper half of the needle shadow color", - "needle-color-shadow-down": "Drop shadow needle color", - "highlights-settings": "Highlights settings", - "highlights-width": "Highlights width", - "highlights": "Highlights", - "highlight-from": "From", - "highlight-to": "To", - "highlight-color": "Color", - "no-highlights": "No highlights configured", - "add-highlight": "Add highlight", - "animation-settings": "Animation settings", - "enable-animation": "Enable animation", - "animation-duration": "Animation duration", - "animation-rule": "Animation rule", - "animation-linear": "Linear", - "animation-quad": "Quad", - "animation-quint": "Quint", - "animation-cycle": "Cycle", - "animation-bounce": "Bounce", - "animation-elastic": "Elastic", - "animation-dequad": "Dequad", - "animation-dequint": "Dequint", - "animation-decycle": "Decycle", - "animation-debounce": "Debounce", - "animation-delastic": "Delastic", - "linear-gauge-settings": "Linear gauge settings", - "bar-stroke-width": "Bar stroke width", - "bar-stroke-color": "Bar stroke color", - "bar-background-color": "Gauge bar background color", - "bar-background-color-end": "Bar background color - end gradient", - "progress-bar-color": "Progress bar color", - "progress-bar-color-end": "Progress bar color - end gradient", - "major-ticks-names": "Major ticks names", - "show-stroke-ticks": "Show ticks stroke", - "major-ticks-font": "Major ticks font", - "border-color": "Border color", - "border-width": "Border width", - "needle-circle-color": "Needle circle color", - "animation-target": "Animation target", - "animation-target-needle": "Needle", - "animation-target-plate": "Plate", - "common-settings": "Common gauge settings", - "gauge-type": "Gauge type", - "gauge-type-arc": "Arc", - "gauge-type-donut": "Donut", - "gauge-type-horizontal-bar": "Horizontal bar", - "gauge-type-vertical-bar": "Vertical bar", - "donut-start-angle": "Angle to start from", - "bar-settings": "Gauge bar settings", - "relative-bar-width": "Relative bar width", - "neon-glow-brightness": "Neon glow effect brightness, (0-100), 0 - disable effect", - "stripes-thickness": "Thickness of the stripes, 0 - no stripes", - "rounded-line-cap": "Display rounded line cap", - "bar-color-settings": "Bar color settings", - "use-precise-level-color-values": "Use precise color levels", - "bar-colors": "Bar colors, from lower to upper", - "color": "Color", - "no-bar-colors": "No bar colors configured", - "add-bar-color": "Add bar color", - "from": "From", - "to": "To", - "fixed-level-colors": "Bar colors using boundary values", - "gauge-title-settings": "Gauge title settings", - "show-gauge-title": "Show gauge title", - "gauge-title": "Gauge title", - "gauge-title-font": "Gauge title font", - "unit-title-and-timestamp-settings": "Unit title and timestamp settings", - "show-timestamp": "Show value timestamp", - "timestamp-format": "Timestamp format", - "label-font": "Font of label showing under value", - "value-settings": "Value settings", - "show-value": "Show value text", - "min-max-settings": "Minimum/maximum labels settings", - "show-min-max": "Show min and max values", - "min-max-font": "Font of minimum and maximum labels", - "show-ticks": "Show ticks", - "tick-width": "Tick width", - "tick-color": "Tick color", - "tick-values": "Tick values", - "no-tick-values": "No tick values configured", - "add-tick-value": "Add tick value" + "default-color": "Default color", + "radial-gauge-settings": "Radial gauge settings", + "ticks-settings": "Ticks settings", + "min-value": "Minimum value", + "max-value": "Maximum value", + "start-ticks-angle": "Start ticks angle", + "ticks-angle": "Ticks angle", + "major-ticks-count": "Major ticks count", + "major-ticks-color": "Major ticks color", + "minor-ticks-count": "Minor ticks count", + "minor-ticks-color": "Minor ticks color", + "tick-numbers-font": "Tick numbers font", + "unit-title-settings": "Unit title settings", + "show-unit-title": "Show unit title", + "unit-title": "Unit title", + "title-font": "Title text font", + "units-settings": "Units settings", + "units-font": "Units text font", + "value-box-settings": "Value box settings", + "show-value-box": "Show value box", + "value-int": "Digits count for integer part of value", + "value-font": "Value text font", + "value-box-rect-stroke-color": "Value box rectangle stroke color", + "value-box-rect-stroke-color-end": "Value box rectangle stroke color - end gradient", + "value-box-background-color": "Value box background color", + "value-box-shadow-color": "Value box shadow color", + "plate-settings": "Plate settings", + "show-plate-border": "Show plate border", + "plate-color": "Plate color", + "needle-settings": "Needle settings", + "needle-circle-size": "Needle circle size", + "needle-color": "Needle color", + "needle-color-end": "Needle color - end gradient", + "needle-color-shadow-up": "Upper half of the needle shadow color", + "needle-color-shadow-down": "Drop shadow needle color", + "highlights-settings": "Highlights settings", + "highlights-width": "Highlights width", + "highlights": "Highlights", + "highlight-from": "From", + "highlight-to": "To", + "highlight-color": "Color", + "no-highlights": "No highlights configured", + "add-highlight": "Add highlight", + "animation-settings": "Animation settings", + "enable-animation": "Enable animation", + "animation-duration": "Animation duration", + "animation-rule": "Animation rule", + "animation-linear": "Linear", + "animation-quad": "Quad", + "animation-quint": "Quint", + "animation-cycle": "Cycle", + "animation-bounce": "Bounce", + "animation-elastic": "Elastic", + "animation-dequad": "Dequad", + "animation-dequint": "Dequint", + "animation-decycle": "Decycle", + "animation-debounce": "Debounce", + "animation-delastic": "Delastic", + "linear-gauge-settings": "Linear gauge settings", + "bar-stroke-width": "Bar stroke width", + "bar-stroke-color": "Bar stroke color", + "bar-background-color": "Gauge bar background color", + "bar-background-color-end": "Bar background color - end gradient", + "progress-bar-color": "Progress bar color", + "progress-bar-color-end": "Progress bar color - end gradient", + "major-ticks-names": "Major ticks names", + "show-stroke-ticks": "Show ticks stroke", + "major-ticks-font": "Major ticks font", + "border-color": "Border color", + "border-width": "Border width", + "needle-circle-color": "Needle circle color", + "animation-target": "Animation target", + "animation-target-needle": "Needle", + "animation-target-plate": "Plate", + "common-settings": "Common gauge settings", + "gauge-type": "Gauge type", + "gauge-type-arc": "Arc", + "gauge-type-donut": "Donut", + "gauge-type-horizontal-bar": "Horizontal bar", + "gauge-type-vertical-bar": "Vertical bar", + "donut-start-angle": "Angle to start from", + "bar-settings": "Gauge bar settings", + "relative-bar-width": "Relative bar width", + "neon-glow-brightness": "Neon glow effect brightness, (0-100), 0 - disable effect", + "stripes-thickness": "Thickness of the stripes, 0 - no stripes", + "rounded-line-cap": "Display rounded line cap", + "bar-color-settings": "Bar color settings", + "use-precise-level-color-values": "Use precise color levels", + "bar-colors": "Bar colors, from lower to upper", + "color": "Color", + "no-bar-colors": "No bar colors configured", + "add-bar-color": "Add bar color", + "from": "From", + "to": "To", + "fixed-level-colors": "Bar colors using boundary values", + "gauge-title-settings": "Gauge title settings", + "show-gauge-title": "Show gauge title", + "gauge-title": "Gauge title", + "gauge-title-font": "Gauge title font", + "unit-title-and-timestamp-settings": "Unit title and timestamp settings", + "show-timestamp": "Show value timestamp", + "timestamp-format": "Timestamp format", + "label-font": "Font of label showing under value", + "value-settings": "Value settings", + "show-value": "Show value text", + "min-max-settings": "Minimum/maximum labels settings", + "show-min-max": "Show min and max values", + "min-max-font": "Font of minimum and maximum labels", + "show-ticks": "Show ticks", + "tick-width": "Tick width", + "tick-color": "Tick color", + "tick-values": "Tick values", + "no-tick-values": "No tick values configured", + "add-tick-value": "Add tick value" }, "html-card": { - "html": "HTML", - "css": "CSS" + "html": "HTML", + "css": "CSS" }, "input-widgets": { "attribute-not-allowed": "Attribute parameter cannot be used in this widget", @@ -3434,18 +3496,18 @@ "qr-code-text-function": "QR code text function" }, "label-widget": { - "label-pattern": "Pattern", - "label-pattern-hint": "Hint: for ex. 'Text ${keyName} units.' or ${#<key index>} units'", - "label-pattern-required": "Pattern is required", - "label-position": "Position (Percentage relative to background)", - "x-pos": "X", - "y-pos": "Y", - "background-color": "Background color", - "font-settings": "Font settings", - "background-image": "Background image", - "labels": "Labels", - "no-labels": "No labels configured", - "add-label": "Add label" + "label-pattern": "Pattern", + "label-pattern-hint": "Hint: for ex. 'Text ${keyName} units.' or ${#<key index>} units'", + "label-pattern-required": "Pattern is required", + "label-position": "Position (Percentage relative to background)", + "x-pos": "X", + "y-pos": "Y", + "background-color": "Background color", + "font-settings": "Font settings", + "background-image": "Background image", + "labels": "Labels", + "no-labels": "No labels configured", + "add-label": "Add label" }, "persistent-table": { "rpc-id": "RPC ID", @@ -3526,87 +3588,87 @@ } }, "markdown": { - "use-markdown-text-function": "Use markdown/HTML value function", - "markdown-text-function": "Markdown/HTML value function", - "markdown-text-pattern": "Markdown/HTML pattern (markdown or HTML with variables, for ex. '${entityName} or ${keyName} - some text.')", - "markdown-css": "Markdown/HTML CSS" + "use-markdown-text-function": "Use markdown/HTML value function", + "markdown-text-function": "Markdown/HTML value function", + "markdown-text-pattern": "Markdown/HTML pattern (markdown or HTML with variables, for ex. '${entityName} or ${keyName} - some text.')", + "markdown-css": "Markdown/HTML CSS" }, "simple-card": { - "label-position": "Label position", - "label-position-left": "Left", - "label-position-top": "Top" + "label-position": "Label position", + "label-position-left": "Left", + "label-position-top": "Top" }, "table": { - "common-table-settings": "Common Table Settings", - "enable-search": "Enable search", - "enable-sticky-header": "Always display header", - "enable-sticky-action": "Always display actions column", - "hidden-cell-button-display-mode": "Hidden cell button actions display mode", - "show-empty-space-hidden-action": "Show empty space instead of hidden cell button action", - "dont-reserve-space-hidden-action": "Don't reserve space for hidden action buttons", - "display-timestamp": "Display timestamp column", - "display-milliseconds": "Display timestamp milliseconds", - "display-pagination": "Display pagination", - "default-page-size": "Default page size", - "use-entity-label-tab-name": "Use entity label in tab name", - "hide-empty-lines": "Hide empty lines", - "row-style": "Row style", - "use-row-style-function": "Use row style function", - "row-style-function": "Row style function", - "cell-style": "Cell style", - "use-cell-style-function": "Use cell style function", - "cell-style-function": "Cell style function", - "cell-content": "Cell content", - "use-cell-content-function": "Use cell content function", - "cell-content-function": "Cell content function", - "show-latest-data-column": "Show latest data column", - "latest-data-column-order": "Latest data column order", - "entities-table-title": "Entities table title", - "enable-select-column-display": "Enable select columns to display", - "display-entity-name": "Display entity name column", - "entity-name-column-title": "Entity name column title", - "display-entity-label": "Display entity label column", - "entity-label-column-title": "Entity label column title", - "display-entity-type": "Display entity type column", - "default-sort-order": "Default sort order", - "column-width": "Column width (px or %)", - "default-column-visibility": "Default column visibility", - "column-visibility-visible": "Visible", - "column-visibility-hidden": "Hidden", - "column-selection-to-display": "Column selection in 'Columns to Display'", - "column-selection-to-display-enabled": "Enabled", - "column-selection-to-display-disabled": "Disabled", - "alarms-table-title": "Alarms table title", - "enable-alarms-selection": "Enable alarms selection", - "enable-alarms-search": "Enable alarms search", - "enable-alarm-filter": "Enable alarm filter", - "display-alarm-details": "Display alarm details", - "allow-alarms-ack": "Allow alarms acknowledgment", - "allow-alarms-clear": "Allow alarms clear" + "common-table-settings": "Common Table Settings", + "enable-search": "Enable search", + "enable-sticky-header": "Always display header", + "enable-sticky-action": "Always display actions column", + "hidden-cell-button-display-mode": "Hidden cell button actions display mode", + "show-empty-space-hidden-action": "Show empty space instead of hidden cell button action", + "dont-reserve-space-hidden-action": "Don't reserve space for hidden action buttons", + "display-timestamp": "Display timestamp column", + "display-milliseconds": "Display timestamp milliseconds", + "display-pagination": "Display pagination", + "default-page-size": "Default page size", + "use-entity-label-tab-name": "Use entity label in tab name", + "hide-empty-lines": "Hide empty lines", + "row-style": "Row style", + "use-row-style-function": "Use row style function", + "row-style-function": "Row style function", + "cell-style": "Cell style", + "use-cell-style-function": "Use cell style function", + "cell-style-function": "Cell style function", + "cell-content": "Cell content", + "use-cell-content-function": "Use cell content function", + "cell-content-function": "Cell content function", + "show-latest-data-column": "Show latest data column", + "latest-data-column-order": "Latest data column order", + "entities-table-title": "Entities table title", + "enable-select-column-display": "Enable select columns to display", + "display-entity-name": "Display entity name column", + "entity-name-column-title": "Entity name column title", + "display-entity-label": "Display entity label column", + "entity-label-column-title": "Entity label column title", + "display-entity-type": "Display entity type column", + "default-sort-order": "Default sort order", + "column-width": "Column width (px or %)", + "default-column-visibility": "Default column visibility", + "column-visibility-visible": "Visible", + "column-visibility-hidden": "Hidden", + "column-selection-to-display": "Column selection in 'Columns to Display'", + "column-selection-to-display-enabled": "Enabled", + "column-selection-to-display-disabled": "Disabled", + "alarms-table-title": "Alarms table title", + "enable-alarms-selection": "Enable alarms selection", + "enable-alarms-search": "Enable alarms search", + "enable-alarm-filter": "Enable alarm filter", + "display-alarm-details": "Display alarm details", + "allow-alarms-ack": "Allow alarms acknowledgment", + "allow-alarms-clear": "Allow alarms clear" }, "value-source": { - "value-source": "Value source", - "predefined-value": "Predefined value", - "entity-attribute": "Value taken from entity attribute", - "value": "Value", - "source-entity-alias": "Source entity alias", - "source-entity-attribute": "Source entity attribute" + "value-source": "Value source", + "predefined-value": "Predefined value", + "entity-attribute": "Value taken from entity attribute", + "value": "Value", + "source-entity-alias": "Source entity alias", + "source-entity-attribute": "Source entity attribute" }, "widget-font": { - "font-family": "Font family", - "size": "Size", - "relative-font-size": "Relative font size (percents)", - "font-style": "Style", - "font-style-normal": "Normal", - "font-style-italic": "Italic", - "font-style-oblique": "Oblique", - "font-weight": "Weight", - "font-weight-normal": "Normal", - "font-weight-bold": "Bold", - "font-weight-bolder": "Bolder", - "font-weight-lighter": "Lighter", - "color": "Color", - "shadow-color": "Shadow color" + "font-family": "Font family", + "size": "Size", + "relative-font-size": "Relative font size (percents)", + "font-style": "Style", + "font-style-normal": "Normal", + "font-style-italic": "Italic", + "font-style-oblique": "Oblique", + "font-weight": "Weight", + "font-weight-normal": "Normal", + "font-weight-bold": "Bold", + "font-weight-bolder": "Bolder", + "font-weight-lighter": "Lighter", + "color": "Color", + "shadow-color": "Shadow color" } }, "icon": { From 4e4ac0012ccf00cdb6d27eeeb3239a9706a35672 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 13 Apr 2022 21:55:03 +0200 Subject: [PATCH 049/276] used queueId instead of queueName in DeviceProfile and TbMsg --- .../server/actors/ActorSystemContext.java | 11 +- .../actors/ruleChain/DefaultTbContext.java | 71 +++--- .../RuleChainActorMessageProcessor.java | 2 +- .../rpc/processor/TelemetryEdgeProcessor.java | 33 ++- .../queue/DefaultQueueRoutingInfoService.java | 10 - .../queue/DefaultTbClusterService.java | 15 +- .../DefaultTbRuleEngineConsumerService.java | 11 +- .../TbRuleEngineProcessingResult.java | 7 +- ...TbRuleEngineProcessingStrategyFactory.java | 7 +- .../transport/DefaultTransportApiService.java | 15 +- common/cluster-api/src/main/proto/queue.proto | 32 ++- .../server/common/data/DeviceProfile.java | 5 +- .../server/common/data/id/QueueId.java | 2 - .../thingsboard/server/common/msg/TbMsg.java | 63 +++-- .../queue/discovery/HashPartitionService.java | 36 +-- .../queue/discovery/PartitionService.java | 3 +- .../queue/discovery/QueueRoutingInfo.java | 11 +- .../discovery/QueueRoutingInfoService.java | 5 - .../common/transport/TransportService.java | 4 - .../service/DefaultTransportService.java | 37 +-- .../TransportQueueRoutingInfoService.java | 15 -- .../dao/device/DeviceProfileServiceImpl.java | 12 +- .../server/dao/model/ModelConstants.java | 2 +- .../dao/model/sql/DeviceProfileEntity.java | 13 +- .../main/resources/sql/schema-entities.sql | 31 +-- dao/src/test/resources/sql/system-data.sql | 6 + .../rule/engine/api/TbContext.java | 14 +- .../TbCopyAttributesToEntityViewNode.java | 2 +- .../rule/engine/action/TbMsgCountNode.java | 4 +- .../rule/engine/debug/TbMsgGeneratorNode.java | 6 +- .../rule/engine/delay/TbMsgDelayNode.java | 2 +- .../rule/engine/flow/TbCheckpointNode.java | 12 +- .../flow/TbCheckpointNodeConfiguration.java | 3 + .../rule/engine/profile/AlarmState.java | 8 +- .../rule/engine/rpc/TbSendRPCRequestNode.java | 4 +- .../profile/TbDeviceProfileNodeTest.java | 30 +-- ui-ngx/src/app/core/http/entity.service.ts | 15 +- ui-ngx/src/app/core/http/queue.service.ts | 4 +- .../add-device-profile-dialog.component.html | 6 +- .../add-device-profile-dialog.component.ts | 8 +- .../profile/device-profile.component.html | 6 +- .../profile/device-profile.component.ts | 8 +- .../device-wizard-dialog.component.html | 6 +- .../device-wizard-dialog.component.scss | 2 +- .../wizard/device-wizard-dialog.component.ts | 11 +- .../queue/queue-autocomplete.component.html | 54 +++++ .../queue/queue-autocomplete.component.ts | 215 ++++++++++++++++++ ui-ngx/src/app/shared/models/device.models.ts | 3 +- ui-ngx/src/app/shared/shared.module.ts | 3 + .../assets/locale/locale.constant-en_US.json | 2 +- 50 files changed, 561 insertions(+), 326 deletions(-) create mode 100644 ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.html create mode 100644 ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.ts 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 889bd616fc..bd78631aa7 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -40,6 +40,7 @@ import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Event; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.msg.TbActorMsg; @@ -63,6 +64,7 @@ import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor; import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor; import org.thingsboard.server.dao.ota.OtaPackageService; +import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; @@ -330,6 +332,11 @@ public class ActorSystemContext { @Getter private TbRpcService tbRpcService; + @Lazy + @Autowired(required = false) + @Getter + private QueueService queueService; + @Value("${actors.session.max_concurrent_sessions_per_device:1}") @Getter private long maxConcurrentSessionsPerDevice; @@ -495,8 +502,8 @@ public class ActorSystemContext { return partitionService.resolve(serviceType, tenantId, entityId); } - public TopicPartitionInfo resolve(ServiceType serviceType, String queueName, TenantId tenantId, EntityId entityId) { - return partitionService.resolve(serviceType, queueName, tenantId, entityId); + public TopicPartitionInfo resolve(ServiceType serviceType, QueueId queueId, TenantId tenantId, EntityId entityId) { + return partitionService.resolve(serviceType, queueId, tenantId, entityId); } public String getServiceId() { diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 66fe935d68..ee3f5e15ac 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -46,6 +46,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; @@ -57,7 +58,6 @@ import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgProcessingStackItem; -import org.thingsboard.server.common.msg.queue.ServiceQueue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.asset.AssetService; @@ -72,6 +72,7 @@ import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.nosql.CassandraStatementTask; import org.thingsboard.server.dao.nosql.TbResultSetFuture; import org.thingsboard.server.dao.ota.OtaPackageService; +import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; @@ -161,8 +162,8 @@ class DefaultTbContext implements TbContext { } @Override - public void enqueue(TbMsg tbMsg, String queueName, Runnable onSuccess, Consumer onFailure) { - TopicPartitionInfo tpi = resolvePartition(tbMsg, queueName); + public void enqueue(TbMsg tbMsg, QueueId queueId, Runnable onSuccess, Consumer onFailure) { + TopicPartitionInfo tpi = resolvePartition(tbMsg, queueId); enqueue(tpi, tbMsg, onFailure, onSuccess); } @@ -213,33 +214,30 @@ class DefaultTbContext implements TbContext { } @Override - public void enqueueForTellNext(TbMsg tbMsg, String queueName, String relationType, Runnable onSuccess, Consumer onFailure) { - TopicPartitionInfo tpi = resolvePartition(tbMsg, queueName); - enqueueForTellNext(tpi, queueName, tbMsg, Collections.singleton(relationType), null, onSuccess, onFailure); + public void enqueueForTellNext(TbMsg tbMsg, QueueId queueId, String relationType, Runnable onSuccess, Consumer onFailure) { + TopicPartitionInfo tpi = resolvePartition(tbMsg, queueId); + enqueueForTellNext(tpi, queueId, tbMsg, Collections.singleton(relationType), null, onSuccess, onFailure); } @Override - public void enqueueForTellNext(TbMsg tbMsg, String queueName, Set relationTypes, Runnable onSuccess, Consumer onFailure) { - TopicPartitionInfo tpi = resolvePartition(tbMsg, queueName); - enqueueForTellNext(tpi, queueName, tbMsg, relationTypes, null, onSuccess, onFailure); + public void enqueueForTellNext(TbMsg tbMsg, QueueId queueId, Set relationTypes, Runnable onSuccess, Consumer onFailure) { + TopicPartitionInfo tpi = resolvePartition(tbMsg, queueId); + enqueueForTellNext(tpi, queueId, tbMsg, relationTypes, null, onSuccess, onFailure); } - private TopicPartitionInfo resolvePartition(TbMsg tbMsg, String queueName) { - if (StringUtils.isEmpty(queueName)) { - queueName = ServiceQueue.MAIN; - } - return mainCtx.resolve(ServiceType.TB_RULE_ENGINE, queueName, getTenantId(), tbMsg.getOriginator()); + private TopicPartitionInfo resolvePartition(TbMsg tbMsg, QueueId queueId) { + return mainCtx.resolve(ServiceType.TB_RULE_ENGINE, queueId, getTenantId(), tbMsg.getOriginator()); } private TopicPartitionInfo resolvePartition(TbMsg tbMsg) { - return resolvePartition(tbMsg, tbMsg.getQueueName()); + return resolvePartition(tbMsg, tbMsg.getQueueId()); } private void enqueueForTellNext(TopicPartitionInfo tpi, TbMsg source, Set relationTypes, String failureMessage, Runnable onSuccess, Consumer onFailure) { - enqueueForTellNext(tpi, source.getQueueName(), source, relationTypes, failureMessage, onSuccess, onFailure); + enqueueForTellNext(tpi, source.getQueueId(), source, relationTypes, failureMessage, onSuccess, onFailure); } - private void enqueueForTellNext(TopicPartitionInfo tpi, String queueName, TbMsg source, Set relationTypes, String failureMessage, Runnable onSuccess, Consumer onFailure) { + private void enqueueForTellNext(TopicPartitionInfo tpi, QueueId queueId, TbMsg source, Set relationTypes, String failureMessage, Runnable onSuccess, Consumer onFailure) { if (!source.isValid()) { log.trace("[{}] Skip invalid message: {}", getTenantId(), source); onFailure.accept(new IllegalArgumentException("Source message is no longer valid!")); @@ -247,7 +245,7 @@ class DefaultTbContext implements TbContext { } RuleChainId ruleChainId = nodeCtx.getSelf().getRuleChainId(); RuleNodeId ruleNodeId = nodeCtx.getSelf().getId(); - TbMsg tbMsg = TbMsg.newMsg(source, queueName, ruleChainId, ruleNodeId); + TbMsg tbMsg = TbMsg.newMsg(source, queueId, ruleChainId, ruleNodeId); TransportProtos.ToRuleEngineMsg.Builder msg = TransportProtos.ToRuleEngineMsg.newBuilder() .setTenantIdMSB(getTenantId().getId().getMostSignificantBits()) .setTenantIdLSB(getTenantId().getId().getLeastSignificantBits()) @@ -306,13 +304,13 @@ class DefaultTbContext implements TbContext { } @Override - public TbMsg newMsg(String queueName, String type, EntityId originator, TbMsgMetaData metaData, String data) { - return newMsg(queueName, type, originator, null, metaData, data); + public TbMsg newMsg(QueueId queueId, String type, EntityId originator, TbMsgMetaData metaData, String data) { + return newMsg(queueId, type, originator, null, metaData, data); } @Override - public TbMsg newMsg(String queueName, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) { - return TbMsg.newMsg(queueName, type, originator, customerId, metaData, data, nodeCtx.getSelf().getRuleChainId(), nodeCtx.getSelf().getId()); + public TbMsg newMsg(QueueId queueId, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) { + return TbMsg.newMsg(queueId, type, originator, customerId, metaData, data, nodeCtx.getSelf().getRuleChainId(), nodeCtx.getSelf().getId()); } @Override @@ -326,20 +324,17 @@ class DefaultTbContext implements TbContext { public TbMsg deviceCreatedMsg(Device device, RuleNodeId ruleNodeId) { RuleChainId ruleChainId = null; - String queueName = ServiceQueue.MAIN; + QueueId queueId = null; if (device.getDeviceProfileId() != null) { DeviceProfile deviceProfile = mainCtx.getDeviceProfileCache().find(device.getDeviceProfileId()); if (deviceProfile == null) { log.warn("[{}] Device profile is null!", device.getDeviceProfileId()); - ruleChainId = null; - queueName = ServiceQueue.MAIN; } else { ruleChainId = deviceProfile.getDefaultRuleChainId(); - String defaultQueueName = deviceProfile.getDefaultQueueName(); - queueName = defaultQueueName != null ? defaultQueueName : ServiceQueue.MAIN; + queueId = deviceProfile.getDefaultQueueId(); } } - return entityActionMsg(device, device.getId(), ruleNodeId, DataConstants.ENTITY_CREATED, queueName, ruleChainId); + return entityActionMsg(device, device.getId(), ruleNodeId, DataConstants.ENTITY_CREATED, queueId, ruleChainId); } public TbMsg assetCreatedMsg(Asset asset, RuleNodeId ruleNodeId) { @@ -348,21 +343,18 @@ class DefaultTbContext implements TbContext { public TbMsg alarmActionMsg(Alarm alarm, RuleNodeId ruleNodeId, String action) { RuleChainId ruleChainId = null; - String queueName = ServiceQueue.MAIN; + QueueId queueId = null; if (EntityType.DEVICE.equals(alarm.getOriginator().getEntityType())) { DeviceId deviceId = new DeviceId(alarm.getOriginator().getId()); DeviceProfile deviceProfile = mainCtx.getDeviceProfileCache().get(getTenantId(), deviceId); if (deviceProfile == null) { log.warn("[{}] Device profile is null!", deviceId); - ruleChainId = null; - queueName = ServiceQueue.MAIN; } else { ruleChainId = deviceProfile.getDefaultRuleChainId(); - String defaultQueueName = deviceProfile.getDefaultQueueName(); - queueName = defaultQueueName != null ? defaultQueueName : ServiceQueue.MAIN; + queueId = deviceProfile.getDefaultQueueId(); } } - return entityActionMsg(alarm, alarm.getId(), ruleNodeId, action, queueName, ruleChainId); + return entityActionMsg(alarm, alarm.getId(), ruleNodeId, action, queueId, ruleChainId); } @Override @@ -371,12 +363,12 @@ class DefaultTbContext implements TbContext { } public TbMsg entityActionMsg(E entity, I id, RuleNodeId ruleNodeId, String action) { - return entityActionMsg(entity, id, ruleNodeId, action, ServiceQueue.MAIN, null); + return entityActionMsg(entity, id, ruleNodeId, action, null, null); } - public TbMsg entityActionMsg(E entity, I id, RuleNodeId ruleNodeId, String action, String queueName, RuleChainId ruleChainId) { + public TbMsg entityActionMsg(E entity, I id, RuleNodeId ruleNodeId, String action, QueueId queueId, RuleChainId ruleChainId) { try { - return TbMsg.newMsg(queueName, action, id, getActionMetaData(ruleNodeId), mapper.writeValueAsString(mapper.valueToTree(entity)), ruleChainId, null); + return TbMsg.newMsg(queueId, action, id, getActionMetaData(ruleNodeId), mapper.writeValueAsString(mapper.valueToTree(entity)), ruleChainId, null); } catch (JsonProcessingException | IllegalArgumentException e) { throw new RuntimeException("Failed to process " + id.getEntityType().name().toLowerCase() + " " + action + " msg: " + e); } @@ -548,6 +540,11 @@ class DefaultTbContext implements TbContext { return mainCtx.getEdgeEventService(); } + @Override + public QueueService getQueueService() { + return mainCtx.getQueueService(); + } + @Override public EventLoopGroup getSharedEventLoop() { return mainCtx.getSharedEventLoopGroupService().getSharedEventLoopGroup(); diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java index b4911650f3..add500f058 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java @@ -293,7 +293,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor ruleNodeRelations = nodeRoutes.get(originatorNodeId); if (ruleNodeRelations == null) { // When unchecked, this will cause NullPointerException when rule node doesn't exist anymore diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java index 9a9b568a23..9a08962e12 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java @@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; @@ -51,7 +52,6 @@ import org.thingsboard.server.common.data.kv.AttributeKey; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.common.msg.queue.ServiceQueue; import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.common.transport.util.JsonUtils; @@ -134,24 +134,23 @@ public class TelemetryEdgeProcessor extends BaseEdgeProcessor { return metaData; } - private Pair getDefaultQueueNameAndRuleChainId(TenantId tenantId, EntityId entityId) { + private Pair getDefaultQueueNameAndRuleChainId(TenantId tenantId, EntityId entityId) { if (EntityType.DEVICE.equals(entityId.getEntityType())) { DeviceProfile deviceProfile = deviceProfileCache.get(tenantId, new DeviceId(entityId.getId())); RuleChainId ruleChainId; - String queueName; + QueueId queueId; if (deviceProfile == null) { log.warn("[{}] Device profile is null!", entityId); ruleChainId = null; - queueName = ServiceQueue.MAIN; + queueId = null; } else { ruleChainId = deviceProfile.getDefaultRuleChainId(); - String defaultQueueName = deviceProfile.getDefaultQueueName(); - queueName = defaultQueueName != null ? defaultQueueName : ServiceQueue.MAIN; + queueId = deviceProfile.getDefaultQueueId(); } - return new ImmutablePair<>(queueName, ruleChainId); + return new ImmutablePair<>(queueId, ruleChainId); } else { - return new ImmutablePair<>(ServiceQueue.MAIN, null); + return new ImmutablePair<>(null, null); } } @@ -160,10 +159,10 @@ public class TelemetryEdgeProcessor extends BaseEdgeProcessor { for (TransportProtos.TsKvListProto tsKv : msg.getTsKvListList()) { JsonObject json = JsonUtils.getJsonObject(tsKv.getKvList()); metaData.putValue("ts", tsKv.getTs() + ""); - Pair defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId); - String queueName = defaultQueueAndRuleChain.getKey(); + Pair defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId); + QueueId queueId = defaultQueueAndRuleChain.getKey(); RuleChainId ruleChainId = defaultQueueAndRuleChain.getValue(); - TbMsg tbMsg = TbMsg.newMsg(queueName, SessionMsgType.POST_TELEMETRY_REQUEST.name(), entityId, customerId, metaData, gson.toJson(json), ruleChainId, null); + TbMsg tbMsg = TbMsg.newMsg(queueId, SessionMsgType.POST_TELEMETRY_REQUEST.name(), entityId, customerId, metaData, gson.toJson(json), ruleChainId, null); tbClusterService.pushMsgToRuleEngine(tenantId, tbMsg.getOriginator(), tbMsg, new TbQueueCallback() { @Override public void onSuccess(TbQueueMsgMetadata metadata) { @@ -183,10 +182,10 @@ public class TelemetryEdgeProcessor extends BaseEdgeProcessor { private ListenableFuture processPostAttributes(TenantId tenantId, CustomerId customerId, EntityId entityId, TransportProtos.PostAttributeMsg msg, TbMsgMetaData metaData) { SettableFuture futureToSet = SettableFuture.create(); JsonObject json = JsonUtils.getJsonObject(msg.getKvList()); - Pair defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId); - String queueName = defaultQueueAndRuleChain.getKey(); + Pair defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId); + QueueId queueId = defaultQueueAndRuleChain.getKey(); RuleChainId ruleChainId = defaultQueueAndRuleChain.getValue(); - TbMsg tbMsg = TbMsg.newMsg(queueName, SessionMsgType.POST_ATTRIBUTES_REQUEST.name(), entityId, customerId, metaData, gson.toJson(json), ruleChainId, null); + TbMsg tbMsg = TbMsg.newMsg(queueId, SessionMsgType.POST_ATTRIBUTES_REQUEST.name(), entityId, customerId, metaData, gson.toJson(json), ruleChainId, null); tbClusterService.pushMsgToRuleEngine(tenantId, tbMsg.getOriginator(), tbMsg, new TbQueueCallback() { @Override public void onSuccess(TbQueueMsgMetadata metadata) { @@ -210,10 +209,10 @@ public class TelemetryEdgeProcessor extends BaseEdgeProcessor { Futures.addCallback(future, new FutureCallback>() { @Override public void onSuccess(@Nullable List voids) { - Pair defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId); - String queueName = defaultQueueAndRuleChain.getKey(); + Pair defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId); + QueueId queueId = defaultQueueAndRuleChain.getKey(); RuleChainId ruleChainId = defaultQueueAndRuleChain.getValue(); - TbMsg tbMsg = TbMsg.newMsg(queueName, DataConstants.ATTRIBUTES_UPDATED, entityId, customerId, metaData, gson.toJson(json), ruleChainId, null); + TbMsg tbMsg = TbMsg.newMsg(queueId, DataConstants.ATTRIBUTES_UPDATED, entityId, customerId, metaData, gson.toJson(json), ruleChainId, null); tbClusterService.pushMsgToRuleEngine(tenantId, tbMsg.getOriginator(), tbMsg, new TbQueueCallback() { @Override public void onSuccess(TbQueueMsgMetadata metadata) { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultQueueRoutingInfoService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultQueueRoutingInfoService.java index df22d42e62..d7a333a090 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultQueueRoutingInfoService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultQueueRoutingInfoService.java @@ -18,7 +18,6 @@ package org.thingsboard.server.service.queue; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.queue.discovery.QueueRoutingInfo; import org.thingsboard.server.queue.discovery.QueueRoutingInfoService; @@ -42,13 +41,4 @@ public class DefaultQueueRoutingInfoService implements QueueRoutingInfoService { return queueService.findAllQueues().stream().map(QueueRoutingInfo::new).collect(Collectors.toList()); } - @Override - public List getMainQueuesRoutingInfo() { - return queueService.findAllMainQueues().stream().map(QueueRoutingInfo::new).collect(Collectors.toList()); - } - - @Override - public List getQueuesRoutingInfo(TenantId tenantId) { - return queueService.findQueuesByTenantId(tenantId).stream().map(QueueRoutingInfo::new).collect(Collectors.toList()); - } } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 8f1db53764..102dcbc32c 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -29,6 +29,7 @@ import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.data.ApiUsageState; @@ -169,7 +170,7 @@ public class DefaultTbClusterService implements TbClusterService { tbMsg = transformMsg(tbMsg, deviceProfileCache.get(tenantId, new DeviceProfileId(entityId.getId()))); } } - TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tbMsg.getQueueName(), tenantId, entityId); + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tbMsg.getQueueId(), tenantId, entityId); log.trace("PUSHING msg: {} to:{}", tbMsg, tpi); ToRuleEngineMsg msg = ToRuleEngineMsg.newBuilder() .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) @@ -182,16 +183,16 @@ public class DefaultTbClusterService implements TbClusterService { private TbMsg transformMsg(TbMsg tbMsg, DeviceProfile deviceProfile) { if (deviceProfile != null) { RuleChainId targetRuleChainId = deviceProfile.getDefaultRuleChainId(); - String targetQueueName = deviceProfile.getDefaultQueueName(); + QueueId targetQueueId = deviceProfile.getDefaultQueueId(); boolean isRuleChainTransform = targetRuleChainId != null && !targetRuleChainId.equals(tbMsg.getRuleChainId()); - boolean isQueueTransform = targetQueueName != null && !targetQueueName.equals(tbMsg.getQueueName()); + boolean isQueueTransform = targetQueueId != null && !targetQueueId.equals(tbMsg.getQueueId()); if (isRuleChainTransform && isQueueTransform) { - tbMsg = TbMsg.transformMsg(tbMsg, targetRuleChainId, targetQueueName); + tbMsg = TbMsg.transformMsg(tbMsg, targetRuleChainId, targetQueueId); } else if (isRuleChainTransform) { tbMsg = TbMsg.transformMsg(tbMsg, targetRuleChainId); } else if (isQueueTransform) { - tbMsg = TbMsg.transformMsg(tbMsg, targetQueueName); + tbMsg = TbMsg.transformMsg(tbMsg, targetQueueId); } } return tbMsg; @@ -495,6 +496,8 @@ public class DefaultTbClusterService implements TbClusterService { TransportProtos.QueueUpdateMsg queueUpdateMsg = TransportProtos.QueueUpdateMsg.newBuilder() .setTenantIdMSB(queue.getTenantId().getId().getMostSignificantBits()) .setTenantIdLSB(queue.getTenantId().getId().getLeastSignificantBits()) + .setQueueIdMSB(queue.getId().getId().getMostSignificantBits()) + .setQueueIdLSB(queue.getId().getId().getLeastSignificantBits()) .setQueueName(queue.getName()) .setQueueTopic(queue.getTopic()) .setPartitions(queue.getPartitions()) @@ -537,6 +540,8 @@ public class DefaultTbClusterService implements TbClusterService { TransportProtos.QueueDeleteMsg queueDeleteMsg = TransportProtos.QueueDeleteMsg.newBuilder() .setTenantIdMSB(queue.getTenantId().getId().getMostSignificantBits()) .setTenantIdLSB(queue.getTenantId().getId().getLeastSignificantBits()) + .setQueueIdMSB(queue.getId().getId().getMostSignificantBits()) + .setQueueIdLSB(queue.getId().getId().getLeastSignificantBits()) .setQueueName(queue.getName()) .build(); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java index 44cf8b19a6..4f5974457a 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java @@ -22,6 +22,7 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.rpc.RpcError; @@ -275,7 +276,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< final boolean timeout = !ctx.await(configuration.getPackProcessingTimeout(), TimeUnit.MILLISECONDS); - TbRuleEngineProcessingResult result = new TbRuleEngineProcessingResult(configuration.getName(), timeout, ctx); + TbRuleEngineProcessingResult result = new TbRuleEngineProcessingResult(configuration.getId(), timeout, ctx); if (timeout) { printFirstOrAll(configuration, ctx, ctx.getPendingMap(), "Timeout"); } @@ -340,7 +341,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< new TbMsgPackCallback(id, tenantId, ctx); try { if (toRuleEngineMsg.getTbMsg() != null && !toRuleEngineMsg.getTbMsg().isEmpty()) { - forwardToRuleEngineActor(configuration.getName(), tenantId, toRuleEngineMsg, callback); + forwardToRuleEngineActor(configuration.getId(), tenantId, toRuleEngineMsg, callback); } else { callback.onSuccess(); } @@ -354,7 +355,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< log.info("{} to process [{}] messages", prefix, map.size()); for (Map.Entry> pending : map.entrySet()) { ToRuleEngineMsg tmp = pending.getValue().getValue(); - TbMsg tmpMsg = TbMsg.fromBytes(configuration.getName(), tmp.getTbMsg().toByteArray(), TbMsgCallback.EMPTY); + TbMsg tmpMsg = TbMsg.fromBytes(configuration.getId(), tmp.getTbMsg().toByteArray(), TbMsgCallback.EMPTY); RuleNodeInfo ruleNodeInfo = ctx.getLastVisitedRuleNode(pending.getKey()); if (printAll) { log.trace("[{}] {} to process message: {}, Last Rule Node: {}", TenantId.fromUUID(new UUID(tmp.getTenantIdMSB(), tmp.getTenantIdLSB())), prefix, tmpMsg, ruleNodeInfo); @@ -454,8 +455,8 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< partitionService.removeQueue(queueDeleteMsg); } - private void forwardToRuleEngineActor(String queueName, TenantId tenantId, ToRuleEngineMsg toRuleEngineMsg, TbMsgCallback callback) { - TbMsg tbMsg = TbMsg.fromBytes(queueName, toRuleEngineMsg.getTbMsg().toByteArray(), callback); + private void forwardToRuleEngineActor(QueueId queueId, TenantId tenantId, ToRuleEngineMsg toRuleEngineMsg, TbMsgCallback callback) { + TbMsg tbMsg = TbMsg.fromBytes(queueId, toRuleEngineMsg.getTbMsg().toByteArray(), callback); QueueToRuleEngineMsg msg; ProtocolStringList relationTypesList = toRuleEngineMsg.getRelationTypesList(); Set relationTypes = null; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingResult.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingResult.java index 895de2412c..2e3fedb766 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingResult.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingResult.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.queue.processing; import lombok.Getter; +import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; @@ -28,7 +29,7 @@ import java.util.concurrent.ConcurrentMap; public class TbRuleEngineProcessingResult { @Getter - private final String queueName; + private final QueueId queueId; @Getter private final boolean success; @Getter @@ -36,8 +37,8 @@ public class TbRuleEngineProcessingResult { @Getter private final TbMsgPackProcessingContext ctx; - public TbRuleEngineProcessingResult(String queueName, boolean timeout, TbMsgPackProcessingContext ctx) { - this.queueName = queueName; + public TbRuleEngineProcessingResult(QueueId queueId, boolean timeout, TbMsgPackProcessingContext ctx) { + this.queueId = queueId; this.timeout = timeout; this.ctx = ctx; this.success = !timeout && ctx.getPendingMap().isEmpty() && ctx.getFailedMap().isEmpty(); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java index 3056e65c78..2191c38017 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java @@ -23,7 +23,6 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.queue.TbMsgCallback; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.settings.TbRuleEngineQueueAckStrategyConfiguration; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -126,7 +125,7 @@ public class TbRuleEngineProcessingStrategyFactory { } log.debug("[{}] Going to reprocess {} messages", queueName, toReprocess.size()); if (log.isTraceEnabled()) { - toReprocess.forEach((id, msg) -> log.trace("Going to reprocess [{}]: {}", id, TbMsg.fromBytes(result.getQueueName(), msg.getValue().getTbMsg().toByteArray(), TbMsgCallback.EMPTY))); + toReprocess.forEach((id, msg) -> log.trace("Going to reprocess [{}]: {}", id, TbMsg.fromBytes(result.getQueueId(), msg.getValue().getTbMsg().toByteArray(), TbMsgCallback.EMPTY))); } if (pauseBetweenRetries > 0) { try { @@ -165,10 +164,10 @@ public class TbRuleEngineProcessingStrategyFactory { log.debug("[{}] Reprocessing skipped for {} failed and {} timeout messages", queueName, result.getFailedMap().size(), result.getPendingMap().size()); } if (log.isTraceEnabled()) { - result.getFailedMap().forEach((id, msg) -> log.trace("Failed messages [{}]: {}", id, TbMsg.fromBytes(result.getQueueName(), msg.getValue().getTbMsg().toByteArray(), TbMsgCallback.EMPTY))); + result.getFailedMap().forEach((id, msg) -> log.trace("Failed messages [{}]: {}", id, TbMsg.fromBytes(result.getQueueId(), msg.getValue().getTbMsg().toByteArray(), TbMsgCallback.EMPTY))); } if (log.isTraceEnabled()) { - result.getPendingMap().forEach((id, msg) -> log.trace("Timeout messages [{}]: {}", id, TbMsg.fromBytes(result.getQueueName(), msg.getValue().getTbMsg().toByteArray(), TbMsgCallback.EMPTY))); + result.getPendingMap().forEach((id, msg) -> log.trace("Timeout messages [{}]: {}", id, TbMsg.fromBytes(result.getQueueId(), msg.getValue().getTbMsg().toByteArray(), TbMsgCallback.EMPTY))); } return new TbRuleEngineProcessingDecision(true, null); } diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 0cba1ec325..ca5a978426 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -180,10 +180,6 @@ public class DefaultTransportApiService implements TransportApiService { result = handle(transportApiRequestMsg.getDeviceCredentialsRequestMsg()); } else if (transportApiRequestMsg.hasOtaPackageRequestMsg()) { result = handle(transportApiRequestMsg.getOtaPackageRequestMsg()); - } else if (transportApiRequestMsg.hasGetAllMainQueueRoutingInfoRequestMsg()) { - return Futures.transform(handle(transportApiRequestMsg.getGetAllMainQueueRoutingInfoRequestMsg()), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); - } else if (transportApiRequestMsg.hasGetTenantQueueRoutingInfoRequestMsg()) { - return Futures.transform(handle(transportApiRequestMsg.getGetTenantQueueRoutingInfoRequestMsg()), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); } else if (transportApiRequestMsg.hasGetAllQueueRoutingInfoRequestMsg()) { return Futures.transform(handle(transportApiRequestMsg.getGetAllQueueRoutingInfoRequestMsg()), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); } @@ -646,25 +642,18 @@ public class DefaultTransportApiService implements TransportApiService { } } - private ListenableFuture handle(TransportProtos.GetAllMainQueueRoutingInfoRequestMsg requestMsg) { - return queuesToTransportApiResponseMsg(queueService.findAllMainQueues()); - } - private ListenableFuture handle(TransportProtos.GetAllQueueRoutingInfoRequestMsg requestMsg) { return queuesToTransportApiResponseMsg(queueService.findAllQueues()); } - private ListenableFuture handle(TransportProtos.GetTenantQueueRoutingInfoRequestMsg requestMsg) { - TenantId tenantId = new TenantId(new UUID(requestMsg.getTenantIdMSB(), requestMsg.getTenantIdLSB())); - return queuesToTransportApiResponseMsg(queueService.findQueuesByTenantId(tenantId)); - } - private ListenableFuture queuesToTransportApiResponseMsg(List queues) { return Futures.immediateFuture(TransportApiResponseMsg.newBuilder() .addAllGetQueueRoutingInfoResponseMsgs(queues.stream() .map(queue -> TransportProtos.GetQueueRoutingInfoResponseMsg.newBuilder() .setTenantIdMSB(queue.getTenantId().getId().getMostSignificantBits()) .setTenantIdLSB(queue.getTenantId().getId().getLeastSignificantBits()) + .setQueueIdMSB(queue.getId().getId().getMostSignificantBits()) + .setQueueIdLSB(queue.getId().getId().getLeastSignificantBits()) .setQueueName(queue.getName()) .setQueueTopic(queue.getTopic()) .setPartitions(queue.getPartitions()) diff --git a/common/cluster-api/src/main/proto/queue.proto b/common/cluster-api/src/main/proto/queue.proto index 014244cb08..8b4164e30f 100644 --- a/common/cluster-api/src/main/proto/queue.proto +++ b/common/cluster-api/src/main/proto/queue.proto @@ -197,37 +197,35 @@ message GetEntityProfileRequestMsg { int64 entityIdLSB = 3; } -message GetAllMainQueueRoutingInfoRequestMsg { -} - message GetAllQueueRoutingInfoRequestMsg { } -message GetTenantQueueRoutingInfoRequestMsg { - int64 tenantIdMSB = 1; - int64 tenantIdLSB = 2; -} - message GetQueueRoutingInfoResponseMsg { int64 tenantIdMSB = 1; int64 tenantIdLSB = 2; - string queueName = 3; - string queueTopic = 4; - int32 partitions = 5; + int64 queueIdMSB = 3; + int64 queueIdLSB = 4; + string queueName = 5; + string queueTopic = 6; + int32 partitions = 7; } message QueueUpdateMsg { int64 tenantIdMSB = 1; int64 tenantIdLSB = 2; - string queueName = 3; - string queueTopic = 4; - int32 partitions = 5; + int64 queueIdMSB = 3; + int64 queueIdLSB = 4; + string queueName = 5; + string queueTopic = 6; + int32 partitions = 7; } message QueueDeleteMsg { int64 tenantIdMSB = 1; int64 tenantIdLSB = 2; - string queueName = 3; + int64 queueIdMSB = 3; + int64 queueIdLSB = 4; + string queueName = 5; } message LwM2MRegistrationRequestMsg { @@ -706,9 +704,7 @@ message TransportApiRequestMsg { GetSnmpDevicesRequestMsg snmpDevicesRequestMsg = 11; GetDeviceRequestMsg deviceRequestMsg = 12; GetDeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 13; - GetAllMainQueueRoutingInfoRequestMsg GetAllMainQueueRoutingInfoRequestMsg = 14; - GetTenantQueueRoutingInfoRequestMsg getTenantQueueRoutingInfoRequestMsg = 15; - GetAllQueueRoutingInfoRequestMsg getAllQueueRoutingInfoRequestMsg = 16; + GetAllQueueRoutingInfoRequestMsg getAllQueueRoutingInfoRequestMsg = 14; } /* Response from ThingsBoard Core Service to Transport Service */ diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java index 1dd6a2ad19..36cbda47c3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.OtaPackageId; +import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.validation.Length; @@ -76,7 +77,7 @@ public class DeviceProfile extends SearchTextBased implements H @ApiModelProperty(position = 8, value = "Reference to the rule engine queue. " + "If present, the specified queue will be used to store all unprocessed messages related to device, including telemetry, attribute updates, etc. " + "Otherwise, the 'Main' queue will be used to store those messages.") - private String defaultQueueName; + private QueueId defaultQueueId; @Valid private transient DeviceProfileData profileData; @JsonIgnore @@ -107,7 +108,7 @@ public class DeviceProfile extends SearchTextBased implements H this.isDefault = deviceProfile.isDefault(); this.defaultRuleChainId = deviceProfile.getDefaultRuleChainId(); this.defaultDashboardId = deviceProfile.getDefaultDashboardId(); - this.defaultQueueName = deviceProfile.getDefaultQueueName(); + this.defaultQueueId = deviceProfile.getDefaultQueueId(); this.setProfileData(deviceProfile.getProfileData()); this.provisionDeviceKey = deviceProfile.getProvisionDeviceKey(); this.firmwareId = deviceProfile.getFirmwareId(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java index 37a4136d46..c0c4a3cde2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java @@ -16,7 +16,6 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import org.thingsboard.server.common.data.EntityType; @@ -35,7 +34,6 @@ public class QueueId extends UUIDBased implements EntityId { return new QueueId(UUID.fromString(queueId)); } - @JsonIgnore @Override public EntityType getEntityType() { return EntityType.QUEUE; diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java index 879c10ed5e..bb3be80629 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java @@ -27,15 +27,14 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.msg.gen.MsgProtos; -import org.thingsboard.server.common.msg.queue.ServiceQueue; import org.thingsboard.server.common.msg.queue.TbMsgCallback; import java.io.Serializable; import java.util.UUID; -import java.util.concurrent.atomic.AtomicInteger; /** * Created by ashvayka on 13.01.18. @@ -44,7 +43,7 @@ import java.util.concurrent.atomic.AtomicInteger; @Slf4j public final class TbMsg implements Serializable { - private final String queueName; + private final QueueId queueId; private final UUID id; private final long ts; private final String type; @@ -68,12 +67,12 @@ public final class TbMsg implements Serializable { return ctx.getAndIncrementRuleNodeCounter(); } - public static TbMsg newMsg(String queueName, String type, EntityId originator, TbMsgMetaData metaData, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { - return newMsg(queueName, type, originator, null, metaData, data, ruleChainId, ruleNodeId); + public static TbMsg newMsg(QueueId queueId, String type, EntityId originator, TbMsgMetaData metaData, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { + return newMsg(queueId, type, originator, null, metaData, data, ruleChainId, ruleNodeId); } - public static TbMsg newMsg(String queueName, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { - return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, + public static TbMsg newMsg(QueueId queueId, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { + return new TbMsg(queueId, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, metaData.copy(), TbMsgDataType.JSON, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY); } @@ -82,23 +81,23 @@ public final class TbMsg implements Serializable { } public static TbMsg newMsg(String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) { - return new TbMsg(ServiceQueue.MAIN, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, + return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY); } // REALLY NEW MSG - public static TbMsg newMsg(String queueName, String type, EntityId originator, TbMsgMetaData metaData, String data) { - return newMsg(queueName, type, originator, null, metaData, data); + public static TbMsg newMsg(QueueId queueId, String type, EntityId originator, TbMsgMetaData metaData, String data) { + return newMsg(queueId, type, originator, null, metaData, data); } - public static TbMsg newMsg(String queueName, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) { - return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, + public static TbMsg newMsg(QueueId queueId, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) { + return new TbMsg(queueId, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY); } public static TbMsg newMsg(String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data) { - return new TbMsg(ServiceQueue.MAIN, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, + return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, metaData.copy(), dataType, data, null, null, null, TbMsgCallback.EMPTY); } @@ -109,50 +108,50 @@ public final class TbMsg implements Serializable { // For Tests only public static TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, TbMsgDataType dataType, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { - return new TbMsg(ServiceQueue.MAIN, UUID.randomUUID(), System.currentTimeMillis(), type, originator, null, + return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, null, metaData.copy(), dataType, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY); } public static TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, String data, TbMsgCallback callback) { - return new TbMsg(ServiceQueue.MAIN, UUID.randomUUID(), System.currentTimeMillis(), type, originator, null, + return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, null, metaData.copy(), TbMsgDataType.JSON, data, null, null, null, callback); } public static TbMsg transformMsg(TbMsg tbMsg, String type, EntityId originator, TbMsgMetaData metaData, String data) { - return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, type, originator, tbMsg.customerId, metaData.copy(), tbMsg.dataType, + return new TbMsg(tbMsg.queueId, tbMsg.id, tbMsg.ts, type, originator, tbMsg.customerId, metaData.copy(), tbMsg.dataType, data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.callback); } public static TbMsg transformMsg(TbMsg tbMsg, CustomerId customerId) { - return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, customerId, tbMsg.metaData, tbMsg.dataType, + return new TbMsg(tbMsg.queueId, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, customerId, tbMsg.metaData, tbMsg.dataType, tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); } public static TbMsg transformMsg(TbMsg tbMsg, RuleChainId ruleChainId) { - return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, + return new TbMsg(tbMsg.queueId, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, tbMsg.data, ruleChainId, null, tbMsg.ctx.copy(), tbMsg.getCallback()); } - public static TbMsg transformMsg(TbMsg tbMsg, String queueName) { - return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, + public static TbMsg transformMsg(TbMsg tbMsg, QueueId queueId) { + return new TbMsg(queueId, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, tbMsg.data, tbMsg.getRuleChainId(), null, tbMsg.ctx.copy(), tbMsg.getCallback()); } - public static TbMsg transformMsg(TbMsg tbMsg, RuleChainId ruleChainId, String queueName) { - return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, + public static TbMsg transformMsg(TbMsg tbMsg, RuleChainId ruleChainId, QueueId queueId) { + return new TbMsg(queueId, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, tbMsg.data, ruleChainId, null, tbMsg.ctx.copy(), tbMsg.getCallback()); } //used for enqueueForTellNext - public static TbMsg newMsg(TbMsg tbMsg, String queueName, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { - return new TbMsg(queueName, UUID.randomUUID(), tbMsg.getTs(), tbMsg.getType(), tbMsg.getOriginator(), tbMsg.customerId, tbMsg.getMetaData().copy(), + public static TbMsg newMsg(TbMsg tbMsg, QueueId queueId, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { + return new TbMsg(queueId, UUID.randomUUID(), tbMsg.getTs(), tbMsg.getType(), tbMsg.getOriginator(), tbMsg.customerId, tbMsg.getMetaData().copy(), tbMsg.getDataType(), tbMsg.getData(), ruleChainId, ruleNodeId, tbMsg.ctx.copy(), TbMsgCallback.EMPTY); } - private TbMsg(String queueName, UUID id, long ts, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data, + private TbMsg(QueueId queueId, UUID id, long ts, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId, TbMsgProcessingCtx ctx, TbMsgCallback callback) { this.id = id; - this.queueName = queueName != null ? queueName : ServiceQueue.MAIN; + this.queueId = queueId; if (ts > 0) { this.ts = ts; } else { @@ -221,7 +220,7 @@ public final class TbMsg implements Serializable { return builder.build().toByteArray(); } - public static TbMsg fromBytes(String queueName, byte[] data, TbMsgCallback callback) { + public static TbMsg fromBytes(QueueId queueId, byte[] data, TbMsgCallback callback) { try { MsgProtos.TbMsgProto proto = MsgProtos.TbMsgProto.parseFrom(data); TbMsgMetaData metaData = new TbMsgMetaData(proto.getMetaData().getDataMap()); @@ -248,7 +247,7 @@ public final class TbMsg implements Serializable { } TbMsgDataType dataType = TbMsgDataType.values()[proto.getDataType()]; - return new TbMsg(queueName, UUID.fromString(proto.getId()), proto.getTs(), proto.getType(), entityId, customerId, + return new TbMsg(queueId, UUID.fromString(proto.getId()), proto.getTs(), proto.getType(), entityId, customerId, metaData, dataType, proto.getData(), ruleChainId, ruleNodeId, ctx, callback); } catch (InvalidProtocolBufferException e) { throw new IllegalStateException("Could not parse protobuf for TbMsg", e); @@ -260,12 +259,12 @@ public final class TbMsg implements Serializable { } public TbMsg copyWithRuleChainId(RuleChainId ruleChainId, UUID msgId) { - return new TbMsg(this.queueName, msgId, this.ts, this.type, this.originator, this.customerId, + return new TbMsg(this.queueId, msgId, this.ts, this.type, this.originator, this.customerId, this.metaData, this.dataType, this.data, ruleChainId, null, this.ctx, callback); } public TbMsg copyWithRuleNodeId(RuleChainId ruleChainId, RuleNodeId ruleNodeId, UUID msgId) { - return new TbMsg(this.queueName, msgId, this.ts, this.type, this.originator, this.customerId, + return new TbMsg(this.queueId, msgId, this.ts, this.type, this.originator, this.customerId, this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ctx, callback); } @@ -278,10 +277,6 @@ public final class TbMsg implements Serializable { } } - public String getQueueName() { - return queueName != null ? queueName : ServiceQueue.MAIN; - } - public void pushToStack(RuleChainId ruleChainId, RuleNodeId ruleNodeId) { ctx.push(ruleChainId, ruleNodeId); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index 58ef13ac6d..ce7c93741a 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -22,6 +22,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.queue.ServiceQueue; import org.thingsboard.server.common.msg.queue.ServiceQueueKey; @@ -62,6 +63,7 @@ public class HashPartitionService implements PartitionService { private final TbServiceInfoProvider serviceInfoProvider; private final TenantRoutingInfoService tenantRoutingInfoService; private final QueueRoutingInfoService queueRoutingInfoService; + private final ConcurrentMap queueNames = new ConcurrentHashMap<>(); private final ConcurrentMap> partitionTopicsMap = new ConcurrentHashMap<>(); private final ConcurrentMap> partitionSizesMap = new ConcurrentHashMap<>(); private final ConcurrentMap tenantRoutingInfoMap = new ConcurrentHashMap<>(); @@ -91,27 +93,21 @@ public class HashPartitionService implements PartitionService { } private void partitionsInit() { - addPartitionSizeToMap(TenantId.SYS_TENANT_ID, new ServiceQueue(ServiceType.TB_CORE, "Main"), corePartitions); - addPartitionTopicToMap(TenantId.SYS_TENANT_ID, new ServiceQueue(ServiceType.TB_CORE, "Main"), coreTopic); + addPartitionSizeToMap(TenantId.SYS_TENANT_ID, new ServiceQueue(ServiceType.TB_CORE), corePartitions); + addPartitionTopicToMap(TenantId.SYS_TENANT_ID, new ServiceQueue(ServiceType.TB_CORE), coreTopic); List queueRoutingInfoList; String serviceType = serviceInfoProvider.getServiceType(); - if ("tb-rule-engine".equals(serviceType)) { - queueRoutingInfoList = queueRoutingInfoService.getQueuesRoutingInfo(serviceInfoProvider.getIsolatedTenant().orElse(TenantId.SYS_TENANT_ID)); - } else if ("monolith".equals(serviceType)) { - queueRoutingInfoList = queueRoutingInfoService.getAllQueuesRoutingInfo(); - } else if ("tb-core".equals(serviceType)) { - queueRoutingInfoList = queueRoutingInfoService.getMainQueuesRoutingInfo(); - } else { + if ("tb-transport".equals(serviceType)) { //If transport started earlier than tb-core int getQueuesRetries = 10; while (true) { if (getQueuesRetries > 0) { log.info("Try to get queue routing info."); try { - queueRoutingInfoList = queueRoutingInfoService.getMainQueuesRoutingInfo(); + queueRoutingInfoList = queueRoutingInfoService.getAllQueuesRoutingInfo(); break; } catch (Exception e) { log.info("Failed to get queues routing info!"); @@ -126,11 +122,14 @@ public class HashPartitionService implements PartitionService { throw new RuntimeException("Failed to await queues routing info!"); } } + } else { + queueRoutingInfoList = queueRoutingInfoService.getAllQueuesRoutingInfo(); } queueRoutingInfoList.forEach(queue -> { addPartitionTopicToMap(queue.getTenantId(), new ServiceQueue(ServiceType.TB_RULE_ENGINE, queue.getQueueName()), queue.getQueueTopic()); addPartitionSizeToMap(queue.getTenantId(), new ServiceQueue(ServiceType.TB_RULE_ENGINE, queue.getQueueName()), queue.getPartitions()); + queueNames.put(queue.getQueueId(), queue.getQueueName()); }); } @@ -139,6 +138,8 @@ public class HashPartitionService implements PartitionService { TenantId tenantId = new TenantId(new UUID(queueUpdateMsg.getTenantIdMSB(), queueUpdateMsg.getTenantIdLSB())); addPartitionTopicToMap(tenantId, new ServiceQueue(ServiceType.TB_RULE_ENGINE, queueUpdateMsg.getQueueName()), queueUpdateMsg.getQueueTopic()); addPartitionSizeToMap(tenantId, new ServiceQueue(ServiceType.TB_RULE_ENGINE, queueUpdateMsg.getQueueName()), queueUpdateMsg.getPartitions()); + queueNames.putIfAbsent(new QueueId(new UUID(queueUpdateMsg.getQueueIdMSB(), queueUpdateMsg.getQueueIdLSB())), queueUpdateMsg.getQueueName()); + tpiCache.clear(); } @Override @@ -148,6 +149,8 @@ public class HashPartitionService implements PartitionService { partitionTopicsMap.get(tenantId).remove(serviceQueue); partitionSizesMap.get(tenantId).remove(serviceQueue); myPartitions.remove(new ServiceQueueKey(serviceQueue, tenantId)); + queueNames.remove(new QueueId(new UUID(queueDeleteMsg.getQueueIdMSB(), queueDeleteMsg.getQueueIdLSB()))); + tpiCache.clear(); } private void addPartitionSizeToMap(TenantId tenantId, ServiceQueue serviceQueue, int partitions) { @@ -164,13 +167,14 @@ public class HashPartitionService implements PartitionService { } @Override - public TopicPartitionInfo resolve(ServiceType serviceType, String queueName, TenantId tenantId, EntityId entityId) { - ServiceQueue serviceQueue = new ServiceQueue(serviceType, queueName); - ConcurrentMap queues = partitionTopicsMap.get(getSystemIsolatedTenantId(serviceType, tenantId)); - - if (!queues.containsKey(serviceQueue)) { - serviceQueue = new ServiceQueue(serviceType); + public TopicPartitionInfo resolve(ServiceType serviceType, QueueId queueId, TenantId tenantId, EntityId entityId) { + String queueName; + if (queueId == null) { + queueName = ServiceQueue.MAIN; + } else { + queueName = queueNames.get(queueId); } + ServiceQueue serviceQueue = new ServiceQueue(serviceType, queueName); return resolve(serviceQueue, tenantId, entityId); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java index 84065ec283..6fbd3d32e2 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.queue.discovery; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; @@ -33,7 +34,7 @@ public interface PartitionService { TopicPartitionInfo resolve(ServiceType serviceType, TenantId tenantId, EntityId entityId); - TopicPartitionInfo resolve(ServiceType serviceType, String queueName, TenantId tenantId, EntityId entityId); + TopicPartitionInfo resolve(ServiceType serviceType, QueueId queueId, TenantId tenantId, EntityId entityId); /** * Received from the Discovery service when network topology is changed. diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueRoutingInfo.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueRoutingInfo.java index 8afecc4413..111ca5626a 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueRoutingInfo.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueRoutingInfo.java @@ -16,6 +16,7 @@ package org.thingsboard.server.queue.discovery; import lombok.Data; +import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.gen.transport.TransportProtos.GetQueueRoutingInfoResponseMsg; @@ -27,19 +28,14 @@ import java.util.UUID; public class QueueRoutingInfo { private final TenantId tenantId; + private final QueueId queueId; private final String queueName; private final String queueTopic; private final int partitions; - public QueueRoutingInfo(TenantId tenantId, String queueName, String queueTopic, int partitions) { - this.tenantId = tenantId; - this.queueName = queueName; - this.queueTopic = queueTopic; - this.partitions = partitions; - } - public QueueRoutingInfo(Queue queue) { this.tenantId = queue.getTenantId(); + this.queueId = queue.getId(); this.queueName = queue.getName(); this.queueTopic = queue.getTopic(); this.partitions = queue.getPartitions(); @@ -47,6 +43,7 @@ public class QueueRoutingInfo { public QueueRoutingInfo(GetQueueRoutingInfoResponseMsg routingInfo) { this.tenantId = new TenantId(new UUID(routingInfo.getTenantIdMSB(), routingInfo.getTenantIdLSB())); + this.queueId = new QueueId(new UUID(routingInfo.getQueueIdMSB(), routingInfo.getQueueIdLSB())); this.queueName = routingInfo.getQueueName(); this.queueTopic = routingInfo.getQueueTopic(); this.partitions = routingInfo.getPartitions(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueRoutingInfoService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueRoutingInfoService.java index 366452d9e8..98cd5cccba 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueRoutingInfoService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueRoutingInfoService.java @@ -15,15 +15,10 @@ */ package org.thingsboard.server.queue.discovery; -import org.thingsboard.server.common.data.id.TenantId; - import java.util.List; public interface QueueRoutingInfoService { List getAllQueuesRoutingInfo(); - List getMainQueuesRoutingInfo(); - - List getQueuesRoutingInfo(TenantId tenantId); } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java index 53c1997f5c..f1d3f7e26c 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java @@ -68,10 +68,6 @@ public interface TransportService { GetEntityProfileResponseMsg getEntityProfile(GetEntityProfileRequestMsg msg); - List getQueueRoutingInfo(TransportProtos.GetAllMainQueueRoutingInfoRequestMsg msg); - - List getQueueRoutingInfo(TransportProtos.GetTenantQueueRoutingInfoRequestMsg msg); - List getQueueRoutingInfo(TransportProtos.GetAllQueueRoutingInfoRequestMsg msg); GetResourceResponseMsg getResource(GetResourceRequestMsg msg); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index f06a7f76ea..70a9ae1225 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -44,13 +44,13 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.rpc.RpcStatus; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.common.msg.queue.ServiceQueue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.session.SessionMsgType; @@ -322,30 +322,6 @@ public class DefaultTransportService implements TransportService { } } - @Override - public List getQueueRoutingInfo(TransportProtos.GetAllMainQueueRoutingInfoRequestMsg msg) { - TbProtoQueueMsg protoMsg = - new TbProtoQueueMsg<>(UUID.randomUUID(), TransportProtos.TransportApiRequestMsg.newBuilder().setGetAllMainQueueRoutingInfoRequestMsg(msg).build()); - try { - TbProtoQueueMsg response = transportApiRequestTemplate.send(protoMsg).get(); - return response.getValue().getGetQueueRoutingInfoResponseMsgsList(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } - } - - @Override - public List getQueueRoutingInfo(TransportProtos.GetTenantQueueRoutingInfoRequestMsg msg) { - TbProtoQueueMsg protoMsg = - new TbProtoQueueMsg<>(UUID.randomUUID(), TransportProtos.TransportApiRequestMsg.newBuilder().setGetTenantQueueRoutingInfoRequestMsg(msg).build()); - try { - TbProtoQueueMsg response = transportApiRequestTemplate.send(protoMsg).get(); - return response.getValue().getGetQueueRoutingInfoResponseMsgsList(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } - } - @Override public TransportProtos.GetResourceResponseMsg getResource(TransportProtos.GetResourceRequestMsg msg) { TbProtoQueueMsg protoMsg = @@ -1112,7 +1088,7 @@ public class DefaultTransportService implements TransportService { } private void sendToRuleEngine(TenantId tenantId, TbMsg tbMsg, TbQueueCallback callback) { - TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tbMsg.getQueueName(), tenantId, tbMsg.getOriginator()); + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tbMsg.getQueueId(), tenantId, tbMsg.getOriginator()); if (log.isTraceEnabled()) { log.trace("[{}][{}] Pushing to topic {} message {}", tenantId, tbMsg.getOriginator(), tpi.getFullTopicName(), tbMsg); } @@ -1129,19 +1105,18 @@ public class DefaultTransportService implements TransportService { DeviceProfileId deviceProfileId = new DeviceProfileId(new UUID(sessionInfo.getDeviceProfileIdMSB(), sessionInfo.getDeviceProfileIdLSB())); DeviceProfile deviceProfile = deviceProfileCache.get(deviceProfileId); RuleChainId ruleChainId; - String queueName; + QueueId queueId; if (deviceProfile == null) { log.warn("[{}] Device profile is null!", deviceProfileId); ruleChainId = null; - queueName = ServiceQueue.MAIN; + queueId = null; } else { ruleChainId = deviceProfile.getDefaultRuleChainId(); - String defaultQueueName = deviceProfile.getDefaultQueueName(); - queueName = defaultQueueName != null ? defaultQueueName : ServiceQueue.MAIN; + queueId = deviceProfile.getDefaultQueueId(); } - TbMsg tbMsg = TbMsg.newMsg(queueName, sessionMsgType.name(), deviceId, customerId, metaData, gson.toJson(json), ruleChainId, null); + TbMsg tbMsg = TbMsg.newMsg(queueId, sessionMsgType.name(), deviceId, customerId, metaData, gson.toJson(json), ruleChainId, null); sendToRuleEngine(tenantId, tbMsg, callback); } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportQueueRoutingInfoService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportQueueRoutingInfoService.java index 537a86b1d6..ae1b6934c3 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportQueueRoutingInfoService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportQueueRoutingInfoService.java @@ -20,11 +20,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.gen.transport.TransportProtos.GetAllQueueRoutingInfoRequestMsg; -import org.thingsboard.server.gen.transport.TransportProtos.GetAllMainQueueRoutingInfoRequestMsg; -import org.thingsboard.server.gen.transport.TransportProtos.GetTenantQueueRoutingInfoRequestMsg; import org.thingsboard.server.queue.discovery.QueueRoutingInfo; import org.thingsboard.server.queue.discovery.QueueRoutingInfoService; @@ -45,16 +42,4 @@ public class TransportQueueRoutingInfoService implements QueueRoutingInfoService GetAllQueueRoutingInfoRequestMsg msg = GetAllQueueRoutingInfoRequestMsg.newBuilder().build(); return transportService.getQueueRoutingInfo(msg).stream().map(QueueRoutingInfo::new).collect(Collectors.toList()); } - - @Override - public List getMainQueuesRoutingInfo() { - GetAllMainQueueRoutingInfoRequestMsg msg = GetAllMainQueueRoutingInfoRequestMsg.newBuilder().build(); - return transportService.getQueueRoutingInfo(msg).stream().map(QueueRoutingInfo::new).collect(Collectors.toList()); - } - - @Override - public List getQueuesRoutingInfo(TenantId tenantId) { - GetTenantQueueRoutingInfoRequestMsg msg = GetTenantQueueRoutingInfoRequestMsg.newBuilder().build(); - return transportService.getQueueRoutingInfo(msg).stream().map(QueueRoutingInfo::new).collect(Collectors.toList()); - } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index 7d6412cbea..7bfacb5f78 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -114,7 +114,7 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D return "[Transport Configuration] invalid " + schemaName + " provided!"; } - @Autowired(required = false) + @Autowired private QueueService queueService; @Autowired @@ -386,13 +386,9 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D throw new DataValidationException("Another default device profile is present in scope of current tenant!"); } } - if (!StringUtils.isEmpty(deviceProfile.getDefaultQueueName()) && queueService != null) { - //TODO: Yevhen replace queue name to queue id - if (!queueService.findQueuesByTenantId(tenantId) - .stream() - .map(Queue::getName) - .collect(Collectors.toSet()) - .contains(deviceProfile.getDefaultQueueName())) { + if (deviceProfile.getDefaultQueueId() != null) { + Queue queue = queueService.findQueueById(tenantId, deviceProfile.getDefaultQueueId()); + if (queue == null) { throw new DataValidationException("Device profile is referencing to non-existent queue!"); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 73c19cab3c..c9f89ccc91 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -180,7 +180,7 @@ public class ModelConstants { public static final String DEVICE_PROFILE_IS_DEFAULT_PROPERTY = "is_default"; public static final String DEVICE_PROFILE_DEFAULT_RULE_CHAIN_ID_PROPERTY = "default_rule_chain_id"; public static final String DEVICE_PROFILE_DEFAULT_DASHBOARD_ID_PROPERTY = "default_dashboard_id"; - public static final String DEVICE_PROFILE_DEFAULT_QUEUE_NAME_PROPERTY = "default_queue_name"; + public static final String DEVICE_PROFILE_DEFAULT_QUEUE_ID_PROPERTY = "default_queue_id"; public static final String DEVICE_PROFILE_PROVISION_DEVICE_KEY = "provision_device_key"; public static final String DEVICE_PROFILE_FIRMWARE_ID_PROPERTY = "firmware_id"; public static final String DEVICE_PROFILE_SOFTWARE_ID_PROPERTY = "software_id"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java index bd214a6cbc..9dc5c9780e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.OtaPackageId; +import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.BaseSqlEntity; @@ -87,8 +88,8 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl @Column(name = ModelConstants.DEVICE_PROFILE_DEFAULT_DASHBOARD_ID_PROPERTY) private UUID defaultDashboardId; - @Column(name = ModelConstants.DEVICE_PROFILE_DEFAULT_QUEUE_NAME_PROPERTY) - private String defaultQueueName; + @Column(name = ModelConstants.DEVICE_PROFILE_DEFAULT_QUEUE_ID_PROPERTY) + private UUID defaultQueueId; @Type(type = "jsonb") @Column(name = ModelConstants.DEVICE_PROFILE_PROFILE_DATA_PROPERTY, columnDefinition = "jsonb") @@ -129,7 +130,9 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl if (deviceProfile.getDefaultDashboardId() != null) { this.defaultDashboardId = deviceProfile.getDefaultDashboardId().getId(); } - this.defaultQueueName = deviceProfile.getDefaultQueueName(); + if (deviceProfile.getDefaultQueueId() != null) { + this.defaultQueueId = deviceProfile.getDefaultQueueId().getId(); + } this.provisionDeviceKey = deviceProfile.getProvisionDeviceKey(); if (deviceProfile.getFirmwareId() != null) { this.firmwareId = deviceProfile.getFirmwareId().getId(); @@ -174,7 +177,9 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl if (defaultDashboardId != null) { deviceProfile.setDefaultDashboardId(new DashboardId(defaultDashboardId)); } - deviceProfile.setDefaultQueueName(defaultQueueName); + if (defaultQueueId != null) { + deviceProfile.setDefaultQueueId(new QueueId(defaultQueueId)); + } deviceProfile.setProvisionDeviceKey(provisionDeviceKey); if (firmwareId != null) { diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 54c3cbfa93..b85171efee 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -216,6 +216,20 @@ CREATE TABLE IF NOT EXISTS ota_package ( -- CONSTRAINT fk_device_profile_firmware FOREIGN KEY (device_profile_id) REFERENCES device_profile(id) ON DELETE CASCADE ); +CREATE TABLE IF NOT EXISTS queue( + id uuid NOT NULL CONSTRAINT queue_pkey PRIMARY KEY, + created_time bigint NOT NULL, + tenant_id uuid, + name varchar(255), + topic varchar(255), + poll_interval int, + partitions int, + consumer_per_partition boolean, + pack_processing_timeout bigint, + submit_strategy varchar(255), + processing_strategy varchar(255) +); + CREATE TABLE IF NOT EXISTS device_profile ( id uuid NOT NULL CONSTRAINT device_profile_pkey PRIMARY KEY, created_time bigint NOT NULL, @@ -233,12 +247,13 @@ CREATE TABLE IF NOT EXISTS device_profile ( software_id uuid, default_rule_chain_id uuid, default_dashboard_id uuid, - default_queue_name varchar(255), + default_queue_id uuid, provision_device_key varchar, CONSTRAINT device_profile_name_unq_key UNIQUE (tenant_id, name), CONSTRAINT device_provision_key_unq_key UNIQUE (provision_device_key), CONSTRAINT fk_default_rule_chain_device_profile FOREIGN KEY (default_rule_chain_id) REFERENCES rule_chain(id), CONSTRAINT fk_default_dashboard_device_profile FOREIGN KEY (default_dashboard_id) REFERENCES dashboard(id), + CONSTRAINT fk_default_queue_device_profile FOREIGN KEY (default_queue_id) REFERENCES queue(id), CONSTRAINT fk_firmware_device_profile FOREIGN KEY (firmware_id) REFERENCES ota_package(id), CONSTRAINT fk_software_device_profile FOREIGN KEY (software_id) REFERENCES ota_package(id) ); @@ -634,20 +649,6 @@ CREATE TABLE IF NOT EXISTS rpc ( status varchar(255) NOT NULL ); -CREATE TABLE IF NOT EXISTS queue( - id uuid NOT NULL CONSTRAINT queue_pkey PRIMARY KEY, - created_time bigint NOT NULL, - tenant_id uuid, - name varchar(255), - topic varchar(255), - poll_interval int, - partitions int, - consumer_per_partition boolean, - pack_processing_timeout bigint, - submit_strategy varchar(255), - processing_strategy varchar(255) -); - CREATE OR REPLACE PROCEDURE cleanup_events_by_ttl(IN ttl bigint, IN debug_ttl bigint, INOUT deleted bigint) LANGUAGE plpgsql AS $$ diff --git a/dao/src/test/resources/sql/system-data.sql b/dao/src/test/resources/sql/system-data.sql index 339d6e94ba..1d0d29aec5 100644 --- a/dao/src/test/resources/sql/system-data.sql +++ b/dao/src/test/resources/sql/system-data.sql @@ -43,3 +43,9 @@ VALUES ( '6eaaefa6-4612-11e7-a919-92ebcb67fe33', 1592576748000, 'mail', '{ "username": "", "password": "" }' ); + +INSERT INTO queue ( id, created_time, tenant_id, name, topic, poll_interval, partitions, consumer_per_partition, pack_processing_timeout, submit_strategy, processing_strategy ) +VALUES ( '6eaaefa6-4612-11e7-a919-92ebcb67fe33', 1592576748000 ,'13814000-1dd2-11b2-8080-808080808080', 'Main' ,'tb_rule_engine.main', 25, 10, true, 2000, + '{"type": "BURST", "batchSize": 1000}', + '{"type": "SKIP_ALL_FAILURES", "retries": 3, "failurePercentage": 0.0, "pauseBetweenRetries": 3, "maxPauseBetweenRetries": 3}' +); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index ee2a93d7fb..ce66b4fbce 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; @@ -50,6 +51,7 @@ import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.nosql.CassandraStatementTask; import org.thingsboard.server.dao.nosql.TbResultSetFuture; import org.thingsboard.server.dao.ota.OtaPackageService; +import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; @@ -145,7 +147,7 @@ public interface TbContext { * * @param msg - message */ - void enqueue(TbMsg msg, String queueName, Runnable onSuccess, Consumer onFailure); + void enqueue(TbMsg msg, QueueId queueId, Runnable onSuccess, Consumer onFailure); void enqueueForTellFailure(TbMsg msg, String failureMessage); @@ -157,15 +159,15 @@ public interface TbContext { void enqueueForTellNext(TbMsg msg, Set relationTypes, Runnable onSuccess, Consumer onFailure); - void enqueueForTellNext(TbMsg msg, String queueName, String relationType, Runnable onSuccess, Consumer onFailure); + void enqueueForTellNext(TbMsg msg, QueueId queueId, String relationType, Runnable onSuccess, Consumer onFailure); - void enqueueForTellNext(TbMsg msg, String queueName, Set relationTypes, Runnable onSuccess, Consumer onFailure); + void enqueueForTellNext(TbMsg msg, QueueId queueId, Set relationTypes, Runnable onSuccess, Consumer onFailure); void ack(TbMsg tbMsg); - TbMsg newMsg(String queueName, String type, EntityId originator, TbMsgMetaData metaData, String data); + TbMsg newMsg(QueueId queueId, String type, EntityId originator, TbMsgMetaData metaData, String data); - TbMsg newMsg(String queueName, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data); + TbMsg newMsg(QueueId queueId, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data); TbMsg transformMsg(TbMsg origMsg, String type, EntityId originator, TbMsgMetaData metaData, String data); @@ -236,6 +238,8 @@ public interface TbContext { EdgeEventService getEdgeEventService(); + QueueService getQueueService(); + ListeningExecutor getMailExecutor(); ListeningExecutor getSmsExecutor(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java index fe2d5a4cd4..b27caf6838 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java @@ -136,7 +136,7 @@ public class TbCopyAttributesToEntityViewNode implements TbNode { } private void transformAndTellNext(TbContext ctx, TbMsg msg, EntityView entityView) { - ctx.enqueueForTellNext(ctx.newMsg(msg.getQueueName(), msg.getType(), entityView.getId(), msg.getCustomerId(), msg.getMetaData(), msg.getData()), SUCCESS); + ctx.enqueueForTellNext(ctx.newMsg(msg.getQueueId(), msg.getType(), entityView.getId(), msg.getCustomerId(), msg.getMetaData(), msg.getData()), SUCCESS); } private boolean attributeContainsInEntityView(String scope, String attrKey, EntityView entityView) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java index 97f9f8565c..4a29787762 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java @@ -78,7 +78,7 @@ public class TbMsgCountNode implements TbNode { TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("delta", Long.toString(System.currentTimeMillis() - lastScheduledTs + delay)); - TbMsg tbMsg = TbMsg.newMsg(msg.getQueueName(), SessionMsgType.POST_TELEMETRY_REQUEST.name(), ctx.getTenantId(), msg.getCustomerId(), metaData, gson.toJson(telemetryJson)); + TbMsg tbMsg = TbMsg.newMsg(msg.getQueueId(), SessionMsgType.POST_TELEMETRY_REQUEST.name(), ctx.getTenantId(), msg.getCustomerId(), metaData, gson.toJson(telemetryJson)); ctx.enqueueForTellNext(tbMsg, SUCCESS); scheduleTickMsg(ctx, tbMsg); } else { @@ -94,7 +94,7 @@ public class TbMsgCountNode implements TbNode { } lastScheduledTs = lastScheduledTs + delay; long curDelay = Math.max(0L, (lastScheduledTs - curTs)); - TbMsg tickMsg = ctx.newMsg(ServiceQueue.MAIN, TB_MSG_COUNT_NODE_MSG, ctx.getSelfId(), msg != null ? msg.getCustomerId() : null, new TbMsgMetaData(), ""); + TbMsg tickMsg = ctx.newMsg(null, TB_MSG_COUNT_NODE_MSG, ctx.getSelfId(), msg != null ? msg.getCustomerId() : null, new TbMsgMetaData(), ""); nextTickId = tickMsg.getId(); ctx.tellSelf(tickMsg, curDelay); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java index 5ffa9ba5b5..f8b4d64b42 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java @@ -135,7 +135,7 @@ public class TbMsgGeneratorNode implements TbNode { } lastScheduledTs = lastScheduledTs + delay; long curDelay = Math.max(0L, (lastScheduledTs - curTs)); - TbMsg tickMsg = ctx.newMsg(ServiceQueue.MAIN, TB_MSG_GENERATOR_NODE_MSG, ctx.getSelfId(), new TbMsgMetaData(), ""); + TbMsg tickMsg = ctx.newMsg(null, TB_MSG_GENERATOR_NODE_MSG, ctx.getSelfId(), new TbMsgMetaData(), ""); nextTickId = tickMsg.getId(); ctx.tellSelf(tickMsg, curDelay); } @@ -143,14 +143,14 @@ public class TbMsgGeneratorNode implements TbNode { private ListenableFuture generate(TbContext ctx, TbMsg msg) { log.trace("generate, config {}", config); if (prevMsg == null) { - prevMsg = ctx.newMsg(ServiceQueue.MAIN, "", originatorId, msg.getCustomerId(), new TbMsgMetaData(), "{}"); + prevMsg = ctx.newMsg(null, "", originatorId, msg.getCustomerId(), new TbMsgMetaData(), "{}"); } if (initialized.get()) { ctx.logJsEvalRequest(); return Futures.transformAsync(jsEngine.executeGenerateAsync(prevMsg), generated -> { log.trace("generate process response, generated {}, config {}", generated, config); ctx.logJsEvalResponse(); - prevMsg = ctx.newMsg(ServiceQueue.MAIN, generated.getType(), originatorId, msg.getCustomerId(), generated.getMetaData(), generated.getData()); + prevMsg = ctx.newMsg(null, generated.getType(), originatorId, msg.getCustomerId(), generated.getMetaData(), generated.getData()); return Futures.immediateFuture(prevMsg); }, MoreExecutors.directExecutor()); //usually it runs on js-executor-remote-callback thread pool } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java index 57d087e42a..464acadb94 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java @@ -73,7 +73,7 @@ public class TbMsgDelayNode implements TbNode { } else { if (pendingMsgs.size() < config.getMaxPendingMsgs()) { pendingMsgs.put(msg.getId(), msg); - TbMsg tickMsg = ctx.newMsg(ServiceQueue.MAIN, TB_MSG_DELAY_NODE_MSG, ctx.getSelfId(), msg.getCustomerId(), new TbMsgMetaData(), msg.getId().toString()); + TbMsg tickMsg = ctx.newMsg(null, TB_MSG_DELAY_NODE_MSG, ctx.getSelfId(), msg.getCustomerId(), new TbMsgMetaData(), msg.getId().toString()); ctx.tellSelf(tickMsg, getDelay(msg)); ctx.ack(msg); } else { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java index 286ffea9d2..9bd51dfdf4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java @@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.flow; import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; -import org.thingsboard.rule.engine.api.ScriptEngine; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -25,10 +24,9 @@ import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbRelationTypes; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.TbMsg; -import static org.thingsboard.common.util.DonAsynchron.withCallback; - @Slf4j @RuleNode( type = ComponentType.FLOW, @@ -46,11 +44,17 @@ public class TbCheckpointNode implements TbNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { this.config = TbNodeUtils.convert(configuration, TbCheckpointNodeConfiguration.class); + if (config.getQueueId() == null) { + Queue foundQueue = ctx.getQueueService().findQueueByTenantIdAndName(ctx.getTenantId(), config.getQueueName()); + if (foundQueue != null) { + config.setQueueId(foundQueue.getId()); + } + } } @Override public void onMsg(TbContext ctx, TbMsg msg) { - ctx.enqueueForTellNext(msg, config.getQueueName(), TbRelationTypes.SUCCESS, () -> ctx.ack(msg), error -> ctx.tellFailure(msg, error)); + ctx.enqueueForTellNext(msg, config.getQueueId(), TbRelationTypes.SUCCESS, () -> ctx.ack(msg), error -> ctx.tellFailure(msg, error)); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNodeConfiguration.java index 9957e6b07c..b2da9142d7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNodeConfiguration.java @@ -17,12 +17,15 @@ package org.thingsboard.rule.engine.flow; import lombok.Data; import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.server.common.data.id.QueueId; @Data public class TbCheckpointNodeConfiguration implements NodeConfiguration { private String queueName; + private QueueId queueId; + @Override public TbCheckpointNodeConfiguration defaultConfiguration() { TbCheckpointNodeConfiguration configuration = new TbCheckpointNodeConfiguration(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java index 2a46b77791..db41c0f6d5 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java @@ -36,9 +36,9 @@ import org.thingsboard.server.common.data.device.profile.AlarmConditionSpecType; import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.common.msg.queue.ServiceQueue; import org.thingsboard.server.dao.alarm.AlarmOperationResult; import java.util.ArrayList; @@ -60,7 +60,7 @@ class AlarmState { private volatile Alarm currentAlarm; private volatile boolean initialFetchDone; private volatile TbMsgMetaData lastMsgMetaData; - private volatile String lastMsgQueueName; + private volatile QueueId lastMsgQueueId; private volatile DataSnapshot dataSnapshot; private final DynamicPredicateValueCtx dynamicPredicateValueCtx; @@ -74,7 +74,7 @@ class AlarmState { public boolean process(TbContext ctx, TbMsg msg, DataSnapshot data, SnapshotUpdate update) throws ExecutionException, InterruptedException { initCurrentAlarm(ctx); lastMsgMetaData = msg.getMetaData(); - lastMsgQueueName = msg.getQueueName(); + lastMsgQueueId = msg.getQueueId(); this.dataSnapshot = data; try { return createOrClearAlarms(ctx, msg, data, update, AlarmRuleState::eval); @@ -195,7 +195,7 @@ class AlarmState { metaData.putValue(DataConstants.IS_CLEARED_ALARM, Boolean.TRUE.toString()); } setAlarmConditionMetadata(ruleState, metaData); - TbMsg newMsg = ctx.newMsg(lastMsgQueueName != null ? lastMsgQueueName : ServiceQueue.MAIN, "ALARM", + TbMsg newMsg = ctx.newMsg(lastMsgQueueId != null ? lastMsgQueueId : null, "ALARM", originator, msg != null ? msg.getCustomerId() : null, metaData, data); ctx.enqueueForTellNext(newMsg, relationType); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java index 45b45e91f9..265475c0d9 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java @@ -116,10 +116,10 @@ public class TbSendRPCRequestNode implements TbNode { ctx.getRpcService().sendRpcRequestToDevice(request, ruleEngineDeviceRpcResponse -> { if (ruleEngineDeviceRpcResponse.getError().isEmpty()) { - TbMsg next = ctx.newMsg(msg.getQueueName(), msg.getType(), msg.getOriginator(), msg.getCustomerId(), msg.getMetaData(), ruleEngineDeviceRpcResponse.getResponse().orElse("{}")); + TbMsg next = ctx.newMsg(msg.getQueueId(), msg.getType(), msg.getOriginator(), msg.getCustomerId(), msg.getMetaData(), ruleEngineDeviceRpcResponse.getResponse().orElse("{}")); ctx.enqueueForTellNext(next, TbRelationTypes.SUCCESS); } else { - TbMsg next = ctx.newMsg(msg.getQueueName(), msg.getType(), msg.getOriginator(), msg.getCustomerId(), msg.getMetaData(), wrap("error", ruleEngineDeviceRpcResponse.getError().get().name())); + TbMsg next = ctx.newMsg(msg.getQueueId(), msg.getType(), msg.getOriginator(), msg.getCustomerId(), msg.getMetaData(), wrap("error", ruleEngineDeviceRpcResponse.getError().get().name())); ctx.enqueueForTellFailure(next, ruleEngineDeviceRpcResponse.getError().get().name()); } }); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java index dcad6f1567..b00713cefa 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java @@ -188,7 +188,7 @@ public class TbDeviceProfileNodeTest { Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())).thenReturn(theMsg); + Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())).thenReturn(theMsg); ObjectNode data = mapper.createObjectNode(); data.put("temperature", 42); @@ -200,7 +200,7 @@ public class TbDeviceProfileNodeTest { verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); TbMsg theMsg2 = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), "2"); - Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())).thenReturn(theMsg2); + Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())).thenReturn(theMsg2); TbMsg msg2 = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), @@ -279,7 +279,7 @@ public class TbDeviceProfileNodeTest { .thenReturn(attrListListenableFuture); TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = mapper.createObjectNode(); @@ -366,7 +366,7 @@ public class TbDeviceProfileNodeTest { .thenReturn(attrListListenableFuture); TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = mapper.createObjectNode(); @@ -435,7 +435,7 @@ public class TbDeviceProfileNodeTest { .thenReturn(listListenableFutureWithLess); TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = mapper.createObjectNode(); @@ -529,7 +529,7 @@ public class TbDeviceProfileNodeTest { .thenReturn(listListenableFuture); TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = mapper.createObjectNode(); @@ -653,7 +653,7 @@ public class TbDeviceProfileNodeTest { .thenReturn(listNoDurationAttribute); TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = mapper.createObjectNode(); @@ -762,7 +762,7 @@ public class TbDeviceProfileNodeTest { .thenReturn(listListenableFuture); TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = mapper.createObjectNode(); @@ -878,7 +878,7 @@ public class TbDeviceProfileNodeTest { .thenReturn(listNoDurationAttribute); TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = mapper.createObjectNode(); @@ -974,7 +974,7 @@ public class TbDeviceProfileNodeTest { .thenReturn(listListenableFuture); TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = mapper.createObjectNode(); @@ -1072,7 +1072,7 @@ public class TbDeviceProfileNodeTest { .thenReturn(listListenableFuture); TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = mapper.createObjectNode(); @@ -1153,7 +1153,7 @@ public class TbDeviceProfileNodeTest { .thenReturn(optionalListenableFutureWithLess); TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = mapper.createObjectNode(); @@ -1227,7 +1227,7 @@ public class TbDeviceProfileNodeTest { .thenReturn(optionalListenableFutureWithLess); TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = mapper.createObjectNode(); @@ -1311,7 +1311,7 @@ public class TbDeviceProfileNodeTest { .thenReturn(optionalListenableFutureWithLess); TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = mapper.createObjectNode(); @@ -1397,7 +1397,7 @@ public class TbDeviceProfileNodeTest { .thenReturn(optionalListenableFutureWithLess); TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = mapper.createObjectNode(); diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index b71f5a95ef..71e2a3de95 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -83,15 +83,12 @@ import { import { alarmFields } from '@shared/models/alarm.models'; import { OtaPackageService } from '@core/http/ota-package.service'; import { EdgeService } from '@core/http/edge.service'; -import { - Edge, - EdgeEvent, - EdgeEventType, - bodyContentEdgeEventActionTypes -} from '@shared/models/edge.models'; +import { bodyContentEdgeEventActionTypes, Edge, EdgeEvent, EdgeEventType } from '@shared/models/edge.models'; import { RuleChainMetaData, RuleChainType } from '@shared/models/rule-chain.models'; import { WidgetService } from '@core/http/widget.service'; import { DeviceProfileService } from '@core/http/device-profile.service'; +import { QueueService } from "@core/http/queue.service"; +import { ServiceType } from "@shared/models/queue.models"; @Injectable({ providedIn: 'root' @@ -115,7 +112,8 @@ export class EntityService { private otaPackageService: OtaPackageService, private widgetService: WidgetService, private deviceProfileService: DeviceProfileService, - private utils: UtilsService + private utils: UtilsService, + private queueService: QueueService ) { } private getEntityObservable(entityType: EntityType, entityId: string, @@ -156,6 +154,9 @@ export class EntityService { case EntityType.OTA_PACKAGE: observable = this.otaPackageService.getOtaPackageInfo(entityId, config); break; + case EntityType.QUEUE: + observable = this.queueService.getQueueById(entityId, config); + break; } return observable; } diff --git a/ui-ngx/src/app/core/http/queue.service.ts b/ui-ngx/src/app/core/http/queue.service.ts index dc98c15d39..bf2183449d 100644 --- a/ui-ngx/src/app/core/http/queue.service.ts +++ b/ui-ngx/src/app/core/http/queue.service.ts @@ -36,8 +36,8 @@ export class QueueService { defaultHttpOptionsFromConfig(config)); } - public getQueueById(queueId: string): Observable { - return this.http.get(`/api/tenant/queues/${queueId}`); + public getQueueById(queueId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/tenant/queues/${queueId}`, defaultHttpOptionsFromConfig(config)); } public getTenantQueuesByServiceType(pageLink: PageLink, diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html index 96584a7ba8..ace76e27cd 100644 --- a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html @@ -54,10 +54,10 @@ formControlName="defaultDashboardId">
{{'device-profile.mobile-dashboard-hint' | translate}}
- - + formControlName="defaultQueueId"> + device-profile.type diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts index b5535b22cd..2d80e9a25b 100644 --- a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts @@ -50,6 +50,7 @@ import { StepperSelectionEvent } from '@angular/cdk/stepper'; import { deepTrim } from '@core/utils'; import { ServiceType } from '@shared/models/queue.models'; import { DashboardId } from '@shared/models/id/dashboard-id'; +import { QueueId } from "@shared/models/id/queue-id"; export interface AddDeviceProfileDialogData { deviceProfileName: string; @@ -110,7 +111,7 @@ export class AddDeviceProfileDialogComponent extends image: [null, []], defaultRuleChainId: [null, []], defaultDashboardId: [null, []], - defaultQueueName: ['', []], + defaultQueueId: [null, []], description: ['', []] } ); @@ -187,7 +188,7 @@ export class AddDeviceProfileDialogComponent extends name: this.deviceProfileDetailsFormGroup.get('name').value, type: this.deviceProfileDetailsFormGroup.get('type').value, image: this.deviceProfileDetailsFormGroup.get('image').value, - defaultQueueName: this.deviceProfileDetailsFormGroup.get('defaultQueueName').value, + // defaultQueueId: this.deviceProfileDetailsFormGroup.get('defaultQueueId').value, transportType: this.transportConfigFormGroup.get('transportType').value, provisionType: deviceProvisionConfiguration.type, provisionDeviceKey, @@ -205,6 +206,9 @@ export class AddDeviceProfileDialogComponent extends if (this.deviceProfileDetailsFormGroup.get('defaultDashboardId').value) { deviceProfile.defaultDashboardId = new DashboardId(this.deviceProfileDetailsFormGroup.get('defaultDashboardId').value); } + if (this.deviceProfileDetailsFormGroup.get('defaultQueueId').value) { + deviceProfile.defaultQueueId = new QueueId(this.deviceProfileDetailsFormGroup.get('defaultQueueId').value); + } this.deviceProfileService.saveDeviceProfile(deepTrim(deviceProfile)).subscribe( (savedDeviceProfile) => { this.dialogRef.close(savedDeviceProfile); diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html index 565eb5b160..f5b9c15a0f 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html @@ -73,10 +73,10 @@ formControlName="defaultDashboardId">
{{'device-profile.mobile-dashboard-hint' | translate}}
- - + formControlName="defaultQueueId"> + { }), defaultRuleChainId: [entity && entity.defaultRuleChainId ? entity.defaultRuleChainId.id : null, []], defaultDashboardId: [entity && entity.defaultDashboardId ? entity.defaultDashboardId.id : null, []], - defaultQueueName: [entity ? entity.defaultQueueName : '', []], + defaultQueueId: [entity && entity.defaultQueueId ? entity.defaultQueueId.id : null, []], firmwareId: [entity ? entity.firmwareId : null], softwareId: [entity ? entity.softwareId : null], description: [entity ? entity.description : '', []], @@ -197,7 +198,7 @@ export class DeviceProfileComponent extends EntityComponent { }}, {emitEvent: false}); this.entityForm.patchValue({defaultRuleChainId: entity.defaultRuleChainId ? entity.defaultRuleChainId.id : null}, {emitEvent: false}); this.entityForm.patchValue({defaultDashboardId: entity.defaultDashboardId ? entity.defaultDashboardId.id : null}, {emitEvent: false}); - this.entityForm.patchValue({defaultQueueName: entity.defaultQueueName}, {emitEvent: false}); + this.entityForm.patchValue({defaultQueueId: entity.defaultQueueId ? entity.defaultQueueId.id: null}, {emitEvent: false}); this.entityForm.patchValue({firmwareId: entity.firmwareId}, {emitEvent: false}); this.entityForm.patchValue({softwareId: entity.softwareId}, {emitEvent: false}); this.entityForm.patchValue({description: entity.description}, {emitEvent: false}); @@ -210,6 +211,9 @@ export class DeviceProfileComponent extends EntityComponent { if (formValue.defaultDashboardId) { formValue.defaultDashboardId = new DashboardId(formValue.defaultDashboardId); } + if (formValue.defaultQueueId) { + formValue.defaultQueueId = new QueueId(formValue.defaultQueueId); + } const deviceProvisionConfiguration: DeviceProvisionConfiguration = formValue.profileData.provisionConfiguration; formValue.provisionType = deviceProvisionConfiguration.type; formValue.provisionDeviceKey = deviceProvisionConfiguration.provisionDeviceKey; diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html index 6b00667509..9cd7460c85 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html @@ -92,11 +92,11 @@
- - + formControlName="defaultQueueId"> +
diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss index 31594ada95..ffeaad5acd 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss @@ -64,7 +64,7 @@ } } - tb-device-profile-autocomplete, tb-queue-type-list{ + tb-device-profile-autocomplete, tb-queue-autocomplete{ .mat-form-field-wrapper{ width: 180px !important; } diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts index 694deabd96..1f1a5266ba 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts @@ -49,6 +49,7 @@ import { MediaBreakpoints } from '@shared/models/constants'; import { RuleChainId } from '@shared/models/id/rule-chain-id'; import { ServiceType } from '@shared/models/queue.models'; import { deepTrim } from '@core/utils'; +import { QueueId } from "@shared/models/id/queue-id"; @Component({ selector: 'tb-device-wizard', @@ -113,7 +114,7 @@ export class DeviceWizardDialogComponent extends deviceProfileId: [null, Validators.required], newDeviceProfileTitle: [{value: null, disabled: true}], defaultRuleChainId: [{value: null, disabled: true}], - defaultQueueName: [{value: null, disabled: true}], + defaultQueueId: [{value: null, disabled: true}], description: [''] } ); @@ -126,7 +127,7 @@ export class DeviceWizardDialogComponent extends this.deviceWizardFormGroup.get('newDeviceProfileTitle').setValidators(null); this.deviceWizardFormGroup.get('newDeviceProfileTitle').disable(); this.deviceWizardFormGroup.get('defaultRuleChainId').disable(); - this.deviceWizardFormGroup.get('defaultQueueName').disable(); + this.deviceWizardFormGroup.get('defaultQueueId').disable(); this.deviceWizardFormGroup.updateValueAndValidity(); this.createProfile = false; } else { @@ -135,7 +136,7 @@ export class DeviceWizardDialogComponent extends this.deviceWizardFormGroup.get('newDeviceProfileTitle').setValidators([Validators.required]); this.deviceWizardFormGroup.get('newDeviceProfileTitle').enable(); this.deviceWizardFormGroup.get('defaultRuleChainId').enable(); - this.deviceWizardFormGroup.get('defaultQueueName').enable(); + this.deviceWizardFormGroup.get('defaultQueueId').enable(); this.deviceWizardFormGroup.updateValueAndValidity(); this.createProfile = true; @@ -297,7 +298,6 @@ export class DeviceWizardDialogComponent extends const deviceProfile: DeviceProfile = { name: this.deviceWizardFormGroup.get('newDeviceProfileTitle').value, type: DeviceProfileType.DEFAULT, - defaultQueueName: this.deviceWizardFormGroup.get('defaultQueueName').value, transportType: this.transportConfigFormGroup.get('transportType').value, provisionType: deviceProvisionConfiguration.type, provisionDeviceKey, @@ -311,6 +311,9 @@ export class DeviceWizardDialogComponent extends if (this.deviceWizardFormGroup.get('defaultRuleChainId').value) { deviceProfile.defaultRuleChainId = new RuleChainId(this.deviceWizardFormGroup.get('defaultRuleChainId').value); } + if (this.deviceWizardFormGroup.get('defaultQueueId').value) { + deviceProfile.defaultQueueId = new QueueId(this.deviceWizardFormGroup.get('defaultQueueId').value); + } return this.deviceProfileService.saveDeviceProfile(deepTrim(deviceProfile)).pipe( tap((profile) => { this.currentDeviceProfileTransportType = profile.transportType; diff --git a/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.html b/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.html new file mode 100644 index 0000000000..db9e899f1b --- /dev/null +++ b/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.html @@ -0,0 +1,54 @@ + + + + + + + + + +
+
+ device-profile.no-device-profiles-found +
+ + + {{ translate.get('queue.no-queues-matching', + {entity: truncate.transform(searchText, true, 6, '...')}) | async }} + + +
+
+
+ + {{ 'queue.queue-required' | translate }} + +
diff --git a/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.ts b/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.ts new file mode 100644 index 0000000000..28f40425ea --- /dev/null +++ b/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.ts @@ -0,0 +1,215 @@ +/// +/// Copyright © 2016-2022 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 { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { catchError, debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; +import { BaseData } from '@shared/models/base-data'; +import { EntityService } from '@core/http/entity.service'; +import { TruncatePipe } from '@shared/pipe/truncate.pipe'; +import {QueueInfo, ServiceType} from "@shared/models/queue.models"; +import { QueueService } from "@core/http/queue.service"; +import { PageLink } from "@shared/models/page/page-link"; +import { Direction } from "@shared/models/page/sort-order"; +import { emptyPageData } from "@shared/models/page/page-data"; + +@Component({ + selector: 'tb-queue-autocomplete', + templateUrl: './queue-autocomplete.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => QueueAutocompleteComponent), + multi: true + }] +}) +export class QueueAutocompleteComponent implements ControlValueAccessor, OnInit { + + selectQueueFormGroup: FormGroup; + + modelValue: string | null; + + @Input() + labelText: string; + + @Input() + requiredText: string; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + queueType: ServiceType; + + @Input() + disabled: boolean; + + @ViewChild('queueInput', {static: true}) queueInput: ElementRef; + + filteredQueues: Observable>>; + + searchText = ''; + + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + public translate: TranslateService, + public truncate: TruncatePipe, + private entityService: EntityService, + private queueService: QueueService, + private fb: FormBuilder) { + this.selectQueueFormGroup = this.fb.group({ + queueId: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.filteredQueues = this.selectQueueFormGroup.get('queueId').valueChanges + .pipe( + debounceTime(150), + tap(value => { + let modelValue; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = value.id.id; + } + this.updateView(modelValue); + if (value === null) { + this.clear(); + } + }), + map(value => value ? (typeof value === 'string' ? value : value.name) : ''), + distinctUntilChanged(), + switchMap(name => this.fetchQueue(name) ), + share() + ); + } + + ngAfterViewInit(): void {} + + getCurrentEntity(): BaseData | null { + const currentRuleChain = this.selectQueueFormGroup.get('queueId').value; + if (currentRuleChain && typeof currentRuleChain !== 'string') { + return currentRuleChain as BaseData; + } else { + return null; + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.selectQueueFormGroup.disable({emitEvent: false}); + } else { + this.selectQueueFormGroup.enable({emitEvent: false}); + } + } + + textIsNotEmpty(text: string): boolean { + return (text && text.length > 0); + } + + writeValue(value: string | null): void { + this.searchText = ''; + if (value != null) { + const targetEntityType = EntityType.QUEUE; + this.entityService.getEntity(targetEntityType, value, {ignoreLoading: true, ignoreErrors: true}).subscribe( + (entity) => { + this.modelValue = entity.id.id; + this.selectQueueFormGroup.get('queueId').patchValue(entity, {emitEvent: false}); + }, + () => { + this.modelValue = null; + this.selectQueueFormGroup.get('queueId').patchValue('', {emitEvent: false}); + if (value !== null) { + this.propagateChange(this.modelValue); + } + } + ); + } else { + this.modelValue = null; + this.selectQueueFormGroup.get('queueId').patchValue('', {emitEvent: false}); + } + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.selectQueueFormGroup.get('queueId').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + reset() { + this.selectQueueFormGroup.get('queueId').patchValue('', {emitEvent: false}); + } + + updateView(value: string | null) { + if (this.modelValue !== value) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + displayQueueFn(queue?: BaseData): string | undefined { + return queue ? queue.name : undefined; + } + + fetchQueue(searchText?: string): Observable> { + this.searchText = searchText; + const pageLink = new PageLink(10, 0, searchText, { + property: 'name', + direction: Direction.ASC + }); + return this.queueService.getTenantQueuesByServiceType(pageLink, this.queueType, {ignoreLoading: true}).pipe( + catchError(() => of(emptyPageData())), + map(pageData => { + return pageData.data; + }) + ); + } + + clear() { + this.selectQueueFormGroup.get('queueId').patchValue('', {emitEvent: true}); + setTimeout(() => { + this.queueInput.nativeElement.blur(); + this.queueInput.nativeElement.focus(); + }, 0); + } +} diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index fc47d4c443..c90af9df6a 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -29,6 +29,7 @@ import * as _moment from 'moment'; import { AbstractControl, ValidationErrors } from '@angular/forms'; import { OtaPackageId } from '@shared/models/id/ota-package-id'; import { DashboardId } from '@shared/models/id/dashboard-id'; +import { QueueId } from "@shared/models/id/queue-id"; import { DataType } from '@shared/models/constants'; import { getDefaultProfileClientLwM2mSettingsConfig, @@ -569,7 +570,7 @@ export interface DeviceProfile extends BaseData { provisionDeviceKey?: string; defaultRuleChainId?: RuleChainId; defaultDashboardId?: DashboardId; - defaultQueueName?: string; + defaultQueueId?: QueueId; firmwareId?: OtaPackageId; softwareId?: OtaPackageId; profileData: DeviceProfileData; diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index a356fa5921..7a2b0fb28b 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -137,6 +137,7 @@ import { HistorySelectorComponent } from '@shared/components/time/history-select import { EntityGatewaySelectComponent } from '@shared/components/entity/entity-gateway-select.component'; import { DndModule } from 'ngx-drag-drop'; import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list.component'; +import { QueueAutocompleteComponent } from '@shared/components/queue/queue-autocomplete.component'; import { ContactComponent } from '@shared/components/contact.component'; import { TimezoneSelectComponent } from '@shared/components/time/timezone-select.component'; import { FileSizePipe } from '@shared/pipe/file-size.pipe'; @@ -232,6 +233,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) EntityListSelectComponent, EntityTypeListComponent, QueueTypeListComponent, + QueueAutocompleteComponent, RelationTypeAutocompleteComponent, SocialSharePanelComponent, JsonObjectEditComponent, @@ -380,6 +382,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) EntityListSelectComponent, EntityTypeListComponent, QueueTypeListComponent, + QueueAutocompleteComponent, RelationTypeAutocompleteComponent, SocialSharePanelComponent, JsonObjectEditComponent, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 1d09558170..910632d072 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2677,7 +2677,7 @@ "browser-time": "Browser Time" }, "queue": { - "queue-name": "Queue Name", + "queue-name": "Queue", "no-queues-matching": "No queues matching '{{queue}}' were found.", "select-name": "Select queue name", "name": "Name", From 8fdf12bb2a06a022aa61bb3a569bf830bbc53e45 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 15 Apr 2022 18:23:57 +0300 Subject: [PATCH 050/276] UI: Add flot datakey and latest datakey settings forms --- .../json/system/widget_bundles/charts.json | 12 +- .../widget/lib/flot-widget.models.ts | 814 +----------------- .../home/components/widget/lib/flot-widget.ts | 15 - .../flot-bar-key-settings.component.html | 24 + .../chart/flot-bar-key-settings.component.ts | 55 ++ .../chart/flot-key-settings.component.html | 238 +++++ .../chart/flot-key-settings.component.ts | 333 +++++++ .../flot-latest-key-settings.component.html | 47 + .../flot-latest-key-settings.component.ts | 74 ++ .../flot-line-key-settings.component.html | 24 + .../chart/flot-line-key-settings.component.ts | 55 ++ .../chart/flot-threshold.component.html | 57 ++ .../chart/flot-threshold.component.scss | 40 + .../chart/flot-threshold.component.ts | 136 +++ .../chart/flot-widget-settings.component.ts | 1 - .../lib/settings/widget-settings.module.ts | 30 +- .../assets/locale/locale.constant-en_US.json | 35 +- 17 files changed, 1158 insertions(+), 832 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-bar-key-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-bar-key-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-latest-key-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-latest-key-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-line-key-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-line-key-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/charts.json b/application/src/main/data/json/system/widget_bundles/charts.json index 83f75ae302..e612111e3a 100644 --- a/application/src/main/data/json/system/widget_bundles/charts.json +++ b/application/src/main/data/json/system/widget_bundles/charts.json @@ -146,10 +146,12 @@ "resources": [], "templateHtml": "", "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", - "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'state'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.flot.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true,\n hasAdditionalLatestDataKeys: true\n };\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.getLatestDataKeySettingsSchema = function() {\n return TbFlot.latestDatakeySettingsSchema();\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'state'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.flot.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true,\n hasAdditionalLatestDataKeys: true\n };\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}", "settingsDirective": "tb-flot-line-widget-settings", + "dataKeySettingsDirective": "tb-flot-line-key-settings", + "latestDataKeySettingsDirective": "tb-flot-latest-key-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\",\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":false,\"tooltipIndividual\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"smoothLines\":false},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"direction\":\"column\",\",position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}" } }, @@ -165,11 +167,13 @@ "resources": [], "templateHtml": "", "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", - "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.flot.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.getLatestDataKeySettingsSchema = function() {\n return TbFlot.latestDatakeySettingsSchema();\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasAdditionalLatestDataKeys: true\n };\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.flot.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasAdditionalLatestDataKeys: true\n };\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}", "latestDataKeySettingsSchema": "{}", "settingsDirective": "tb-flot-line-widget-settings", + "dataKeySettingsDirective": "tb-flot-line-key-settings", + "latestDataKeySettingsDirective": "tb-flot-latest-key-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"legend\":{\"show\":true,\"position\":\"nw\",\"backgroundColor\":\"#f0f0f0\",\"backgroundOpacity\":0.85,\"labelBoxBorderColor\":\"rgba(1, 1, 1, 0.45)\"},\"decimals\":1,\"stack\":false,\"tooltipIndividual\":false},\"title\":\"Timeseries Line Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}" } }, @@ -185,10 +189,12 @@ "resources": [], "templateHtml": "", "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", - "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'bar'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.flot.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(false, 'bar');\n}\n\nself.getLatestDataKeySettingsSchema = function() {\n return TbFlot.latestDatakeySettingsSchema();\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasAdditionalLatestDataKeys: true\n };\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'bar'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.flot.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasAdditionalLatestDataKeys: true\n };\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}", "settingsDirective": "tb-flot-bar-widget-settings", + "dataKeySettingsDirective": "tb-flot-bar-key-settings", + "latestDataKeySettingsDirective": "tb-flot-latest-key-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000},\"aggregation\":{\"limit\":200,\"type\":\"AVG\"}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":true,\"tooltipIndividual\":false,\"defaultBarWidth\":600},\"title\":\"Timeseries Bar Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{}}" } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts index 66317bf596..fcc524ad1e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts @@ -18,7 +18,6 @@ /// import { DataKey, Datasource, DatasourceData, JsonSettingsSchema } from '@shared/models/widget.models'; -import * as moment_ from 'moment'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { ComparisonDuration } from '@shared/models/time/time.models'; @@ -163,13 +162,15 @@ export interface TbFlotLabelPatternSettings { settings?: any; } -export interface TbFlotGraphSettings extends TbFlotBaseSettings, TbFlotThresholdsSettings, TbFlotComparisonSettings, TbFlotCustomLegendSettings { +export interface TbFlotGraphSettings extends TbFlotBaseSettings, + TbFlotThresholdsSettings, TbFlotComparisonSettings, TbFlotCustomLegendSettings { smoothLines: boolean; } export declare type BarAlignment = 'left' | 'right' | 'center'; -export interface TbFlotBarSettings extends TbFlotBaseSettings, TbFlotThresholdsSettings, TbFlotComparisonSettings, TbFlotCustomLegendSettings { +export interface TbFlotBarSettings extends TbFlotBaseSettings, + TbFlotThresholdsSettings, TbFlotComparisonSettings, TbFlotCustomLegendSettings { defaultBarWidth: number; barAlignment: BarAlignment; } @@ -183,7 +184,7 @@ export interface TbFlotPieSettings { color: string; width: number; }; - showTooltip: boolean, + showTooltip: boolean; showLabels: boolean; fontColor: string; fontSize: number; @@ -241,480 +242,6 @@ export interface TbFlotLatestKeySettings { thresholdColor: string; } -export function flotSettingsSchema(chartType: ChartType): JsonSettingsSchema { - - const schema: JsonSettingsSchema = { - schema: { - type: 'object', - title: 'Settings', - properties: { - } - } - }; - - const properties: any = schema.schema.properties; - properties.stack = { - title: 'Stacking', - type: 'boolean', - default: false - }; - if (chartType === 'graph') { - properties.smoothLines = { - title: 'Display smooth (curved) lines', - type: 'boolean', - default: false - }; - } - if (chartType === 'bar') { - properties.defaultBarWidth = { - title: 'Default bar width for non-aggregated data (milliseconds)', - type: 'number', - default: 600 - }; - properties.barAlignment = { - title: 'Bar alignment', - type: 'string', - default: 'left' - }; - } - if (chartType === 'graph' || chartType === 'bar') { - properties.thresholdsLineWidth = { - title: 'Default line width for all thresholds', - type: 'number' - }; - } - properties.shadowSize = { - title: 'Shadow size', - type: 'number', - default: 4 - }; - properties.fontColor = { - title: 'Font color', - type: 'string', - default: '#545454' - }; - properties.fontSize = { - title: 'Font size', - type: 'number', - default: 10 - }; - properties.tooltipIndividual = { - title: 'Hover individual points', - type: 'boolean', - default: false - }; - properties.tooltipCumulative = { - title: 'Show cumulative values in stacking mode', - type: 'boolean', - default: false - }; - properties.tooltipValueFormatter = { - title: 'Tooltip value format function, f(value)', - type: 'string', - default: '' - }; - properties.hideZeros = { - title: 'Hide zero/false values from tooltip', - type: 'boolean', - default: false - }; - - properties.showTooltip = { - title: 'Show tooltip', - type: 'boolean', - default: true - }; - - properties.grid = { - title: 'Grid settings', - type: 'object', - properties: { - color: { - title: 'Primary color', - type: 'string', - default: '#545454' - }, - backgroundColor: { - title: 'Background color', - type: 'string', - default: null - }, - tickColor: { - title: 'Ticks color', - type: 'string', - default: '#DDDDDD' - }, - outlineWidth: { - title: 'Grid outline/border width (px)', - type: 'number', - default: 1 - }, - verticalLines: { - title: 'Show vertical lines', - type: 'boolean', - default: true - }, - horizontalLines: { - title: 'Show horizontal lines', - type: 'boolean', - default: true - } - } - }; - - properties.xaxis = { - title: 'X axis settings', - type: 'object', - properties: { - showLabels: { - title: 'Show labels', - type: 'boolean', - default: true - }, - title: { - title: 'Axis title', - type: 'string', - default: null - }, - color: { - title: 'Ticks color', - type: 'string', - default: null - } - } - }; - - properties.yaxis = { - title: 'Y axis settings', - type: 'object', - properties: { - min: { - title: 'Minimum value on the scale', - type: 'number', - default: null - }, - max: { - title: 'Maximum value on the scale', - type: 'number', - default: null - }, - showLabels: { - title: 'Show labels', - type: 'boolean', - default: true - }, - title: { - title: 'Axis title', - type: 'string', - default: null - }, - color: { - title: 'Ticks color', - type: 'string', - default: null - }, - ticksFormatter: { - title: 'Ticks formatter function, f(value)', - type: 'string', - default: '' - }, - tickDecimals: { - title: 'The number of decimals to display', - type: 'number', - default: 0 - }, - tickSize: { - title: 'Step size between ticks', - type: 'number', - default: null - } - } - }; - - schema.schema.required = []; - schema.form = ['stack']; - if (chartType === 'graph') { - schema.form.push('smoothLines'); - } - if (chartType === 'bar') { - schema.form.push('defaultBarWidth'); - schema.form.push({ - key: 'barAlignment', - type: 'rc-select', - multiple: false, - items: [ - { - value: 'left', - label: 'Left' - }, - { - value: 'right', - label: 'Right' - }, - { - value: 'center', - label: 'Center' - } - ] - }); - } - if (chartType === 'graph' || chartType === 'bar') { - schema.form.push('thresholdsLineWidth'); - } - schema.form.push('shadowSize'); - schema.form.push({ - key: 'fontColor', - type: 'color' - }); - schema.form.push('fontSize'); - schema.form.push('tooltipIndividual'); - schema.form.push('tooltipCumulative'); - schema.form.push({ - key: 'tooltipValueFormatter', - type: 'javascript', - helpId: 'widget/lib/flot/tooltip_value_format_fn' - }); - schema.form.push('hideZeros'); - schema.form.push('showTooltip'); - schema.form.push({ - key: 'grid', - items: [ - { - key: 'grid.color', - type: 'color' - }, - { - key: 'grid.backgroundColor', - type: 'color' - }, - { - key: 'grid.tickColor', - type: 'color' - }, - 'grid.outlineWidth', - 'grid.verticalLines', - 'grid.horizontalLines' - ] - }); - schema.form.push({ - key: 'xaxis', - items: [ - 'xaxis.showLabels', - 'xaxis.title', - { - key: 'xaxis.color', - type: 'color' - } - ] - }); - schema.form.push({ - key: 'yaxis', - items: [ - 'yaxis.min', - 'yaxis.max', - 'yaxis.tickDecimals', - 'yaxis.tickSize', - 'yaxis.showLabels', - 'yaxis.title', - { - key: 'yaxis.color', - type: 'color' - }, - { - key: 'yaxis.ticksFormatter', - type: 'javascript', - helpId: 'widget/lib/flot/ticks_formatter_fn' - } - ] - }); - if (chartType === 'graph' || chartType === 'bar') { - schema.groupInfoes = [{ - formIndex: 0, - GroupTitle: 'Common Settings' - }]; - schema.form = [schema.form]; - schema.schema.properties = {...schema.schema.properties, ...chartSettingsSchemaForComparison.schema.properties, ...chartSettingsSchemaForCustomLegend.schema.properties}; - schema.schema.required = schema.schema.required.concat(chartSettingsSchemaForComparison.schema.required, chartSettingsSchemaForCustomLegend.schema.required); - schema.form.push(chartSettingsSchemaForComparison.form, chartSettingsSchemaForCustomLegend.form); - schema.groupInfoes.push({ - formIndex: schema.groupInfoes.length, - GroupTitle: 'Comparison Settings' - }); - schema.groupInfoes.push({ - formIndex: schema.groupInfoes.length, - GroupTitle: 'Custom Legend Settings' - }); - } - return schema; -} - -const chartSettingsSchemaForComparison: JsonSettingsSchema = { - schema: { - title: 'Comparison Settings', - type: 'object', - properties: { - comparisonEnabled: { - title: 'Enable comparison', - type: 'boolean', - default: false - }, - timeForComparison: { - title: 'Time to show historical data', - type: 'string', - default: 'previousInterval' - }, - comparisonCustomIntervalValue: { - title: 'Custom interval value (ms)', - type: 'number', - default: 7200000 - }, - xaxisSecond: { - title: 'Second X axis', - type: 'object', - properties: { - axisPosition: { - title: 'Axis position', - type: 'string', - default: 'top' - }, - showLabels: { - title: 'Show labels', - type: 'boolean', - default: true - }, - title: { - title: 'Axis title', - type: 'string', - default: null - } - } - } - }, - required: [] - }, - form: [ - 'comparisonEnabled', - { - key: 'timeForComparison', - type: 'rc-select', - multiple: false, - items: [ - { - value: 'previousInterval', - label: 'Previous interval (default)' - }, - { - value: 'days', - label: 'Day ago' - }, - { - value: 'weeks', - label: 'Week ago' - }, - { - value: 'months', - label: 'Month ago' - }, - { - value: 'years', - label: 'Year ago' - }, - { - value: 'customInterval', - label: 'Custom interval' - } - ] - }, - { - key: 'comparisonCustomIntervalValue', - condition: 'model.timeForComparison === "customInterval"' - }, - { - key: 'xaxisSecond', - items: [ - { - key: 'xaxisSecond.axisPosition', - type: 'rc-select', - multiple: false, - items: [ - { - value: 'top', - label: 'Top (default)' - }, - { - value: 'bottom', - label: 'Bottom' - } - ] - }, - 'xaxisSecond.showLabels', - 'xaxisSecond.title', - ] - } - ] -}; - -const chartSettingsSchemaForCustomLegend: JsonSettingsSchema = { - schema: { - title: 'Custom Legend Settings', - type: 'object', - properties: { - customLegendEnabled: { - title: 'Enable custom legend (this will allow you to use attribute/timeseries values in key labels)', - type: 'boolean', - default: false - }, - dataKeysListForLabels: { - title: 'Datakeys list to use in labels', - type: 'array', - items: { - type: 'object', - properties: { - name: { - title: 'Key name', - type: 'string' - }, - type: { - title: 'Key type', - type: 'string', - default: 'attribute' - } - }, - required: [ - 'name' - ] - } - } - }, - required: [] - }, - form: [ - 'customLegendEnabled', - { - key: 'dataKeysListForLabels', - condition: 'model.customLegendEnabled === true', - items: [ - { - key: 'dataKeysListForLabels[].type', - type: 'rc-select', - multiple: false, - items: [ - { - value: 'attribute', - label: 'Attribute' - }, - { - value: 'timeseries', - label: 'Timeseries' - } - ] - }, - 'dataKeysListForLabels[].name' - ] - } - ] -}; - export const flotPieSettingsSchema: JsonSettingsSchema = { schema: { type: 'object', @@ -833,334 +360,3 @@ export const flotPieDatakeySettingsSchema: JsonSettingsSchema = { 'removeFromLegend' ] }; - -export function flotDatakeySettingsSchema(defaultShowLines: boolean, chartType: ChartType): JsonSettingsSchema { - const schema: JsonSettingsSchema = { - schema: { - type: 'object', - title: 'DataKeySettings', - properties: { - excludeFromStacking: { - title: 'Exclude from stacking(available in "Stacking" mode)', - type: 'boolean', - default: false - }, - hideDataByDefault: { - title: 'Data is hidden by default', - type: 'boolean', - default: false - }, - disableDataHiding: { - title: 'Disable data hiding', - type: 'boolean', - default: false - }, - removeFromLegend: { - title: 'Remove datakey from legend', - type: 'boolean', - default: false - }, - showLines: { - title: 'Show lines', - type: 'boolean', - default: defaultShowLines - }, - fillLines: { - title: 'Fill lines', - type: 'boolean', - default: false - }, - showPoints: { - title: 'Show points', - type: 'boolean', - default: false - }, - showPointShape: { - title: 'Select point shape:', - type: 'string', - default: 'circle' - }, - pointShapeFormatter: { - title: 'Point shape format function, f(ctx, x, y, radius, shadow)', - type: 'string', - default: 'var size = radius * Math.sqrt(Math.PI) / 2;\n' + - 'ctx.moveTo(x - size, y - size);\n' + - 'ctx.lineTo(x + size, y + size);\n' + - 'ctx.moveTo(x - size, y + size);\n' + - 'ctx.lineTo(x + size, y - size);' - }, - showPointsLineWidth: { - title: 'Line width of points', - type: 'number', - default: 5 - }, - showPointsRadius: { - title: 'Radius of points', - type: 'number', - default: 3 - }, - tooltipValueFormatter: { - title: 'Tooltip value format function, f(value)', - type: 'string', - default: '' - }, - showSeparateAxis: { - title: 'Show separate axis', - type: 'boolean', - default: false - }, - axisMin: { - title: 'Minimum value on the axis scale', - type: 'number', - default: null - }, - axisMax: { - title: 'Maximum value on the axis scale', - type: 'number', - default: null - }, - axisTitle: { - title: 'Axis title', - type: 'string', - default: '' - }, - axisTickDecimals: { - title: 'Axis tick number of digits after floating point', - type: 'number', - default: null - }, - axisTickSize: { - title: 'Axis step size between ticks', - type: 'number', - default: null - }, - axisPosition: { - title: 'Axis position', - type: 'string', - default: 'left' - }, - axisTicksFormatter: { - title: 'Ticks formatter function, f(value)', - type: 'string', - default: '' - } - }, - required: ['showLines', 'fillLines', 'showPoints'] - }, - form: [ - 'hideDataByDefault', - 'disableDataHiding', - 'removeFromLegend', - 'excludeFromStacking', - 'showLines', - 'fillLines', - 'showPoints', - { - key: 'showPointShape', - type: 'rc-select', - multiple: false, - items: [ - { - value: 'circle', - label: 'Circle' - }, - { - value: 'cross', - label: 'Cross' - }, - { - value: 'diamond', - label: 'Diamond' - }, - { - value: 'square', - label: 'Square' - }, - { - value: 'triangle', - label: 'Triangle' - }, - { - value: 'custom', - label: 'Custom function' - } - ] - }, - { - key: 'pointShapeFormatter', - type: 'javascript', - helpId: 'widget/lib/flot/point_shape_format_fn' - }, - 'showPointsLineWidth', - 'showPointsRadius', - { - key: 'tooltipValueFormatter', - type: 'javascript', - helpId: 'widget/lib/flot/tooltip_value_format_fn' - }, - 'showSeparateAxis', - 'axisMin', - 'axisMax', - 'axisTitle', - 'axisTickDecimals', - 'axisTickSize', - { - key: 'axisPosition', - type: 'rc-select', - multiple: false, - items: [ - { - value: 'left', - label: 'Left' - }, - { - value: 'right', - label: 'Right' - } - ] - }, - { - key: 'axisTicksFormatter', - type: 'javascript', - helpId: 'widget/lib/flot/ticks_formatter_fn' - } - ] - }; - - const properties = schema.schema.properties; - if (chartType === 'graph' || chartType === 'bar') { - properties.thresholds = { - title: 'Thresholds', - type: 'array', - items: { - title: 'Threshold', - type: 'object', - properties: { - thresholdValueSource: { - title: 'Threshold value source', - type: 'string', - default: 'predefinedValue' - }, - thresholdEntityAlias: { - title: 'Thresholds source entity alias', - type: 'string' - }, - thresholdAttribute: { - title: 'Threshold source entity attribute', - type: 'string' - }, - thresholdValue: { - title: 'Threshold value (if predefined value is selected)', - type: 'number' - }, - lineWidth: { - title: 'Line width', - type: 'number' - }, - color: { - title: 'Color', - type: 'string' - } - } - }, - required: [] - }; - schema.form.push({ - key: 'thresholds', - items: [ - { - key: 'thresholds[].thresholdValueSource', - type: 'rc-select', - multiple: false, - items: [ - { - value: 'predefinedValue', - label: 'Predefined value (Default)' - }, - { - value: 'entityAttribute', - label: 'Value taken from entity attribute' - } - ] - }, - 'thresholds[].thresholdValue', - 'thresholds[].thresholdEntityAlias', - 'thresholds[].thresholdAttribute', - { - key: 'thresholds[].color', - type: 'color' - }, - 'thresholds[].lineWidth' - ] - }); - properties.comparisonSettings = { - title: 'Comparison Settings', - type: 'object', - properties: { - showValuesForComparison: { - title: 'Show historical values for comparison', - type: 'boolean', - default: true - }, - comparisonValuesLabel: { - title: 'Historical values label', - type: 'string', - default: '' - }, - color: { - title: 'Color', - type: 'string', - default: '' - } - }, - required: ['showValuesForComparison'] - }; - schema.form.push({ - key: 'comparisonSettings', - items: [ - 'comparisonSettings.showValuesForComparison', - 'comparisonSettings.comparisonValuesLabel', - { - key: 'comparisonSettings.color', - type: 'color' - } - ] - }); - } - - return schema; -} - -export const flotLatestDatakeySettingsSchema: JsonSettingsSchema = { - schema: { - type: 'object', - title: 'LatestDataKeySettings', - properties: { - useAsThreshold: { - title: 'Use key value as threshold', - type: 'boolean', - default: false - }, - thresholdLineWidth: { - title: 'Threshold line width', - type: 'number' - }, - thresholdColor: { - title: 'Threshold color', - type: 'string' - } - } - }, - form: [ - 'useAsThreshold', - { - key: 'thresholdLineWidth', - condition: 'model.useAsThreshold === true' - }, - { - key: 'thresholdColor', - type: 'color', - condition: 'model.useAsThreshold === true' - }, - ] -}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts index 736cc30450..2ed20870dc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts @@ -38,10 +38,8 @@ import { } from '@app/shared/models/widget.models'; import { ChartType, - flotDatakeySettingsSchema, flotLatestDatakeySettingsSchema, flotPieDatakeySettingsSchema, flotPieSettingsSchema, - flotSettingsSchema, TbFlotAxisOptions, TbFlotHoverInfo, TbFlotKeySettings, TbFlotLatestKeySettings, @@ -69,7 +67,6 @@ const moment = moment_; const flotPieSettingsSchemaValue = flotPieSettingsSchema; const flotPieDatakeySettingsSchemaValue = flotPieDatakeySettingsSchema; -const latestDatakeySettingsSchemaValue = flotLatestDatakeySettingsSchema; export class TbFlot { @@ -141,18 +138,6 @@ export class TbFlot { return flotPieDatakeySettingsSchemaValue; } - static settingsSchema(chartType: ChartType): JsonSettingsSchema { - return flotSettingsSchema(chartType); - } - - static datakeySettingsSchema(defaultShowLines: boolean, chartType: ChartType): JsonSettingsSchema { - return flotDatakeySettingsSchema(defaultShowLines, chartType); - } - - static latestDatakeySettingsSchema(): JsonSettingsSchema { - return latestDatakeySettingsSchemaValue; - } - constructor(private ctx: WidgetContext, private readonly chartType: ChartType) { this.chartType = this.chartType || 'line'; this.settings = ctx.settings as TbFlotSettings; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-bar-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-bar-key-settings.component.html new file mode 100644 index 0000000000..f96f2ddbff --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-bar-key-settings.component.html @@ -0,0 +1,24 @@ + + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-bar-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-bar-key-settings.component.ts new file mode 100644 index 0000000000..1dd9c73dd0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-bar-key-settings.component.ts @@ -0,0 +1,55 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { flotDataKeyDefaultSettings } from '@home/components/widget/lib/settings/chart/flot-key-settings.component'; + +@Component({ + selector: 'tb-flot-bar-key-settings', + templateUrl: './flot-bar-key-settings.component.html', + styleUrls: [] +}) +export class FlotBarKeySettingsComponent extends WidgetSettingsComponent { + + flotBarKeySettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.flotBarKeySettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return flotDataKeyDefaultSettings('bar'); + } + + protected onSettingsSet(settings: WidgetSettings) { + this.flotBarKeySettingsForm = this.fb.group({ + flotKeySettings: [settings, []] + }); + } + + protected prepareOutputSettings(settings: any): WidgetSettings { + return settings.flotKeySettings; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html new file mode 100644 index 0000000000..81ebfc7c6f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html @@ -0,0 +1,238 @@ + +
+
+ widgets.chart.common-settings + + {{ 'widgets.chart.data-is-hidden-by-default' | translate }} + + + {{ 'widgets.chart.disable-data-hiding' | translate }} + + + {{ 'widgets.chart.remove-from-legend' | translate }} + + + {{ 'widgets.chart.exclude-from-stacking' | translate }} + +
+
+ widgets.chart.line-settings + + + + + {{ 'widgets.chart.show-line' | translate }} + + + + widget-config.advanced-settings + + + +
+ + widgets.chart.line-width + + + + {{ 'widgets.chart.fill-line' | translate }} + +
+
+
+
+
+ widgets.chart.points-settings + + + + + {{ 'widgets.chart.show-points' | translate }} + + + + widget-config.advanced-settings + + + +
+
+ + widgets.chart.points-line-width + + + + widgets.chart.points-radius + + +
+ + widgets.chart.point-shape + + + {{ 'widgets.chart.point-shape-circle' | translate }} + + + {{ 'widgets.chart.point-shape-cross' | translate }} + + + {{ 'widgets.chart.point-shape-diamond' | translate }} + + + {{ 'widgets.chart.point-shape-square' | translate }} + + + {{ 'widgets.chart.point-shape-triangle' | translate }} + + + {{ 'widgets.chart.point-shape-custom' | translate }} + + + + + +
+
+
+
+
+ widgets.chart.tooltip-settings + + +
+
+ widgets.chart.yaxis-settings + + {{ 'widgets.chart.show-separate-axis' | translate }} + + + widgets.chart.axis-title + + +
+ + widgets.chart.min-scale-value + + + + widgets.chart.max-scale-value + + +
+ + widgets.chart.axis-position + + + {{ 'widgets.chart.axis-position-left' | translate }} + + + {{ 'widgets.chart.axis-position-right' | translate }} + + + +
+ widgets.chart.yaxis-tick-labels-settings +
+ + widgets.chart.tick-step-size + + + + widgets.chart.number-of-decimals + + +
+ + +
+
+
+ widgets.chart.thresholds +
+
+
+ + +
+
+
+ widgets.chart.no-thresholds +
+
+ +
+
+
+
+ widgets.chart.comparison-settings + + + + + {{ 'widgets.chart.show-values-for-comparison' | translate }} + + + + widget-config.advanced-settings + + + +
+ + widgets.chart.comparison-values-label + + + + +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.ts new file mode 100644 index 0000000000..a97ccc84a0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.ts @@ -0,0 +1,333 @@ +/// +/// Copyright © 2016-2022 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 { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { ChartType, TbFlotKeySettings, TbFlotKeyThreshold } from '@home/components/widget/lib/flot-widget.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { WidgetService } from '@core/http/widget.service'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { IAliasController } from 'src/app/core/api/widget-api.models'; + +export function flotDataKeyDefaultSettings(chartType: ChartType): TbFlotKeySettings { + const settings: TbFlotKeySettings = { + // Common settings + hideDataByDefault: false, + disableDataHiding: false, + removeFromLegend: false, + excludeFromStacking: false, + + // Line settings + showLines: chartType === 'graph', + lineWidth: 1, + fillLines: false, + + // Points settings + showPoints: false, + showPointsLineWidth: 5, + showPointsRadius: 3, + showPointShape: 'circle', + pointShapeFormatter: 'var size = radius * Math.sqrt(Math.PI) / 2;\n' + + 'ctx.moveTo(x - size, y - size);\n' + + 'ctx.lineTo(x + size, y + size);\n' + + 'ctx.moveTo(x - size, y + size);\n' + + 'ctx.lineTo(x + size, y - size);', + + // Tooltip settings + tooltipValueFormatter: '', + + // Y axis settings + showSeparateAxis: false, + axisTitle: '', + axisMin: null, + axisMax: null, + axisPosition: 'left', + + // --> Y axis tick labels settings + axisTickSize: null, + axisTickDecimals: null, + axisTicksFormatter: '', + + // Thresholds + thresholds: [], + + // Comparison settings + comparisonSettings: { + showValuesForComparison: true, + comparisonValuesLabel: '', + color: '' + } + }; + return settings; +} + +@Component({ + selector: 'tb-flot-key-settings', + templateUrl: './flot-key-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FlotKeySettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => FlotKeySettingsComponent), + multi: true, + } + ] +}) +export class FlotKeySettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { + + @Input() + disabled: boolean; + + @Input() + chartType: ChartType; + + @Input() + aliasController: IAliasController; + + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + + private modelValue: TbFlotKeySettings; + + private propagateChange = null; + + public flotKeySettingsFormGroup: FormGroup; + + constructor(protected store: Store, + private translate: TranslateService, + private widgetService: WidgetService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.flotKeySettingsFormGroup = this.fb.group({ + + // Common settings + + hideDataByDefault: [false, []], + disableDataHiding: [false, []], + removeFromLegend: [false, []], + excludeFromStacking: [false, []], + + // Line settings + + showLines: [this.chartType === 'graph', []], + lineWidth: [1, [Validators.min(0)]], + fillLines: [false, []], + + // Points settings + + showPoints: [false, []], + showPointsLineWidth: [5, [Validators.min(0)]], + showPointsRadius: [3, [Validators.min(0)]], + showPointShape: ['circle', []], + pointShapeFormatter: ['', []], + + // Tooltip settings + + tooltipValueFormatter: ['', []], + + // Y axis settings + + showSeparateAxis: [false, []], + axisTitle: [null, []], + axisMin: [null, []], + axisMax: [null, []], + axisPosition: ['left', []], + + // --> Y axis tick labels settings + + axisTickSize: [null, [Validators.min(0)]], + axisTickDecimals: [null, [Validators.min(0)]], + axisTicksFormatter: ['', []], + + // Thresholds + + thresholds: this.fb.array([]), + + // Comparison settings + + comparisonSettings: this.fb.group({ + showValuesForComparison: [true, []], + comparisonValuesLabel: ['', []], + color: ['', []] + }) + + }); + + this.flotKeySettingsFormGroup.get('showLines').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + + this.flotKeySettingsFormGroup.get('showPoints').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + + this.flotKeySettingsFormGroup.get('comparisonSettings.showValuesForComparison').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + + this.flotKeySettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + + this.updateValidators(false); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.flotKeySettingsFormGroup.disable({emitEvent: false}); + } else { + this.flotKeySettingsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: TbFlotKeySettings): void { + const thresholds = value?.thresholds; + this.modelValue = value; + this.flotKeySettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + const thresholdsControls: Array = []; + if (thresholds && thresholds.length) { + thresholds.forEach((threshold) => { + thresholdsControls.push(this.fb.control(threshold, [])); + }); + } + this.flotKeySettingsFormGroup.setControl('thresholds', this.fb.array(thresholdsControls), {emitEvent: false}); + this.updateValidators(false); + } + + validate(c: FormControl) { + return (this.flotKeySettingsFormGroup.valid) ? null : { + flotKeySettings: { + valid: false, + }, + }; + } + + private updateModel() { + const value: TbFlotKeySettings = this.flotKeySettingsFormGroup.value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } + + private updateValidators(emitEvent?: boolean): void { + const showLines: boolean = this.flotKeySettingsFormGroup.get('showLines').value; + const showPoints: boolean = this.flotKeySettingsFormGroup.get('showPoints').value; + const showValuesForComparison: boolean = this.flotKeySettingsFormGroup.get('comparisonSettings.showValuesForComparison').value; + + if (showLines) { + this.flotKeySettingsFormGroup.get('lineWidth').enable({emitEvent}); + this.flotKeySettingsFormGroup.get('fillLines').enable({emitEvent}); + } else { + this.flotKeySettingsFormGroup.get('lineWidth').disable({emitEvent}); + this.flotKeySettingsFormGroup.get('fillLines').disable({emitEvent}); + } + + if (showPoints) { + this.flotKeySettingsFormGroup.get('showPointsLineWidth').enable({emitEvent}); + this.flotKeySettingsFormGroup.get('showPointsRadius').enable({emitEvent}); + this.flotKeySettingsFormGroup.get('showPointShape').enable({emitEvent}); + this.flotKeySettingsFormGroup.get('pointShapeFormatter').enable({emitEvent}); + } else { + this.flotKeySettingsFormGroup.get('showPointsLineWidth').disable({emitEvent}); + this.flotKeySettingsFormGroup.get('showPointsRadius').disable({emitEvent}); + this.flotKeySettingsFormGroup.get('showPointShape').disable({emitEvent}); + this.flotKeySettingsFormGroup.get('pointShapeFormatter').disable({emitEvent}); + } + + if (showValuesForComparison) { + this.flotKeySettingsFormGroup.get('comparisonSettings.comparisonValuesLabel').enable({emitEvent}); + this.flotKeySettingsFormGroup.get('comparisonSettings.color').enable({emitEvent}); + } else { + this.flotKeySettingsFormGroup.get('comparisonSettings.comparisonValuesLabel').disable({emitEvent}); + this.flotKeySettingsFormGroup.get('comparisonSettings.color').disable({emitEvent}); + } + + this.flotKeySettingsFormGroup.get('lineWidth').updateValueAndValidity({emitEvent: false}); + this.flotKeySettingsFormGroup.get('fillLines').updateValueAndValidity({emitEvent: false}); + this.flotKeySettingsFormGroup.get('showPointsLineWidth').updateValueAndValidity({emitEvent: false}); + this.flotKeySettingsFormGroup.get('showPointsRadius').updateValueAndValidity({emitEvent: false}); + this.flotKeySettingsFormGroup.get('showPointShape').updateValueAndValidity({emitEvent: false}); + this.flotKeySettingsFormGroup.get('pointShapeFormatter').updateValueAndValidity({emitEvent: false}); + this.flotKeySettingsFormGroup.get('comparisonSettings.comparisonValuesLabel').updateValueAndValidity({emitEvent: false}); + this.flotKeySettingsFormGroup.get('comparisonSettings.color').updateValueAndValidity({emitEvent: false}); + } + + thresholdsFormArray(): FormArray { + return this.flotKeySettingsFormGroup.get('thresholds') as FormArray; + } + + public trackByThreshold(index: number, thresholdControl: AbstractControl): any { + return thresholdControl; + } + + public removeThreshold(index: number) { + (this.flotKeySettingsFormGroup.get('thresholds') as FormArray).removeAt(index); + } + + public addThreshold() { + const threshold: TbFlotKeyThreshold = { + thresholdValueSource: 'predefinedValue', + thresholdEntityAlias: null, + thresholdAttribute: null, + thresholdValue: null, + lineWidth: null, + color: null + }; + const thresholdsArray = this.flotKeySettingsFormGroup.get('thresholds') as FormArray; + const thresholdControl = this.fb.control(threshold, []); + (thresholdControl as any).new = true; + thresholdsArray.push(thresholdControl); + this.flotKeySettingsFormGroup.updateValueAndValidity(); + } + + thresholdDrop(event: CdkDragDrop) { + const thresholdsArray = this.flotKeySettingsFormGroup.get('thresholds') as FormArray; + const threshold = thresholdsArray.at(event.previousIndex); + thresholdsArray.removeAt(event.previousIndex); + thresholdsArray.insert(event.currentIndex, threshold); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-latest-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-latest-key-settings.component.html new file mode 100644 index 0000000000..0e210366d5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-latest-key-settings.component.html @@ -0,0 +1,47 @@ + +
+
+ widgets.chart.threshold-settings + + + + + {{ 'widgets.chart.use-as-threshold' | translate }} + + + + widget-config.advanced-settings + + + +
+ + widgets.chart.threshold-line-width + + + + +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-latest-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-latest-key-settings.component.ts new file mode 100644 index 0000000000..e3b4b5acf1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-latest-key-settings.component.ts @@ -0,0 +1,74 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-flot-latest-key-settings', + templateUrl: './flot-latest-key-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class FlotLatestKeySettingsComponent extends WidgetSettingsComponent { + + flotLatestKeySettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.flotLatestKeySettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + useAsThreshold: false, + thresholdLineWidth: null, + thresholdColor: null + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.flotLatestKeySettingsForm = this.fb.group({ + useAsThreshold: [settings.useAsThreshold, []], + thresholdLineWidth: [settings.thresholdLineWidth, [Validators.min(0)]], + thresholdColor: [settings.thresholdColor, []] + }); + } + + protected validatorTriggers(): string[] { + return ['useAsThreshold']; + } + + protected updateValidators(emitEvent: boolean) { + const useAsThreshold: boolean = this.flotLatestKeySettingsForm.get('useAsThreshold').value; + if (useAsThreshold) { + this.flotLatestKeySettingsForm.get('thresholdLineWidth').enable(); + this.flotLatestKeySettingsForm.get('thresholdColor').enable(); + } else { + this.flotLatestKeySettingsForm.get('thresholdLineWidth').disable(); + this.flotLatestKeySettingsForm.get('thresholdColor').disable(); + } + this.flotLatestKeySettingsForm.get('thresholdLineWidth').updateValueAndValidity({emitEvent}); + this.flotLatestKeySettingsForm.get('thresholdColor').updateValueAndValidity({emitEvent}); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-line-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-line-key-settings.component.html new file mode 100644 index 0000000000..fbc179599f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-line-key-settings.component.html @@ -0,0 +1,24 @@ + + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-line-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-line-key-settings.component.ts new file mode 100644 index 0000000000..46b83715fe --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-line-key-settings.component.ts @@ -0,0 +1,55 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { flotDataKeyDefaultSettings } from '@home/components/widget/lib/settings/chart/flot-key-settings.component'; + +@Component({ + selector: 'tb-flot-line-key-settings', + templateUrl: './flot-line-key-settings.component.html', + styleUrls: [] +}) +export class FlotLineKeySettingsComponent extends WidgetSettingsComponent { + + flotLineKeySettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.flotLineKeySettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return flotDataKeyDefaultSettings('graph'); + } + + protected onSettingsSet(settings: WidgetSettings) { + this.flotLineKeySettingsForm = this.fb.group({ + flotKeySettings: [settings, []] + }); + } + + protected prepareOutputSettings(settings: any): WidgetSettings { + return settings.flotKeySettings; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html new file mode 100644 index 0000000000..9fdf98fec7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html @@ -0,0 +1,57 @@ + + + +
+ +
+
{{ thresholdText() }}
+
+
+
+
+
+ + +
+
+ +
+ +
+ +
+ + widgets.chart.line-width + + + + +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.scss new file mode 100644 index 0000000000..06d9988e53 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.scss @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2022 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. + */ +:host { + display: block; + .mat-expansion-panel { + box-shadow: none; + &.flot-threshold { + border: 1px groove rgba(0, 0, 0, .25); + .mat-expansion-panel-header { + padding: 0 24px 0 8px; + &.mat-expanded { + height: 48px; + } + } + } + } +} + +:host ::ng-deep { + .mat-expansion-panel { + &.flot-threshold { + .mat-expansion-panel-body { + padding: 0 8px 8px; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.ts new file mode 100644 index 0000000000..61f3a16176 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.ts @@ -0,0 +1,136 @@ +/// +/// Copyright © 2016-2022 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 { ValueSourceProperty } from '@home/components/widget/lib/settings/common/value-source.component'; +import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { isNumber } from '@core/utils'; +import { IAliasController } from '@core/api/widget-api.models'; +import { TbFlotKeyThreshold } from '@home/components/widget/lib/flot-widget.models'; + +@Component({ + selector: 'tb-flot-threshold', + templateUrl: './flot-threshold.component.html', + styleUrls: ['./flot-threshold.component.scss', './../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FlotThresholdComponent), + multi: true + } + ] +}) +export class FlotThresholdComponent extends PageComponent implements OnInit, ControlValueAccessor { + + @Input() + disabled: boolean; + + @Input() + expanded = false; + + @Input() + aliasController: IAliasController; + + @Output() + removeThreshold = new EventEmitter(); + + private modelValue: TbFlotKeyThreshold; + + private propagateChange = null; + + public thresholdFormGroup: FormGroup; + + constructor(protected store: Store, + private translate: TranslateService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.thresholdFormGroup = this.fb.group({ + valueSource: [null, []], + lineWidth: [null, [Validators.min(0)]], + color: [null, []] + }); + this.thresholdFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.thresholdFormGroup.disable({emitEvent: false}); + } else { + this.thresholdFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: TbFlotKeyThreshold): void { + this.modelValue = value; + const valueSource: ValueSourceProperty = { + valueSource: value?.thresholdValueSource, + entityAlias: value?.thresholdEntityAlias, + attribute: value?.thresholdAttribute, + value: value?.thresholdValue + }; + this.thresholdFormGroup.patchValue( + {valueSource, lineWidth: value?.lineWidth, color: value?.color}, {emitEvent: false} + ); + } + + thresholdText(): string { + const value: ValueSourceProperty = this.thresholdFormGroup.get('valueSource').value; + return this.valueSourcePropertyText(value); + } + + private valueSourcePropertyText(source?: ValueSourceProperty): string { + if (source) { + if (source.valueSource === 'predefinedValue') { + return `${isNumber(source.value) ? source.value : 0}`; + } else if (source.valueSource === 'entityAttribute') { + const alias = source.entityAlias || 'Undefined'; + const key = source.attribute || 'Undefined'; + return `${alias}.${key}`; + } + } + return 'Undefined'; + } + + private updateModel() { + const value: {valueSource: ValueSourceProperty, lineWidth: number, color: string} = this.thresholdFormGroup.value; + this.modelValue = { + thresholdValueSource: value?.valueSource?.valueSource, + thresholdEntityAlias: value?.valueSource?.entityAlias, + thresholdAttribute: value?.valueSource?.attribute, + thresholdValue: value?.valueSource?.value, + lineWidth: value?.lineWidth, + color: value?.color + }; + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts index 8accd374a6..8d00d55b63 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts @@ -34,7 +34,6 @@ import { AppState } from '@core/core.state'; import { TranslateService } from '@ngx-translate/core'; import { ComparisonDuration } from '@shared/models/time/time.models'; import { WidgetService } from '@core/http/widget.service'; -import { fixedColorLevelValidator } from '@home/components/widget/lib/settings/gauge/fixed-color-level.component'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { LabelDataKey, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index 8ee227e253..c6eb6ce5fe 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -83,6 +83,17 @@ import { LabelDataKeyComponent } from '@home/components/widget/lib/settings/char import { FlotBarWidgetSettingsComponent } from '@home/components/widget/lib/settings/chart/flot-bar-widget-settings.component'; +import { FlotThresholdComponent } from '@home/components/widget/lib/settings/chart/flot-threshold.component'; +import { FlotKeySettingsComponent } from '@home/components/widget/lib/settings/chart/flot-key-settings.component'; +import { + FlotLineKeySettingsComponent +} from '@home/components/widget/lib/settings/chart/flot-line-key-settings.component'; +import { + FlotBarKeySettingsComponent +} from '@home/components/widget/lib/settings/chart/flot-bar-key-settings.component'; +import { + FlotLatestKeySettingsComponent +} from '@home/components/widget/lib/settings/chart/flot-latest-key-settings.component'; @NgModule({ declarations: [ @@ -113,7 +124,12 @@ import { FlotWidgetSettingsComponent, LabelDataKeyComponent, FlotLineWidgetSettingsComponent, - FlotBarWidgetSettingsComponent + FlotBarWidgetSettingsComponent, + FlotThresholdComponent, + FlotKeySettingsComponent, + FlotLineKeySettingsComponent, + FlotBarKeySettingsComponent, + FlotLatestKeySettingsComponent ], imports: [ CommonModule, @@ -148,7 +164,12 @@ import { FlotWidgetSettingsComponent, LabelDataKeyComponent, FlotLineWidgetSettingsComponent, - FlotBarWidgetSettingsComponent + FlotBarWidgetSettingsComponent, + FlotThresholdComponent, + FlotKeySettingsComponent, + FlotLineKeySettingsComponent, + FlotBarKeySettingsComponent, + FlotLatestKeySettingsComponent ] }) export class WidgetSettingsModule { @@ -174,5 +195,8 @@ export const widgetSettingsComponentsMap: {[key: string]: Type
+ + + + + +
+ + widgets.rpc.get-value-method + + + + + +
+ widgets.rpc.update-value-settings + + widgets.rpc.set-value-method + + + + +
+ +
+ widgets.rpc.rpc-settings + + widgets.rpc.request-timeout + + +
+ widgets.rpc.persistent-rpc-settings + + + + + {{ 'widgets.rpc.request-persistent' | translate }} + + + + widget-config.advanced-settings + + + + + widgets.rpc.persistent-polling-interval + + + + +
+
+ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.ts new file mode 100644 index 0000000000..6be84b4241 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.ts @@ -0,0 +1,308 @@ +/// +/// Copyright © 2016-2022 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 { Component, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { WidgetService } from '@core/http/widget.service'; +import { Observable, of } from 'rxjs'; +import { IAliasController } from '@core/api/widget-api.models'; +import { catchError, map, mergeMap, publishReplay, refCount, tap } from 'rxjs/operators'; +import { DataKey } from '@shared/models/widget.models'; +import { EntityService } from '@core/http/entity.service'; + +export declare type RpcRetrieveValueMethod = 'none' | 'rpc' | 'attribute' | 'timeseries'; + +export interface SwitchRpcSettings { + initialValue: boolean; + retrieveValueMethod: RpcRetrieveValueMethod; + valueKey: string; + getValueMethod: string; + setValueMethod: string; + parseValueFunction: string; + convertValueFunction: string; + requestTimeout: number; + requestPersistent: boolean; + persistentPollingInterval: number; +} + +export function switchRpcDefaultSettings(): SwitchRpcSettings { + return { + initialValue: false, + retrieveValueMethod: 'rpc', + valueKey: 'value', + getValueMethod: 'getValue', + parseValueFunction: 'return data ? true : false;', + setValueMethod: 'setValue', + convertValueFunction: 'return value;', + requestTimeout: 500, + requestPersistent: false, + persistentPollingInterval: 5000 + }; +} + +@Component({ + selector: 'tb-switch-rpc-settings', + templateUrl: './switch-rpc-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SwitchRpcSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => SwitchRpcSettingsComponent), + multi: true + } + ] +}) +export class SwitchRpcSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator, OnChanges { + + @ViewChild('keyInput') keyInput: ElementRef; + + @Input() + disabled: boolean; + + @Input() + aliasController: IAliasController; + + @Input() + targetDeviceAliasId: string; + + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + + private modelValue: SwitchRpcSettings; + + private propagateChange = null; + + public switchRpcSettingsFormGroup: FormGroup; + + filteredKeys: Observable>; + keySearchText = ''; + + private latestKeySearchResult: Array = null; + private keysFetchObservable$: Observable> = null; + + constructor(protected store: Store, + private translate: TranslateService, + private widgetService: WidgetService, + private entityService: EntityService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.switchRpcSettingsFormGroup = this.fb.group({ + + // Value settings + + initialValue: [false, []], + + // --> Retrieve value settings + + retrieveValueMethod: ['rpc', []], + valueKey: ['value', [Validators.required]], + getValueMethod: ['getValue', [Validators.required]], + parseValueFunction: ['return data ? true : false;', []], + + // --> Update value settings + + setValueMethod: ['setValue', [Validators.required]], + convertValueFunction: ['return value;', []], + + // RPC settings + + requestTimeout: [500, [Validators.min(0)]], + + // Persistent RPC settings + + requestPersistent: [false, []], + persistentPollingInterval: [5000, [Validators.min(1000)]], + }); + this.switchRpcSettingsFormGroup.get('retrieveValueMethod').valueChanges.subscribe(() => { + this.clearKeysCache(); + this.updateValidators(true); + }); + this.switchRpcSettingsFormGroup.get('requestPersistent').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.switchRpcSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + this.updateValidators(false); + + this.filteredKeys = this.switchRpcSettingsFormGroup.get('valueKey').valueChanges + .pipe( + map(value => value ? value : ''), + mergeMap(name => this.fetchKeys(name) ) + ); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'targetDeviceAliasId') { + this.clearKeysCache(); + } + } + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.switchRpcSettingsFormGroup.disable({emitEvent: false}); + } else { + this.switchRpcSettingsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: SwitchRpcSettings): void { + this.modelValue = value; + this.switchRpcSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + this.updateValidators(false); + } + + clearKey() { + this.switchRpcSettingsFormGroup.get('valueKey').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.keyInput.nativeElement.blur(); + this.keyInput.nativeElement.focus(); + }, 0); + } + + public validate(c: FormControl) { + return this.switchRpcSettingsFormGroup.valid ? null : { + switchRpcSettings: { + valid: false, + }, + }; + } + + private updateModel() { + const value: SwitchRpcSettings = this.switchRpcSettingsFormGroup.value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } + + private updateValidators(emitEvent?: boolean): void { + const retrieveValueMethod: RpcRetrieveValueMethod = this.switchRpcSettingsFormGroup.get('retrieveValueMethod').value; + const requestPersistent: boolean = this.switchRpcSettingsFormGroup.get('requestPersistent').value; + if (retrieveValueMethod === 'none') { + this.switchRpcSettingsFormGroup.get('valueKey').disable({emitEvent}); + this.switchRpcSettingsFormGroup.get('getValueMethod').disable({emitEvent}); + this.switchRpcSettingsFormGroup.get('parseValueFunction').disable({emitEvent}); + } else if (retrieveValueMethod === 'rpc') { + this.switchRpcSettingsFormGroup.get('valueKey').disable({emitEvent}); + this.switchRpcSettingsFormGroup.get('getValueMethod').enable({emitEvent}); + this.switchRpcSettingsFormGroup.get('parseValueFunction').enable({emitEvent}); + } else { + this.switchRpcSettingsFormGroup.get('valueKey').enable({emitEvent}); + this.switchRpcSettingsFormGroup.get('getValueMethod').disable({emitEvent}); + this.switchRpcSettingsFormGroup.get('parseValueFunction').enable({emitEvent}); + } + if (requestPersistent) { + this.switchRpcSettingsFormGroup.get('persistentPollingInterval').enable({emitEvent}); + } else { + this.switchRpcSettingsFormGroup.get('persistentPollingInterval').disable({emitEvent}); + } + this.switchRpcSettingsFormGroup.get('valueKey').updateValueAndValidity({emitEvent: false}); + this.switchRpcSettingsFormGroup.get('getValueMethod').updateValueAndValidity({emitEvent: false}); + this.switchRpcSettingsFormGroup.get('parseValueFunction').updateValueAndValidity({emitEvent: false}); + this.switchRpcSettingsFormGroup.get('persistentPollingInterval').updateValueAndValidity({emitEvent: false}); + } + + private clearKeysCache(): void { + this.latestKeySearchResult = null; + this.keysFetchObservable$ = null; + } + + private fetchKeys(searchText?: string): Observable> { + if (this.keySearchText !== searchText || this.latestKeySearchResult === null) { + this.keySearchText = searchText; + const dataKeyFilter = this.createKeyFilter(this.keySearchText); + return this.getKeys().pipe( + map(name => name.filter(dataKeyFilter)), + tap(res => this.latestKeySearchResult = res) + ); + } + return of(this.latestKeySearchResult); + } + + private getKeys() { + if (this.keysFetchObservable$ === null) { + let fetchObservable: Observable>; + if (this.targetDeviceAliasId) { + const retrieveValueMethod: RpcRetrieveValueMethod = this.switchRpcSettingsFormGroup.get('retrieveValueMethod').value; + const dataKeyTypes = retrieveValueMethod === 'attribute' ? [DataKeyType.attribute] : [DataKeyType.timeseries]; + fetchObservable = this.fetchEntityKeys(this.targetDeviceAliasId, dataKeyTypes); + } else { + fetchObservable = of([]); + } + this.keysFetchObservable$ = fetchObservable.pipe( + map((dataKeys) => dataKeys.map((dataKey) => dataKey.name)), + publishReplay(1), + refCount() + ); + } + return this.keysFetchObservable$; + } + + private fetchEntityKeys(entityAliasId: string, dataKeyTypes: Array): Observable> { + return this.aliasController.getAliasInfo(entityAliasId).pipe( + mergeMap((aliasInfo) => { + return this.entityService.getEntityKeysByEntityFilter( + aliasInfo.entityFilter, + dataKeyTypes, + {ignoreLoading: true, ignoreErrors: true} + ).pipe( + catchError(() => of([])) + ); + }), + catchError(() => of([] as Array)) + ); + } + + private createKeyFilter(query: string): (key: string) => boolean { + const lowercaseQuery = query.toLowerCase(); + return key => key.toLowerCase().startsWith(lowercaseQuery); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index 3f81632178..c5591106ad 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -106,6 +106,16 @@ import { import { DoughnutChartWidgetSettingsComponent } from '@home/components/widget/lib/settings/chart/doughnut-chart-widget-settings.component'; +import { SwitchRpcSettingsComponent } from '@home/components/widget/lib/settings/control/switch-rpc-settings.component'; +import { + RoundSwitchWidgetSettingsComponent +} from '@home/components/widget/lib/settings/control/round-switch-widget-settings.component'; +import { + SwitchControlWidgetSettingsComponent +} from '@home/components/widget/lib/settings/control/switch-control-widget-settings.component'; +import { + SlideToggleWidgetSettingsComponent +} from '@home/components/widget/lib/settings/control/slide-toggle-widget-settings.component'; @NgModule({ declarations: [ @@ -145,7 +155,11 @@ import { FlotPieWidgetSettingsComponent, FlotPieKeySettingsComponent, ChartWidgetSettingsComponent, - DoughnutChartWidgetSettingsComponent + DoughnutChartWidgetSettingsComponent, + SwitchRpcSettingsComponent, + RoundSwitchWidgetSettingsComponent, + SwitchControlWidgetSettingsComponent, + SlideToggleWidgetSettingsComponent ], imports: [ CommonModule, @@ -189,7 +203,11 @@ import { FlotPieWidgetSettingsComponent, FlotPieKeySettingsComponent, ChartWidgetSettingsComponent, - DoughnutChartWidgetSettingsComponent + DoughnutChartWidgetSettingsComponent, + SwitchRpcSettingsComponent, + RoundSwitchWidgetSettingsComponent, + SwitchControlWidgetSettingsComponent, + SlideToggleWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -222,5 +240,8 @@ export const widgetSettingsComponentsMap: {[key: string]: Type Date: Tue, 19 Apr 2022 23:02:33 +0200 Subject: [PATCH 053/276] Implemented isolated rule engine queue consumers --- .../server/actors/app/AppActor.java | 30 +--- .../actors/service/DefaultActorService.java | 2 +- .../server/actors/tenant/TenantActor.java | 24 +-- .../queue/DefaultTbClusterService.java | 101 +++++------ .../DefaultTbRuleEngineConsumerService.java | 79 +++++---- .../src/main/resources/thingsboard.yml | 1 - .../routing/HashPartitionServiceTest.java | 8 +- .../server/queue/TbQueueClusterService.java | 4 +- common/cluster-api/src/main/proto/queue.proto | 4 +- .../server/dao/queue/QueueService.java | 4 +- .../server/common/data/queue/Queue.java | 13 ++ .../tenant/profile/TenantProfileData.java | 5 + .../TenantProfileQueueConfiguration.java | 32 ++++ .../common/msg/queue/PartitionChangeMsg.java | 2 +- .../common/msg/queue/ServiceQueueKey.java | 14 +- .../DefaultTbServiceInfoProvider.java | 20 --- .../queue/discovery/HashPartitionService.java | 161 +++++++----------- .../server/queue/discovery/QueueKey.java | 56 ++++++ .../queue/discovery/QueueRoutingInfo.java | 9 + .../discovery/TbServiceInfoProvider.java | 5 - .../discovery/TopicPartitionInfoKey.java | 44 ----- .../discovery/event/PartitionChangeEvent.java | 10 +- .../server/dao/queue/BaseQueueService.java | 29 ++-- .../server/dao/tenant/TenantServiceImpl.java | 91 +++++++++- .../rule/engine/action/TbMsgCountNode.java | 1 - .../rule/engine/debug/TbMsgGeneratorNode.java | 1 - .../rule/engine/delay/TbMsgDelayNode.java | 1 - .../import-export/import-export.service.ts | 4 +- .../profile/tenant-profile.component.html | 62 +++---- .../profile/tenant-profile.component.ts | 13 +- ui-ngx/src/app/shared/models/tenant.model.ts | 1 + .../assets/locale/locale.constant-en_US.json | 3 +- 32 files changed, 437 insertions(+), 397 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileQueueConfiguration.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java delete mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicPartitionInfoKey.java diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java index 7c2e45477b..2817b1e359 100644 --- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java @@ -30,8 +30,6 @@ import org.thingsboard.server.actors.service.DefaultActorService; import org.thingsboard.server.actors.tenant.TenantActor; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.TenantProfile; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; @@ -45,24 +43,20 @@ import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.tenant.TenantService; -import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper; import java.util.HashSet; -import java.util.Optional; import java.util.Set; @Slf4j public class AppActor extends ContextAwareActor { - private final TbTenantProfileCache tenantProfileCache; private final TenantService tenantService; private final Set deletedTenants; private volatile boolean ruleChainsInitialized; private AppActor(ActorSystemContext systemContext) { super(systemContext); - this.tenantProfileCache = systemContext.getTenantProfileCache(); this.tenantService = systemContext.getTenantService(); this.deletedTenants = new HashSet<>(); } @@ -125,28 +119,12 @@ public class AppActor extends ContextAwareActor { private void initTenantActors() { log.info("Starting main system actor."); try { - // This Service may be started for specific tenant only. - Optional isolatedTenantId = systemContext.getServiceInfoProvider().getIsolatedTenant(); - if (isolatedTenantId.isPresent()) { - Tenant tenant = systemContext.getTenantService().findTenantById(isolatedTenantId.get()); - if (tenant != null) { - log.debug("[{}] Creating tenant actor", tenant.getId()); - getOrCreateTenantActor(tenant.getId()); - log.debug("Tenant actor created."); - } else { - log.error("[{}] Tenant with such ID does not exist", isolatedTenantId.get()); - } - } else if (systemContext.isTenantComponentsInitEnabled()) { + if (systemContext.isTenantComponentsInitEnabled()) { PageDataIterable tenantIterator = new PageDataIterable<>(tenantService::findTenants, ENTITY_PACK_LIMIT); - boolean isRuleEngine = systemContext.getServiceInfoProvider().isService(ServiceType.TB_RULE_ENGINE); - boolean isCore = systemContext.getServiceInfoProvider().isService(ServiceType.TB_CORE); for (Tenant tenant : tenantIterator) { - TenantProfile tenantProfile = tenantProfileCache.get(tenant.getTenantProfileId()); - if (isCore || (isRuleEngine && !tenantProfile.isIsolatedTbRuleEngine())) { - log.debug("[{}] Creating tenant actor", tenant.getId()); - getOrCreateTenantActor(tenant.getId()); - log.debug("[{}] Tenant actor created.", tenant.getId()); - } + log.debug("[{}] Creating tenant actor", tenant.getId()); + getOrCreateTenantActor(tenant.getId()); + log.debug("[{}] Tenant actor created.", tenant.getId()); } } log.info("Main system actor started."); diff --git a/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java index 2c7faf5226..0809afb4b5 100644 --- a/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java +++ b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java @@ -123,7 +123,7 @@ public class DefaultActorService extends TbApplicationEventListener isolatedTenantId = systemContext.getServiceInfoProvider().getIsolatedTenant(); - - TenantProfile tenantProfile = systemContext.getTenantProfileCache().get(tenant.getTenantProfileId()); - isCore = systemContext.getServiceInfoProvider().isService(ServiceType.TB_CORE); isRuleEngineForCurrentTenant = systemContext.getServiceInfoProvider().isService(ServiceType.TB_RULE_ENGINE); if (isRuleEngineForCurrentTenant) { try { - if (isolatedTenantId.map(id -> id.equals(tenantId)).orElseGet(() -> !tenantProfile.isIsolatedTbRuleEngine())) { - if (apiUsageState.isReExecEnabled()) { - log.info("[{}] Going to init rule chains", tenantId); - initRuleChains(); - } else { - log.info("[{}] Skip init of the rule chains due to API limits", tenantId); - } + if (apiUsageState.isReExecEnabled()) { + log.info("[{}] Going to init rule chains", tenantId); + initRuleChains(); } else { - isRuleEngineForCurrentTenant = false; + log.info("[{}] Skip init of the rule chains due to API limits", tenantId); } } catch (Exception e) { cantFindTenant = true; @@ -138,7 +124,7 @@ public class TenantActor extends RuleChainManagerActor { switch (msg.getMsgType()) { case PARTITION_CHANGE_MSG: PartitionChangeMsg partitionChangeMsg = (PartitionChangeMsg) msg; - ServiceType serviceType = partitionChangeMsg.getServiceQueueKey().getServiceType(); + ServiceType serviceType = partitionChangeMsg.getServiceType(); if (ServiceType.TB_RULE_ENGINE.equals(serviceType)) { //To Rule Chain Actors broadcast(msg); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 102dcbc32c..f1ce95f166 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -26,32 +26,33 @@ import org.springframework.stereotype.Service; import org.thingsboard.rule.engine.api.msg.DeviceEdgeUpdateMsg; import org.thingsboard.rule.engine.api.msg.DeviceNameOrTypeUpdateMsg; import org.thingsboard.server.cluster.TbClusterService; -import org.thingsboard.server.common.data.EdgeUtils; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; -import org.thingsboard.server.common.data.edge.EdgeEventType; -import org.thingsboard.server.common.data.id.QueueId; -import org.thingsboard.server.common.data.queue.Queue; -import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.FromDeviceRPCResponseProto; @@ -70,7 +71,6 @@ import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.service.gateway_device.GatewayNotificationsService; import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.profile.TbDeviceProfileCache; -import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import java.util.HashSet; import java.util.Set; @@ -490,7 +490,7 @@ public class DefaultTbClusterService implements TbClusterService { } @Override - public void onQueueChange(Queue queue, TbQueueCallback callback) { + public void onQueueChange(Queue queue) { log.trace("[{}][{}] Processing queue change [{}] event", queue.getTenantId(), queue.getId(), queue.getName()); TransportProtos.QueueUpdateMsg queueUpdateMsg = TransportProtos.QueueUpdateMsg.newBuilder() @@ -503,38 +503,14 @@ public class DefaultTbClusterService implements TbClusterService { .setPartitions(queue.getPartitions()) .build(); - if ("Main".equals(queue.getName())) { - Set tbTransportServices = partitionService.getAllServices(ServiceType.TB_TRANSPORT); - ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setQueueUpdateMsg(queueUpdateMsg).build(); - for (TransportProtos.ServiceInfo transportService : tbTransportServices) { - TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, transportService.getServiceId()); - producerProvider.getTransportNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), transportMsg), callback); - toTransportNfs.incrementAndGet(); - } - - Set tbCoreServices = partitionService.getAllServices(ServiceType.TB_CORE); - ToCoreNotificationMsg coreMsg = ToCoreNotificationMsg.newBuilder().setQueueUpdateMsg(queueUpdateMsg).build(); - for (TransportProtos.ServiceInfo coreService : tbCoreServices) { - TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, coreService.getServiceId()); - producerProvider.getTbCoreNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), coreMsg), callback); - toCoreNfs.incrementAndGet(); - } - } else { - Set tbRuleEngineServices = partitionService.getAllServices(ServiceType.TB_RULE_ENGINE); - ToRuleEngineNotificationMsg ruleEngineMsg = ToRuleEngineNotificationMsg.newBuilder().setQueueUpdateMsg(queueUpdateMsg).build(); - for (TransportProtos.ServiceInfo ruleEngineService : tbRuleEngineServices) { - TenantId tenantId = new TenantId(new UUID(ruleEngineService.getTenantIdMSB(), ruleEngineService.getTenantIdLSB())); - if (tenantId.equals(queue.getTenantId())) { - TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, ruleEngineService.getServiceId()); - producerProvider.getRuleEngineNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), ruleEngineMsg), callback); - toRuleEngineNfs.incrementAndGet(); - } - } - } + ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setQueueUpdateMsg(queueUpdateMsg).build(); + ToCoreNotificationMsg coreMsg = ToCoreNotificationMsg.newBuilder().setQueueUpdateMsg(queueUpdateMsg).build(); + ToRuleEngineNotificationMsg ruleEngineMsg = ToRuleEngineNotificationMsg.newBuilder().setQueueUpdateMsg(queueUpdateMsg).build(); + doSendQueueNotifications(transportMsg, coreMsg, ruleEngineMsg); } @Override - public void onQueueDelete(Queue queue, TbQueueCallback callback) { + public void onQueueDelete(Queue queue) { log.trace("[{}][{}] Processing queue delete [{}] event", queue.getTenantId(), queue.getId(), queue.getName()); TransportProtos.QueueDeleteMsg queueDeleteMsg = TransportProtos.QueueDeleteMsg.newBuilder() @@ -545,33 +521,32 @@ public class DefaultTbClusterService implements TbClusterService { .setQueueName(queue.getName()) .build(); - if ("Main".equals(queue.getName())) { - Set tbTransportServices = partitionService.getAllServices(ServiceType.TB_TRANSPORT); - ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setQueueDeleteMsg(queueDeleteMsg).build(); - for (TransportProtos.ServiceInfo transportService : tbTransportServices) { - TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, transportService.getServiceId()); - producerProvider.getTransportNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), transportMsg), callback); - toTransportNfs.incrementAndGet(); - } + ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setQueueDeleteMsg(queueDeleteMsg).build(); + ToCoreNotificationMsg coreMsg = ToCoreNotificationMsg.newBuilder().setQueueDeleteMsg(queueDeleteMsg).build(); + ToRuleEngineNotificationMsg ruleEngineMsg = ToRuleEngineNotificationMsg.newBuilder().setQueueDeleteMsg(queueDeleteMsg).build(); + doSendQueueNotifications(transportMsg, coreMsg, ruleEngineMsg); + } - Set tbCoreServices = partitionService.getAllServices(ServiceType.TB_CORE); - ToCoreNotificationMsg coreMsg = ToCoreNotificationMsg.newBuilder().setQueueDeleteMsg(queueDeleteMsg).build(); - for (TransportProtos.ServiceInfo coreService : tbCoreServices) { - TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, coreService.getServiceId()); - producerProvider.getTbCoreNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), coreMsg), callback); - toCoreNfs.incrementAndGet(); - } - } else { - Set tbRuleEngineServices = partitionService.getAllServices(ServiceType.TB_RULE_ENGINE); - ToRuleEngineNotificationMsg ruleEngineMsg = ToRuleEngineNotificationMsg.newBuilder().setQueueDeleteMsg(queueDeleteMsg).build(); - for (TransportProtos.ServiceInfo ruleEngineService : tbRuleEngineServices) { - TenantId tenantId = new TenantId(new UUID(ruleEngineService.getTenantIdMSB(), ruleEngineService.getTenantIdLSB())); - if (tenantId.equals(queue.getTenantId())) { - TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, ruleEngineService.getServiceId()); - producerProvider.getRuleEngineNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), ruleEngineMsg), callback); - toRuleEngineNfs.incrementAndGet(); - } - } + private void doSendQueueNotifications(ToTransportMsg transportMsg, ToCoreNotificationMsg coreMsg, ToRuleEngineNotificationMsg ruleEngineMsg) { + Set tbTransportServices = partitionService.getAllServices(ServiceType.TB_TRANSPORT); + for (TransportProtos.ServiceInfo transportService : tbTransportServices) { + TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, transportService.getServiceId()); + producerProvider.getTransportNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), transportMsg), null); + toTransportNfs.incrementAndGet(); + } + + Set tbCoreServices = partitionService.getAllServices(ServiceType.TB_CORE); + for (TransportProtos.ServiceInfo coreService : tbCoreServices) { + TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, coreService.getServiceId()); + producerProvider.getTbCoreNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), coreMsg), null); + toCoreNfs.incrementAndGet(); + } + + Set tbRuleEngineServices = partitionService.getAllServices(ServiceType.TB_RULE_ENGINE); + for (TransportProtos.ServiceInfo ruleEngineService : tbRuleEngineServices) { + TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, ruleEngineService.getServiceId()); + producerProvider.getRuleEngineNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), ruleEngineMsg), null); + toRuleEngineNfs.incrementAndGet(); } } } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java index 4f5974457a..3f8173b9ac 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java @@ -30,7 +30,6 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.RuleNodeInfo; -import org.thingsboard.server.common.msg.queue.ServiceQueue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbMsgCallback; @@ -46,6 +45,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotifica import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory; @@ -105,11 +105,11 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< private final PartitionService partitionService; private final TbServiceInfoProvider serviceInfoProvider; private final QueueService queueService; - private final TenantId tenantId; - private final ConcurrentMap>> consumers = new ConcurrentHashMap<>(); - private final ConcurrentMap consumerConfigurations = new ConcurrentHashMap<>(); - private final ConcurrentMap consumerStats = new ConcurrentHashMap<>(); - private final ConcurrentMap topicsConsumerPerPartition = new ConcurrentHashMap<>(); + // private final TenantId tenantId; + private final ConcurrentMap>> consumers = new ConcurrentHashMap<>(); + private final ConcurrentMap consumerConfigurations = new ConcurrentHashMap<>(); + private final ConcurrentMap consumerStats = new ConcurrentHashMap<>(); + private final ConcurrentMap topicsConsumerPerPartition = new ConcurrentHashMap<>(); final ExecutorService submitExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("tb-rule-engine-consumer-submit")); final ScheduledExecutorService repartitionExecutor = Executors.newScheduledThreadPool(1, ThingsBoardThreadFactory.forName("tb-rule-engine-consumer-repartition")); @@ -135,25 +135,26 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< this.partitionService = partitionService; this.serviceInfoProvider = serviceInfoProvider; this.queueService = queueService; - this.tenantId = actorContext.getServiceInfoProvider().getIsolatedTenant().orElse(TenantId.SYS_TENANT_ID); +// this.tenantId = actorContext.getServiceInfoProvider().getIsolatedTenant().orElse(TenantId.SYS_TENANT_ID); } @PostConstruct public void init() { super.init("tb-rule-engine-consumer", "tb-rule-engine-notifications-consumer"); - List queues = queueService.findQueuesByTenantId(tenantId); + List queues = queueService.findAllQueues(); for (Queue configuration : queues) { initConsumer(configuration); } } private void initConsumer(Queue configuration) { - consumerConfigurations.putIfAbsent(configuration.getName(), configuration); - consumerStats.putIfAbsent(configuration.getName(), new TbRuleEngineConsumerStats(configuration.getName(), statsFactory)); + QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, configuration); + consumerConfigurations.putIfAbsent(queueKey, configuration); + consumerStats.putIfAbsent(queueKey, new TbRuleEngineConsumerStats(configuration.getName(), statsFactory)); if (!configuration.isConsumerPerPartition()) { - consumers.computeIfAbsent(configuration.getName(), queueName -> tbRuleEngineQueueFactory.createToRuleEngineMsgConsumer(configuration)); + consumers.computeIfAbsent(queueKey, queueName -> tbRuleEngineQueueFactory.createToRuleEngineMsgConsumer(configuration)); } else { - topicsConsumerPerPartition.computeIfAbsent(configuration.getName(), TbTopicWithConsumerPerPartition::new); + topicsConsumerPerPartition.computeIfAbsent(queueKey, k -> new TbTopicWithConsumerPerPartition(k.getQueueName())); } } @@ -167,31 +168,31 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< @Override protected void onTbApplicationEvent(PartitionChangeEvent event) { if (event.getServiceType().equals(getServiceType())) { - ServiceQueue serviceQueue = event.getServiceQueueKey().getServiceQueue(); - log.info("[{}] Subscribing to partitions: {}", serviceQueue.getQueue(), event.getPartitions()); - if (!consumerConfigurations.get(serviceQueue.getQueue()).isConsumerPerPartition()) { - consumers.get(serviceQueue.getQueue()).subscribe(event.getPartitions()); + String serviceQueue = event.getQueueKey().getQueueName(); + log.info("[{}] Subscribing to partitions: {}", serviceQueue, event.getPartitions()); + if (!consumerConfigurations.get(event.getQueueKey()).isConsumerPerPartition()) { + consumers.get(event.getQueueKey()).subscribe(event.getPartitions()); } else { - log.info("[{}] Subscribing consumer per partition: {}", serviceQueue.getQueue(), event.getPartitions()); - subscribeConsumerPerPartition(serviceQueue.getQueue(), event.getPartitions()); + log.info("[{}] Subscribing consumer per partition: {}", serviceQueue, event.getPartitions()); + subscribeConsumerPerPartition(event.getQueueKey(), event.getPartitions()); } } } - void subscribeConsumerPerPartition(String queue, Set partitions) { + void subscribeConsumerPerPartition(QueueKey queue, Set partitions) { topicsConsumerPerPartition.get(queue).getSubscribeQueue().add(partitions); scheduleTopicRepartition(queue); } - private void scheduleTopicRepartition(String queue) { + private void scheduleTopicRepartition(QueueKey queue) { repartitionExecutor.schedule(() -> repartitionTopicWithConsumerPerPartition(queue), 1, TimeUnit.SECONDS); } - void repartitionTopicWithConsumerPerPartition(final String queueName) { + void repartitionTopicWithConsumerPerPartition(final QueueKey queueKey) { if (stopped) { return; } - TbTopicWithConsumerPerPartition tbTopicWithConsumerPerPartition = topicsConsumerPerPartition.get(queueName); + TbTopicWithConsumerPerPartition tbTopicWithConsumerPerPartition = topicsConsumerPerPartition.get(queueKey); java.util.Queue> subscribeQueue = tbTopicWithConsumerPerPartition.getSubscribeQueue(); if (subscribeQueue.isEmpty()) { return; @@ -216,15 +217,15 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< log.info("calculated removedPartitions {}", removedPartitions); removedPartitions.forEach((tpi) -> { - removeConsumerForTopicByTpi(queueName, consumers, tpi); + removeConsumerForTopicByTpi(queueKey.getQueueName(), consumers, tpi); }); addedPartitions.forEach((tpi) -> { - log.info("[{}] Adding consumer for topic: {}", queueName, tpi); - Queue configuration = consumerConfigurations.get(queueName); + log.info("[{}] Adding consumer for topic: {}", queueKey, tpi); + Queue configuration = consumerConfigurations.get(queueKey); TbQueueConsumer> consumer = tbRuleEngineQueueFactory.createToRuleEngineMsgConsumer(configuration); consumers.put(tpi, consumer); - launchConsumer(consumer, consumerConfigurations.get(queueName), consumerStats.get(queueName), "" + queueName + "-" + tpi.getPartition().orElse(-999999)); + launchConsumer(consumer, consumerConfigurations.get(queueKey), consumerStats.get(queueKey), "" + queueKey + "-" + tpi.getPartition().orElse(-999999)); consumer.subscribe(Collections.singleton(tpi)); }); @@ -232,7 +233,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< tbTopicWithConsumerPerPartition.getLock().unlock(); } } else { - scheduleTopicRepartition(queueName); //reschedule later + scheduleTopicRepartition(queueKey); //reschedule later } } @@ -245,7 +246,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< @Override protected void launchMainConsumers() { - consumers.forEach((queue, consumer) -> launchConsumer(consumer, consumerConfigurations.get(queue), consumerStats.get(queue), queue)); + consumers.forEach((queue, consumer) -> launchConsumer(consumer, consumerConfigurations.get(queue), consumerStats.get(queue), queue.getQueueName())); } @Override @@ -408,11 +409,14 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< private void updateQueue(TransportProtos.QueueUpdateMsg queueUpdateMsg) { String queueName = queueUpdateMsg.getQueueName(); - Queue queue = queueService.findQueueByTenantIdAndName(tenantId, queueName); - Queue oldQueue = consumerConfigurations.remove(queueName); + TenantId tenantId = new TenantId(new UUID(queueUpdateMsg.getTenantIdMSB(), queueUpdateMsg.getTenantIdLSB())); + QueueId queueId = new QueueId(new UUID(queueUpdateMsg.getQueueIdMSB(), queueUpdateMsg.getQueueIdLSB())); + QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueUpdateMsg.getQueueName(), tenantId); + Queue queue = queueService.findQueueById(tenantId, queueId); + Queue oldQueue = consumerConfigurations.remove(queueKey); if (oldQueue != null) { if (oldQueue.isConsumerPerPartition()) { - TbTopicWithConsumerPerPartition consumerPerPartition = topicsConsumerPerPartition.remove(queueName); + TbTopicWithConsumerPerPartition consumerPerPartition = topicsConsumerPerPartition.remove(queueKey); ReentrantLock lock = consumerPerPartition.getLock(); try { lock.lock(); @@ -421,7 +425,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< lock.unlock(); } } else { - TbQueueConsumer> consumer = consumers.remove(queueName); + TbQueueConsumer> consumer = consumers.remove(queueKey); consumer.unsubscribe(); } } @@ -429,7 +433,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< initConsumer(queue); if (!queue.isConsumerPerPartition()) { - launchConsumer(consumers.get(queueName), consumerConfigurations.get(queueName), consumerStats.get(queueName), queueName); + launchConsumer(consumers.get(queueKey), consumerConfigurations.get(queueKey), consumerStats.get(queueKey), queueName); } partitionService.updateQueue(queueUpdateMsg); @@ -437,16 +441,19 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< } private void deleteQueue(TransportProtos.QueueDeleteMsg queueDeleteMsg) { - Queue queue = consumerConfigurations.remove(queueDeleteMsg.getQueueName()); + TenantId tenantId = new TenantId(new UUID(queueDeleteMsg.getTenantIdMSB(), queueDeleteMsg.getTenantIdLSB())); + QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueDeleteMsg.getQueueName(), tenantId); + + Queue queue = consumerConfigurations.remove(queueKey); if (queue != null) { if (queue.isConsumerPerPartition()) { - TbTopicWithConsumerPerPartition tbTopicWithConsumerPerPartition = topicsConsumerPerPartition.remove(queueDeleteMsg.getQueueName()); + TbTopicWithConsumerPerPartition tbTopicWithConsumerPerPartition = topicsConsumerPerPartition.remove(queueKey); if (tbTopicWithConsumerPerPartition != null) { tbTopicWithConsumerPerPartition.getConsumers().values().forEach(TbQueueConsumer::unsubscribe); tbTopicWithConsumerPerPartition.getConsumers().clear(); } } else { - TbQueueConsumer> consumer = consumers.remove(queueDeleteMsg.getQueueName()); + TbQueueConsumer> consumer = consumers.remove(queueKey); if (consumer != null) { consumer.unsubscribe(); } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index eaa76d4273..658a403654 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1094,7 +1094,6 @@ service: type: "${TB_SERVICE_TYPE:monolith}" # monolith or tb-core or tb-rule-engine # Unique id for this service (autogenerated if empty) id: "${TB_SERVICE_ID:}" - tenant_id: "${TB_SERVICE_TENANT_ID:}" # empty or specific tenant id. metrics: # Enable/disable actuator metrics. diff --git a/application/src/test/java/org/thingsboard/server/service/cluster/routing/HashPartitionServiceTest.java b/application/src/test/java/org/thingsboard/server/service/cluster/routing/HashPartitionServiceTest.java index 0c13157623..c8fb678470 100644 --- a/application/src/test/java/org/thingsboard/server/service/cluster/routing/HashPartitionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cluster/routing/HashPartitionServiceTest.java @@ -74,8 +74,8 @@ public class HashPartitionServiceTest { ReflectionTestUtils.setField(clusterRoutingService, "hashFunctionName", hashFunctionName); TransportProtos.ServiceInfo currentServer = TransportProtos.ServiceInfo.newBuilder() .setServiceId("tb-core-0") - .setTenantIdMSB(TenantId.NULL_UUID.getMostSignificantBits()) - .setTenantIdLSB(TenantId.NULL_UUID.getLeastSignificantBits()) +// .setTenantIdMSB(TenantId.NULL_UUID.getMostSignificantBits()) +// .setTenantIdLSB(TenantId.NULL_UUID.getLeastSignificantBits()) .addAllServiceTypes(Collections.singletonList(ServiceType.TB_CORE.name())) .build(); // when(queueService.resolve(Mockito.any(), Mockito.anyString())).thenAnswer(i -> i.getArguments()[1]); @@ -84,8 +84,8 @@ public class HashPartitionServiceTest { for (int i = 1; i < SERVER_COUNT; i++) { otherServers.add(TransportProtos.ServiceInfo.newBuilder() .setServiceId("tb-rule-" + i) - .setTenantIdMSB(TenantId.NULL_UUID.getMostSignificantBits()) - .setTenantIdLSB(TenantId.NULL_UUID.getLeastSignificantBits()) +// .setTenantIdMSB(TenantId.NULL_UUID.getMostSignificantBits()) +// .setTenantIdLSB(TenantId.NULL_UUID.getLeastSignificantBits()) .addAllServiceTypes(Collections.singletonList(ServiceType.TB_CORE.name())) .build()); } diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueClusterService.java b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueClusterService.java index d37760211f..068304df1f 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueClusterService.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueClusterService.java @@ -18,7 +18,7 @@ package org.thingsboard.server.queue; import org.thingsboard.server.common.data.queue.Queue; public interface TbQueueClusterService { - void onQueueChange(Queue queue, TbQueueCallback callback); + void onQueueChange(Queue queue); - void onQueueDelete(Queue queue, TbQueueCallback callback); + void onQueueDelete(Queue queue); } diff --git a/common/cluster-api/src/main/proto/queue.proto b/common/cluster-api/src/main/proto/queue.proto index 8b4164e30f..32bcfd688d 100644 --- a/common/cluster-api/src/main/proto/queue.proto +++ b/common/cluster-api/src/main/proto/queue.proto @@ -32,8 +32,8 @@ option java_outer_classname = "TransportProtos"; message ServiceInfo { string serviceId = 1; repeated string serviceTypes = 2; - int64 tenantIdMSB = 3; - int64 tenantIdLSB = 4; +// int64 tenantIdMSB = 3; +// int64 tenantIdLSB = 4; // repeated QueueInfo ruleEngineQueues = 5; repeated string transports = 6; } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/queue/QueueService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/queue/QueueService.java index edb51fa2a3..3fbb228f3c 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/queue/QueueService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/queue/QueueService.java @@ -30,12 +30,12 @@ public interface QueueService { void deleteQueue(TenantId tenantId, QueueId queueId); + void deleteQueueByQueueName(TenantId tenantId, String queueName); + List findQueuesByTenantId(TenantId tenantId); PageData findQueuesByTenantId(TenantId tenantId, PageLink pageLink); - List findAllMainQueues(); - List findAllQueues(); Queue findQueueById(TenantId tenantId, QueueId queueId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java index 004bd0057d..90caa9c03f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; @Data public class Queue extends BaseData implements HasName, HasTenantId { @@ -40,4 +41,16 @@ public class Queue extends BaseData implements HasName, HasTenantId { public Queue(QueueId id) { super(id); } + + public Queue(TenantId tenantId, TenantProfileQueueConfiguration queueConfiguration) { + this.tenantId = tenantId; + this.name = queueConfiguration.getName(); + this.topic = queueConfiguration.getTopic(); + this.pollInterval = queueConfiguration.getPollInterval(); + this.partitions = queueConfiguration.getPartitions(); + this.consumerPerPartition = queueConfiguration.isConsumerPerPartition(); + this.packProcessingTimeout = queueConfiguration.getPackProcessingTimeout(); + this.submitStrategy = queueConfiguration.getSubmitStrategy(); + this.processingStrategy = queueConfiguration.getProcessingStrategy(); + } } \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileData.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileData.java index 8427a6ca78..78b6d9ae3c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileData.java @@ -19,6 +19,8 @@ import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; +import java.util.List; + @ApiModel @Data public class TenantProfileData { @@ -26,4 +28,7 @@ public class TenantProfileData { @ApiModelProperty(position = 1, value = "Complex JSON object that contains profile settings: max devices, max assets, rate limits, etc.") private TenantProfileConfiguration configuration; + @ApiModelProperty(position = 2, value = "JSON array of queue configuration per tenant profile") + private List queueConfiguration; + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileQueueConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileQueueConfiguration.java new file mode 100644 index 0000000000..8d3389af3a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileQueueConfiguration.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.common.data.tenant.profile; + +import lombok.Data; +import org.thingsboard.server.common.data.queue.ProcessingStrategy; +import org.thingsboard.server.common.data.queue.SubmitStrategy; + +@Data +public class TenantProfileQueueConfiguration { + private String name; + private String topic; + private int pollInterval; + private int partitions; + private boolean consumerPerPartition; + private long packProcessingTimeout; + private SubmitStrategy submitStrategy; + private ProcessingStrategy processingStrategy; +} diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/PartitionChangeMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/PartitionChangeMsg.java index 3a2fdbd187..4917444e2d 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/PartitionChangeMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/PartitionChangeMsg.java @@ -29,7 +29,7 @@ import java.util.Set; public final class PartitionChangeMsg implements TbActorMsg { @Getter - private final ServiceQueueKey serviceQueueKey; + private final ServiceType serviceType; @Getter private final Set partitions; diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/ServiceQueueKey.java b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/ServiceQueueKey.java index 1701221c0e..f47f5ab8b1 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/ServiceQueueKey.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/ServiceQueueKey.java @@ -17,7 +17,6 @@ package org.thingsboard.server.common.msg.queue; import lombok.Getter; import lombok.ToString; -import org.thingsboard.server.common.data.id.TenantId; import java.util.Objects; @@ -26,12 +25,8 @@ public class ServiceQueueKey { @Getter private final ServiceQueue serviceQueue; - @Getter - private final TenantId tenantId; - - public ServiceQueueKey(ServiceQueue serviceQueue, TenantId tenantId) { + public ServiceQueueKey(ServiceQueue serviceQueue) { this.serviceQueue = serviceQueue; - this.tenantId = tenantId; } @Override @@ -39,16 +34,15 @@ public class ServiceQueueKey { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ServiceQueueKey that = (ServiceQueueKey) o; - return serviceQueue.equals(that.serviceQueue) && - Objects.equals(tenantId, that.tenantId); + return serviceQueue.equals(that.serviceQueue); } @Override public int hashCode() { - return Objects.hash(serviceQueue, tenantId); + return Objects.hash(serviceQueue); } public ServiceType getServiceType() { return serviceQueue.getType(); } -} +} \ No newline at end of file diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java index 62e9852dd1..78ed1175ba 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java @@ -23,10 +23,8 @@ import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.TbTransportService; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.transport.TransportProtos.ServiceInfo; -import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; import org.thingsboard.server.queue.util.AfterContextReady; import javax.annotation.PostConstruct; @@ -36,8 +34,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Optional; -import java.util.UUID; import java.util.stream.Collectors; @Component @@ -56,14 +52,11 @@ public class DefaultTbServiceInfoProvider implements TbServiceInfoProvider { @Value("${service.tenant_id:}") private String tenantIdStr; - @Autowired(required = false) - private TbQueueRuleEngineSettings ruleEngineSettings; @Autowired private ApplicationContext applicationContext; private List serviceTypes; private ServiceInfo serviceInfo; - private TenantId isolatedTenant; @PostConstruct public void init() { @@ -83,15 +76,6 @@ public class DefaultTbServiceInfoProvider implements TbServiceInfoProvider { ServiceInfo.Builder builder = ServiceInfo.newBuilder() .setServiceId(serviceId) .addAllServiceTypes(serviceTypes.stream().map(ServiceType::name).collect(Collectors.toList())); - UUID tenantId; - if (!StringUtils.isEmpty(tenantIdStr)) { - tenantId = UUID.fromString(tenantIdStr); - isolatedTenant = TenantId.fromUUID(tenantId); - } else { - tenantId = TenantId.NULL_UUID; - } - builder.setTenantIdMSB(tenantId.getMostSignificantBits()); - builder.setTenantIdLSB(tenantId.getLeastSignificantBits()); serviceInfo = builder.build(); } @@ -119,8 +103,4 @@ public class DefaultTbServiceInfoProvider implements TbServiceInfoProvider { return serviceTypes.contains(serviceType); } - @Override - public Optional getIsolatedTenant() { - return Optional.ofNullable(isolatedTenant); - } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index ce7c93741a..861f7b657e 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -63,13 +63,15 @@ public class HashPartitionService implements PartitionService { private final TbServiceInfoProvider serviceInfoProvider; private final TenantRoutingInfoService tenantRoutingInfoService; private final QueueRoutingInfoService queueRoutingInfoService; - private final ConcurrentMap queueNames = new ConcurrentHashMap<>(); - private final ConcurrentMap> partitionTopicsMap = new ConcurrentHashMap<>(); - private final ConcurrentMap> partitionSizesMap = new ConcurrentHashMap<>(); - private final ConcurrentMap tenantRoutingInfoMap = new ConcurrentHashMap<>(); - private ConcurrentMap> myPartitions = new ConcurrentHashMap<>(); - private ConcurrentMap tpiCache = new ConcurrentHashMap<>(); + private final ConcurrentMap queuesById = new ConcurrentHashMap<>(); + + private ConcurrentMap> myPartitions = new ConcurrentHashMap<>(); + + private final ConcurrentMap partitionTopicsMap = new ConcurrentHashMap<>(); + private final ConcurrentMap partitionSizesMap = new ConcurrentHashMap<>(); + + private final ConcurrentMap tenantRoutingInfoMap = new ConcurrentHashMap<>(); private Map> tbTransportServicesByType = new HashMap<>(); private List currentOtherServices; @@ -93,8 +95,9 @@ public class HashPartitionService implements PartitionService { } private void partitionsInit() { - addPartitionSizeToMap(TenantId.SYS_TENANT_ID, new ServiceQueue(ServiceType.TB_CORE), corePartitions); - addPartitionTopicToMap(TenantId.SYS_TENANT_ID, new ServiceQueue(ServiceType.TB_CORE), coreTopic); + QueueKey coreKey = new QueueKey(ServiceType.TB_CORE); + partitionSizesMap.put(coreKey, corePartitions); + partitionTopicsMap.put(coreKey, coreTopic); List queueRoutingInfoList; @@ -127,69 +130,59 @@ public class HashPartitionService implements PartitionService { } queueRoutingInfoList.forEach(queue -> { - addPartitionTopicToMap(queue.getTenantId(), new ServiceQueue(ServiceType.TB_RULE_ENGINE, queue.getQueueName()), queue.getQueueTopic()); - addPartitionSizeToMap(queue.getTenantId(), new ServiceQueue(ServiceType.TB_RULE_ENGINE, queue.getQueueName()), queue.getPartitions()); - queueNames.put(queue.getQueueId(), queue.getQueueName()); + QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queue); + partitionTopicsMap.put(queueKey, queue.getQueueTopic()); + partitionSizesMap.put(queueKey, queue.getPartitions()); + queuesById.put(queue.getQueueId(), queue); }); } @Override public void updateQueue(TransportProtos.QueueUpdateMsg queueUpdateMsg) { TenantId tenantId = new TenantId(new UUID(queueUpdateMsg.getTenantIdMSB(), queueUpdateMsg.getTenantIdLSB())); - addPartitionTopicToMap(tenantId, new ServiceQueue(ServiceType.TB_RULE_ENGINE, queueUpdateMsg.getQueueName()), queueUpdateMsg.getQueueTopic()); - addPartitionSizeToMap(tenantId, new ServiceQueue(ServiceType.TB_RULE_ENGINE, queueUpdateMsg.getQueueName()), queueUpdateMsg.getPartitions()); - queueNames.putIfAbsent(new QueueId(new UUID(queueUpdateMsg.getQueueIdMSB(), queueUpdateMsg.getQueueIdLSB())), queueUpdateMsg.getQueueName()); - tpiCache.clear(); + QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueUpdateMsg.getQueueName(), tenantId); + partitionTopicsMap.put(queueKey, queueUpdateMsg.getQueueTopic()); + partitionSizesMap.put(queueKey, queueUpdateMsg.getPartitions()); + QueueRoutingInfo queue = new QueueRoutingInfo(queueUpdateMsg); + queuesById.put(queue.getQueueId(), queue); } @Override public void removeQueue(TransportProtos.QueueDeleteMsg queueDeleteMsg) { TenantId tenantId = new TenantId(new UUID(queueDeleteMsg.getTenantIdMSB(), queueDeleteMsg.getTenantIdLSB())); - ServiceQueue serviceQueue = new ServiceQueue(ServiceType.TB_RULE_ENGINE, queueDeleteMsg.getQueueName()); - partitionTopicsMap.get(tenantId).remove(serviceQueue); - partitionSizesMap.get(tenantId).remove(serviceQueue); - myPartitions.remove(new ServiceQueueKey(serviceQueue, tenantId)); - queueNames.remove(new QueueId(new UUID(queueDeleteMsg.getQueueIdMSB(), queueDeleteMsg.getQueueIdLSB()))); - tpiCache.clear(); - } - - private void addPartitionSizeToMap(TenantId tenantId, ServiceQueue serviceQueue, int partitions) { - partitionSizesMap.computeIfAbsent(tenantId, id -> new ConcurrentHashMap<>()).put(serviceQueue, partitions); - } - - private void addPartitionTopicToMap(TenantId tenantId, ServiceQueue serviceQueue, String topic) { - partitionTopicsMap.computeIfAbsent(tenantId, id -> new ConcurrentHashMap<>()).put(serviceQueue, topic); + QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueDeleteMsg.getQueueName(), tenantId); + partitionTopicsMap.remove(queueKey); + partitionSizesMap.remove(queueKey); + queuesById.remove(new QueueId(new UUID(queueDeleteMsg.getQueueIdMSB(), queueDeleteMsg.getQueueIdLSB()))); + myPartitions.remove(queueKey); } @Override public TopicPartitionInfo resolve(ServiceType serviceType, TenantId tenantId, EntityId entityId) { - return resolve(new ServiceQueue(serviceType), tenantId, entityId); + return resolve(serviceType, null, tenantId, entityId); } @Override public TopicPartitionInfo resolve(ServiceType serviceType, QueueId queueId, TenantId tenantId, EntityId entityId) { - String queueName; + QueueKey queueKey; if (queueId == null) { - queueName = ServiceQueue.MAIN; + queueKey = isIsolated(serviceType, tenantId) ? new QueueKey(serviceType, tenantId) : new QueueKey(serviceType); } else { - queueName = queueNames.get(queueId); + queueKey = new QueueKey(serviceType, queuesById.get(queueId)); } - ServiceQueue serviceQueue = new ServiceQueue(serviceType, queueName); - return resolve(serviceQueue, tenantId, entityId); + return resolve(queueKey, entityId); } - private TopicPartitionInfo resolve(ServiceQueue serviceQueue, TenantId tenantId, EntityId entityId) { + private TopicPartitionInfo resolve(QueueKey queueKey, EntityId entityId) { int hash = hashFunction.newHasher() .putLong(entityId.getId().getMostSignificantBits()) .putLong(entityId.getId().getLeastSignificantBits()).hash().asInt(); - boolean isolatedTenant = isIsolated(serviceQueue.getType(), tenantId); - Integer partitionSize = partitionSizesMap.get(isolatedTenant ? tenantId : TenantId.SYS_TENANT_ID).get(serviceQueue); + Integer partitionSize = partitionSizesMap.get(queueKey); int partition = Math.abs(hash % partitionSize); - TopicPartitionInfoKey cacheKey = new TopicPartitionInfoKey(serviceQueue, isolatedTenant ? tenantId : null, partition); - return tpiCache.computeIfAbsent(cacheKey, key -> buildTopicPartitionInfo(serviceQueue, tenantId, partition)); + return buildTopicPartitionInfo(queueKey, partition); } @Override @@ -197,43 +190,39 @@ public class HashPartitionService implements PartitionService { tbTransportServicesByType.clear(); logServiceInfo(currentService); otherServices.forEach(this::logServiceInfo); - Map> queueServicesMap = new HashMap<>(); + + Map> queueServicesMap = new HashMap<>(); addNode(queueServicesMap, currentService); for (ServiceInfo other : otherServices) { addNode(queueServicesMap, other); } queueServicesMap.values().forEach(list -> list.sort(Comparator.comparing(ServiceInfo::getServiceId))); - ConcurrentMap> oldPartitions = myPartitions; - TenantId myIsolatedOrSystemTenantId = getSystemOrIsolatedTenantId(currentService); + ConcurrentMap> oldPartitions = myPartitions; myPartitions = new ConcurrentHashMap<>(); - partitionSizesMap.get(myIsolatedOrSystemTenantId).forEach((serviceQueue, size) -> { - ServiceQueueKey myServiceQueueKey = new ServiceQueueKey(serviceQueue, myIsolatedOrSystemTenantId); + partitionSizesMap.forEach((queueKey, size) -> { for (int i = 0; i < size; i++) { - ServiceInfo serviceInfo = resolveByPartitionIdx(queueServicesMap.get(myServiceQueueKey), i); + ServiceInfo serviceInfo = resolveByPartitionIdx(queueServicesMap.get(queueKey), i); if (currentService.equals(serviceInfo)) { - ServiceQueueKey serviceQueueKey = new ServiceQueueKey(serviceQueue, getSystemOrIsolatedTenantId(serviceInfo)); - myPartitions.computeIfAbsent(serviceQueueKey, key -> new ArrayList<>()).add(i); + myPartitions.computeIfAbsent(queueKey, key -> new ArrayList<>()).add(i); } } }); - tpiCache.clear(); - - oldPartitions.forEach((serviceQueueKey, partitions) -> { - if (!myPartitions.containsKey(serviceQueueKey)) { - log.info("[{}] NO MORE PARTITIONS FOR CURRENT KEY", serviceQueueKey); - applicationEventPublisher.publishEvent(new PartitionChangeEvent(this, serviceQueueKey, Collections.emptySet())); + oldPartitions.forEach((queueKey, partitions) -> { + if (!myPartitions.containsKey(queueKey)) { + log.info("[{}] NO MORE PARTITIONS FOR CURRENT KEY", queueKey); + applicationEventPublisher.publishEvent(new PartitionChangeEvent(this, queueKey, Collections.emptySet())); } }); - myPartitions.forEach((serviceQueueKey, partitions) -> { - if (!partitions.equals(oldPartitions.get(serviceQueueKey))) { - log.info("[{}] NEW PARTITIONS: {}", serviceQueueKey, partitions); + myPartitions.forEach((queueKey, partitions) -> { + if (!partitions.equals(oldPartitions.get(queueKey))) { + log.info("[{}] NEW PARTITIONS: {}", queueKey, partitions); Set tpiList = partitions.stream() - .map(partition -> buildTopicPartitionInfo(serviceQueueKey, partition)) + .map(partition -> buildTopicPartitionInfo(queueKey, partition)) .collect(Collectors.toSet()); - applicationEventPublisher.publishEvent(new PartitionChangeEvent(this, serviceQueueKey, tpiList)); + applicationEventPublisher.publishEvent(new PartitionChangeEvent(this, queueKey, tpiList)); } }); @@ -313,7 +302,7 @@ public class HashPartitionService implements PartitionService { // currentMap.computeIfAbsent(serviceQueueKey, key -> new ArrayList<>()).add(serviceInfo); // } } else { - ServiceQueueKey serviceQueueKey = new ServiceQueueKey(new ServiceQueue(serviceType), getSystemOrIsolatedTenantId(serviceInfo)); + ServiceQueueKey serviceQueueKey = new ServiceQueueKey(new ServiceQueue(serviceType)); currentMap.computeIfAbsent(serviceQueueKey, key -> new ArrayList<>()).add(serviceInfo); } } @@ -321,24 +310,13 @@ public class HashPartitionService implements PartitionService { return currentMap; } - private TopicPartitionInfo buildTopicPartitionInfo(ServiceQueueKey serviceQueueKey, int partition) { - return buildTopicPartitionInfo(serviceQueueKey.getServiceQueue(), serviceQueueKey.getTenantId(), partition); - } - - private TopicPartitionInfo buildTopicPartitionInfo(ServiceQueue serviceQueue, TenantId tenantId, int partition) { - boolean isolatedTenant = isIsolated(serviceQueue.getType(), tenantId); - + private TopicPartitionInfo buildTopicPartitionInfo(QueueKey queueKey, int partition) { TopicPartitionInfo.TopicPartitionInfoBuilder tpi = TopicPartitionInfo.builder(); - tpi.topic(partitionTopicsMap.get(isolatedTenant ? tenantId : TenantId.SYS_TENANT_ID).get(serviceQueue)); + tpi.topic(partitionTopicsMap.get(queueKey)); tpi.partition(partition); - ServiceQueueKey myPartitionsSearchKey; - if (isolatedTenant) { - tpi.tenantId(tenantId); - myPartitionsSearchKey = new ServiceQueueKey(serviceQueue, tenantId); - } else { - myPartitionsSearchKey = new ServiceQueueKey(serviceQueue, TenantId.SYS_TENANT_ID); - } - List partitions = myPartitions.get(myPartitionsSearchKey); + tpi.tenantId(queueKey.getTenantId()); + + List partitions = myPartitions.get(queueKey); if (partitions != null) { tpi.myPartition(partitions.contains(partition)); } else { @@ -374,39 +352,24 @@ public class HashPartitionService implements PartitionService { } } - private TenantId getSystemIsolatedTenantId(ServiceType serviceType, TenantId tenantId) { - return isIsolated(serviceType, tenantId) ? tenantId : TenantId.SYS_TENANT_ID; - } - private void logServiceInfo(TransportProtos.ServiceInfo server) { - TenantId tenantId = getSystemOrIsolatedTenantId(server); - if (tenantId.isNullUid()) { - log.info("[{}] Found common server: [{}]", server.getServiceId(), server.getServiceTypesList()); - } else { - log.info("[{}][{}] Found specific server: [{}]", server.getServiceId(), tenantId, server.getServiceTypesList()); - } + log.info("[{}] Found common server: [{}]", server.getServiceId(), server.getServiceTypesList()); } - private TenantId getSystemOrIsolatedTenantId(TransportProtos.ServiceInfo serviceInfo) { - return TenantId.fromUUID(new UUID(serviceInfo.getTenantIdMSB(), serviceInfo.getTenantIdLSB())); - } - - private void addNode(Map> queueServiceList, ServiceInfo instance) { - TenantId tenantId = getSystemOrIsolatedTenantId(instance); + private void addNode(Map> queueServiceList, ServiceInfo instance) { for (String serviceTypeStr : instance.getServiceTypesList()) { ServiceType serviceType = ServiceType.valueOf(serviceTypeStr.toUpperCase()); if (ServiceType.TB_RULE_ENGINE.equals(serviceType)) { - partitionTopicsMap.get(tenantId).forEach((serviceQueue, topic) -> { - if (serviceQueue.getType().equals(ServiceType.TB_RULE_ENGINE)) { - ServiceQueueKey serviceQueueKey = new ServiceQueueKey(serviceQueue, tenantId); - queueServiceList.computeIfAbsent(serviceQueueKey, key -> new ArrayList<>()).add(instance); + partitionTopicsMap.keySet().forEach(key -> { + if (key.getType().equals(ServiceType.TB_RULE_ENGINE)) { + queueServiceList.computeIfAbsent(key, k -> new ArrayList<>()).add(instance); } }); - } else { - ServiceQueueKey serviceQueueKey = new ServiceQueueKey(new ServiceQueue(serviceType), tenantId); - queueServiceList.computeIfAbsent(serviceQueueKey, key -> new ArrayList<>()).add(instance); + } else if (ServiceType.TB_CORE.equals(serviceType)) { + queueServiceList.computeIfAbsent(new QueueKey(serviceType), key -> new ArrayList<>()).add(instance); } } + for (String transportType : instance.getTransportsList()) { tbTransportServicesByType.computeIfAbsent(transportType, t -> new ArrayList<>()).add(instance); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java new file mode 100644 index 0000000000..a2d5982421 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.queue.discovery; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.common.msg.queue.ServiceType; + +@Data +@AllArgsConstructor +public class QueueKey { + private static final String MAIN = "Main"; + + private final ServiceType type; + private final String queueName; + private final TenantId tenantId; + + public QueueKey(ServiceType type, Queue queue) { + this.type = type; + this.queueName = queue.getName(); + this.tenantId = queue.getTenantId(); + } + + public QueueKey(ServiceType type, QueueRoutingInfo queueRoutingInfo) { + this.type = type; + this.queueName = queueRoutingInfo.getQueueName(); + this.tenantId = queueRoutingInfo.getTenantId(); + } + + public QueueKey(ServiceType type, TenantId tenantId) { + this.type = type; + this.queueName = MAIN; + this.tenantId = tenantId != null ? tenantId : TenantId.SYS_TENANT_ID; + } + + public QueueKey(ServiceType type) { + this.type = type; + this.queueName = MAIN; + this.tenantId = TenantId.SYS_TENANT_ID; + } +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueRoutingInfo.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueRoutingInfo.java index 111ca5626a..7e80ebc048 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueRoutingInfo.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueRoutingInfo.java @@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.gen.transport.TransportProtos.GetQueueRoutingInfoResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.QueueUpdateMsg; import java.util.UUID; @@ -48,4 +49,12 @@ public class QueueRoutingInfo { this.queueTopic = routingInfo.getQueueTopic(); this.partitions = routingInfo.getPartitions(); } + + public QueueRoutingInfo(QueueUpdateMsg queueUpdateMsg) { + this.tenantId = new TenantId(new UUID(queueUpdateMsg.getTenantIdMSB(), queueUpdateMsg.getQueueIdLSB())); + this.queueId = new QueueId(new UUID(queueUpdateMsg.getQueueIdMSB(), queueUpdateMsg.getQueueIdLSB())); + this.queueName = queueUpdateMsg.getQueueName(); + this.queueTopic = queueUpdateMsg.getQueueTopic(); + this.partitions = queueUpdateMsg.getPartitions(); + } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbServiceInfoProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbServiceInfoProvider.java index 8e73c23c7e..2e88e34425 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbServiceInfoProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbServiceInfoProvider.java @@ -15,12 +15,9 @@ */ package org.thingsboard.server.queue.discovery; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.transport.TransportProtos.ServiceInfo; -import java.util.Optional; - public interface TbServiceInfoProvider { String getServiceId(); @@ -31,6 +28,4 @@ public interface TbServiceInfoProvider { boolean isService(ServiceType serviceType); - Optional getIsolatedTenant(); - } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicPartitionInfoKey.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicPartitionInfoKey.java deleted file mode 100644 index 86cddf2119..0000000000 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicPartitionInfoKey.java +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.queue.discovery; - -import lombok.AllArgsConstructor; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.msg.queue.ServiceQueue; - -import java.util.Objects; - -@AllArgsConstructor -public class TopicPartitionInfoKey { - private ServiceQueue serviceQueue; - private TenantId isolatedTenantId; - private int partition; - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - TopicPartitionInfoKey that = (TopicPartitionInfoKey) o; - return partition == that.partition && - serviceQueue.equals(that.serviceQueue) && - Objects.equals(isolatedTenantId, that.isolatedTenantId); - } - - @Override - public int hashCode() { - return Objects.hash(serviceQueue, isolatedTenantId, partition); - } -} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java index 342f7596eb..a6335fb4da 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java @@ -17,9 +17,9 @@ package org.thingsboard.server.queue.discovery.event; import lombok.Getter; import lombok.ToString; -import org.thingsboard.server.common.msg.queue.ServiceQueueKey; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.queue.discovery.QueueKey; import java.util.Set; @@ -29,17 +29,17 @@ public class PartitionChangeEvent extends TbApplicationEvent { private static final long serialVersionUID = -8731788167026510559L; @Getter - private final ServiceQueueKey serviceQueueKey; + private final QueueKey queueKey; @Getter private final Set partitions; - public PartitionChangeEvent(Object source, ServiceQueueKey serviceQueueKey, Set partitions) { + public PartitionChangeEvent(Object source, QueueKey queueKey, Set partitions) { super(source); - this.serviceQueueKey = serviceQueueKey; + this.queueKey = queueKey; this.partitions = partitions; } public ServiceType getServiceType() { - return serviceQueueKey.getServiceQueue().getType(); + return queueKey.getType(); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java index e2fd459189..28c0330267 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java @@ -87,7 +87,7 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ } if (queueClusterService != null) { - queueClusterService.onQueueChange(createdQueue, null); + queueClusterService.onQueueChange(createdQueue); } return createdQueue; @@ -107,12 +107,12 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ tbQueueAdmin.createTopicIfNotExists(new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName()); } if (queueClusterService != null) { - queueClusterService.onQueueChange(updatedQueue, null); + queueClusterService.onQueueChange(updatedQueue); } } else { log.info("Removed [{}] partitions from [{}] queue", oldPartitions - currentPartitions, queue.getName()); if (queueClusterService != null) { - queueClusterService.onQueueChange(updatedQueue, null); + queueClusterService.onQueueChange(updatedQueue); } await(); for (int i = currentPartitions; i < oldPartitions; i++) { @@ -120,7 +120,7 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ } } } else if (!oldQueue.equals(queue) && queueClusterService != null) { - queueClusterService.onQueueChange(updatedQueue, null); + queueClusterService.onQueueChange(updatedQueue); } return updatedQueue; @@ -130,12 +130,23 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ public void deleteQueue(TenantId tenantId, QueueId queueId) { log.trace("Executing deleteQueue, queueId: [{}]", queueId); Queue queue = findQueueById(tenantId, queueId); + doDelete(tenantId, queue); + } + + @Override + public void deleteQueueByQueueName(TenantId tenantId, String queueName) { + log.trace("Executing deleteQueueByQueueName, name: [{}]", queueName); + Queue queue = findQueueByTenantIdAndName(tenantId, queueName); + doDelete(tenantId, queue); + } + + private void doDelete(TenantId tenantId, Queue queue) { if (queueClusterService != null) { - queueClusterService.onQueueDelete(queue, null); + queueClusterService.onQueueDelete(queue); await(); } // queueStatsService.deleteQueueStatsByQueueId(tenantId, queueId); - boolean result = queueDao.removeById(tenantId, queueId.getId()); + boolean result = queueDao.removeById(tenantId, queue.getUuidId()); if (result && tbQueueAdmin != null) { for (int i = 0; i < queue.getPartitions(); i++) { String fullTopicName = new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName(); @@ -167,12 +178,6 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ return queueDao.findQueuesByTenantId(getSystemOrIsolatedTenantId(tenantId), pageLink); } - @Override - public List findAllMainQueues() { - log.trace("Executing findAllMainQueues"); - return queueDao.findAllMainQueues(); - } - @Override public List findAllQueues() { log.trace("Executing findAllQueues"); diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index b8d0f1f84b..4b935ca700 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -24,10 +24,11 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.TenantProfile; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; @@ -48,6 +49,12 @@ import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetsBundleService; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + import static org.thingsboard.server.dao.service.Validator.validateId; @Service @@ -138,18 +145,90 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe tenant.setTenantProfileId(tenantProfile.getId()); } tenantValidator.validate(tenant, Tenant::getId); + + Tenant oldTenant = tenant.getId() != null ? tenantDao.findById(tenant.getId(), tenant.getUuidId()) : null; + Tenant savedTenant = tenantDao.save(tenant.getId(), tenant); if (tenant.getId() == null) { deviceProfileService.createDefaultDeviceProfile(savedTenant.getId()); apiUsageStateService.createDefaultApiUsageState(savedTenant.getId(), null); - TenantProfile tenantProfile = tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, savedTenant.getTenantProfileId()); - if(tenantProfile.isIsolatedTbRuleEngine()) { - queueService.createDefaultMainQueue(tenantProfile, savedTenant.getTenantId()); - } } + + updateQueuesForTenant(oldTenant, savedTenant); + return savedTenant; } + private void updateQueuesForTenant(Tenant oldTenant, Tenant newTenant) { + TenantProfile oldTenantProfile = oldTenant != null ? tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, oldTenant.getTenantProfileId()) : null; + TenantProfile newTenantProfile = tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, newTenant.getTenantProfileId()); + + TenantId tenantId = newTenant.getId(); + + boolean oldIsolated = oldTenantProfile != null && oldTenantProfile.isIsolatedTbRuleEngine(); + boolean newIsolated = newTenantProfile.isIsolatedTbRuleEngine(); + + if (!oldIsolated && !newIsolated) { + return; + } + + if (newTenantProfile.equals(oldTenantProfile)) { + return; + } + + Map oldQueues; + Map newQueues; + + if (oldIsolated) { + oldQueues = oldTenantProfile.getProfileData().getQueueConfiguration().stream() + .collect(Collectors.toMap(TenantProfileQueueConfiguration::getName, q -> q)); + } else { + oldQueues = Collections.emptyMap(); + } + + if (newIsolated) { + newQueues = newTenantProfile.getProfileData().getQueueConfiguration().stream() + .collect(Collectors.toMap(TenantProfileQueueConfiguration::getName, q -> q)); + } else { + newQueues = Collections.emptyMap(); + } + + List toRemove = new ArrayList<>(); + List toCreate = new ArrayList<>(); + List toUpdate = new ArrayList<>(); + + for (String oldQueue : oldQueues.keySet()) { + if (!newQueues.containsKey(oldQueue)) { + toRemove.add(oldQueue); + } + } + + for (String newQueue : newQueues.keySet()) { + if (oldQueues.containsKey(newQueue)) { + toUpdate.add(newQueue); + } else { + toCreate.add(newQueue); + } + } + + toRemove.forEach(q -> queueService.deleteQueueByQueueName(tenantId, q)); + + toCreate.forEach(key -> queueService.saveQueue(new Queue(tenantId, newQueues.get(key)))); + + toUpdate.forEach(key -> { + Queue queueToUpdate = new Queue(tenantId, newQueues.get(key)); + Queue foundQueue = queueService.findQueueByTenantIdAndName(tenantId, key); + queueToUpdate.setId(foundQueue.getId()); + queueToUpdate.setCreatedTime(foundQueue.getCreatedTime()); + + if (queueToUpdate.equals(foundQueue)) { + //Queue not changed + } else { + queueService.saveQueue(queueToUpdate); + } + }); + } + @Override public void deleteTenant(TenantId tenantId) { log.trace("Executing deleteTenant [{}]", tenantId); @@ -218,7 +297,7 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe private void validateTenantProfile(TenantId tenantId, Tenant tenant) { TenantProfile tenantProfileById = tenantProfileService.findTenantProfileById(tenantId, tenant.getTenantProfileId()); if (!zkEnabled && (tenantProfileById.isIsolatedTbCore() || tenantProfileById.isIsolatedTbRuleEngine())) { - throw new DataValidationException("Can't use isolated tenant profiles in monolith setup!"); +// throw new DataValidationException("Can't use isolated tenant profiles in monolith setup!"); } } }; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java index 4a29787762..f08b9c06a0 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java @@ -27,7 +27,6 @@ import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.common.msg.queue.ServiceQueue; import org.thingsboard.server.common.msg.session.SessionMsgType; import java.util.UUID; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java index f8b4d64b42..1a26cf97ef 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java @@ -34,7 +34,6 @@ import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; -import org.thingsboard.server.common.msg.queue.ServiceQueue; import java.util.UUID; import java.util.concurrent.TimeUnit; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java index 464acadb94..5a313da469 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java @@ -26,7 +26,6 @@ import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.common.msg.queue.ServiceQueue; import java.util.HashMap; import java.util.Map; diff --git a/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts b/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts index 2bddd229bc..78507cb121 100644 --- a/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts +++ b/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts @@ -567,7 +567,9 @@ export class ImportExportService { return isDefined(tenantProfile.name) && isDefined(tenantProfile.profileData) && isDefined(tenantProfile.isolatedTbCore) - && isDefined(tenantProfile.isolatedTbRuleEngine); + && isDefined(tenantProfile.isolatedTbRuleEngine) + && isDefined(tenantProfile.maxNumberOfQueues) + && isDefined(tenantProfile.maxNumberOfPartitionsPerQueue); } private sumObject(obj1: any, obj2: any): any { diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html index 03ff1ff921..6377e59a45 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html @@ -67,36 +67,38 @@
{{ 'tenant.isolated-tb-rule-engine' | translate }}
{{'tenant.isolated-tb-rule-engine-details' | translate}}
- - tenant.max-number-of-queues - - - {{ 'tenant.max-number-of-queues-required' | translate }} - - - {{ 'tenant.max-number-of-queues-min-length' | translate }} - - - - tenant.max-number-of-partitions-per-queue - - - {{ 'tenant.max-number-of-partitions-per-queue-required' | translate }} - - - {{ 'tenant.max-number-of-partitions-per-queue-min-length' | translate }} - - +
+ + tenant.max-number-of-queues + + + {{ 'tenant.max-number-of-queues-required' | translate }} + + + {{ 'tenant.max-number-of-queues-min-length' | translate }} + + + + tenant.max-number-of-partitions-per-queue + + + {{ 'tenant.max-number-of-partitions-per-queue-required' | translate }} + + + {{ 'tenant.max-number-of-partitions-per-queue-min-length' | translate }} + + +
{ super(store, fb, entityValue, entitiesTableConfigValue, cd); } + ngOnInit() { + this.showQueueParams(); + this.entityForm.get('isolatedTbRuleEngine').valueChanges.subscribe(() => this.showQueueParams()); + } + hideDelete() { if (this.entitiesTableConfig) { return !this.entitiesTableConfig.deleteEnabled(this.entity); @@ -87,11 +92,11 @@ export class TenantProfileComponent extends EntityComponent { showQueueParams(): boolean { let isolatedTbRuleEngine: boolean = this.entityForm.get('isolatedTbRuleEngine').value; if (isolatedTbRuleEngine) { - this.entityForm.controls['maxNumberOfQueues'].enable(); - this.entityForm.controls['maxNumberOfPartitionsPerQueue'].enable(); + this.entityForm.get('maxNumberOfQueues').enable(); + this.entityForm.get('maxNumberOfPartitionsPerQueue').enable(); } else { - this.entityForm.controls['maxNumberOfQueues'].disable(); - this.entityForm.controls['maxNumberOfPartitionsPerQueue'].disable(); + this.entityForm.get('maxNumberOfQueues').disable(); + this.entityForm.get('maxNumberOfPartitionsPerQueue').disable(); } return isolatedTbRuleEngine; } diff --git a/ui-ngx/src/app/shared/models/tenant.model.ts b/ui-ngx/src/app/shared/models/tenant.model.ts index dc3c5200d3..311529a42a 100644 --- a/ui-ngx/src/app/shared/models/tenant.model.ts +++ b/ui-ngx/src/app/shared/models/tenant.model.ts @@ -98,6 +98,7 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan export interface TenantProfileData { configuration: TenantProfileConfiguration; + queueConfiguration?: any; } export interface TenantProfile extends BaseData { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 910632d072..4c93854125 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -259,7 +259,8 @@ "queues": "Queues", "queue-partitions": "Partitions", "queue-submit-strategy": "Submit strategy", - "queue-processing-strategy": "Processing strategy" + "queue-processing-strategy": "Processing strategy", + "queue-configuration": "Queue configuration" }, "alarm": { "alarm": "Alarm", From 9b19da096f6b832567b2e9aeec9b94da2e882c23 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 21 Apr 2022 09:51:25 +0200 Subject: [PATCH 054/276] removed max queues and max partitions per queue in tenant profile --- .../server/controller/QueueController.java | 14 +-- .../server/dao/queue/QueueService.java | 3 - .../server/common/data/TenantProfile.java | 6 +- .../server/dao/model/ModelConstants.java | 2 - .../dao/model/sql/TenantProfileEntity.java | 10 --- .../server/dao/queue/BaseQueueService.java | 37 -------- .../dao/tenant/TenantProfileServiceImpl.java | 85 +++++++++++++++++++ .../main/resources/sql/schema-entities.sql | 2 - .../dao/service/BaseQueueServiceTest.java | 49 ++++++----- .../service/BaseTenantProfileServiceTest.java | 28 ++++++ ui-ngx/src/app/core/http/queue.service.ts | 10 +-- .../import-export/import-export.service.ts | 4 +- .../tenant-profile-data.component.html | 5 ++ .../profile/tenant-profile.component.html | 32 ------- .../profile/tenant-profile.component.ts | 12 +-- ui-ngx/src/app/shared/models/tenant.model.ts | 2 - .../assets/locale/locale.constant-en_US.json | 8 +- 17 files changed, 159 insertions(+), 150 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/QueueController.java b/application/src/main/java/org/thingsboard/server/controller/QueueController.java index c48fc8036f..dd0e023152 100644 --- a/application/src/main/java/org/thingsboard/server/controller/QueueController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QueueController.java @@ -55,7 +55,7 @@ public class QueueController extends BaseController { @ApiOperation(value = "Get queue names (getTenantQueuesByServiceType)", notes = "Returns a set of unique queue names based on service type. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/queues", params = {"serviceType"}, produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET) + @RequestMapping(value = "/queues", params = {"serviceType"}, produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET) @ResponseBody() public Set getTenantQueuesByServiceType(@ApiParam(value = QUEUE_SERVICE_TYPE_DESCRIPTION, allowableValues = QUEUE_SERVICE_TYPE_ALLOWABLE_VALUES) @RequestParam String serviceType) throws ThingsboardException { @@ -74,7 +74,7 @@ public class QueueController extends BaseController { } @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/tenant/queues", params = {"serviceType", "pageSize", "page"}, method = RequestMethod.GET) + @RequestMapping(value = "/queues", params = {"serviceType", "pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getTenantQueuesByServiceType(@RequestParam String serviceType, @RequestParam int pageSize, @@ -98,7 +98,7 @@ public class QueueController extends BaseController { } @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/tenant/queues/{queueId}", method = RequestMethod.GET) + @RequestMapping(value = "/queues/{queueId}", method = RequestMethod.GET) @ResponseBody public Queue getQueueById(@PathVariable("queueId") String queueIdStr) throws ThingsboardException { checkParameter("queueId", queueIdStr); @@ -111,8 +111,8 @@ public class QueueController extends BaseController { } } - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/tenant/queues", params = {"serviceType"}, method = RequestMethod.POST) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @RequestMapping(value = "/queues", params = {"serviceType"}, method = RequestMethod.POST) @ResponseBody public Queue saveQueue(@RequestBody Queue queue, @RequestParam String serviceType) throws ThingsboardException { @@ -137,8 +137,8 @@ public class QueueController extends BaseController { } } - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/tenant/queues/{queueId}", method = RequestMethod.DELETE) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @RequestMapping(value = "/queues/{queueId}", method = RequestMethod.DELETE) @ResponseBody public void deleteQueue(@PathVariable("queueId") String queueIdStr) throws ThingsboardException { checkParameter("queueId", queueIdStr); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/queue/QueueService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/queue/QueueService.java index 3fbb228f3c..bdea0ef09c 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/queue/QueueService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/queue/QueueService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.queue; -import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -42,7 +41,5 @@ public interface QueueService { Queue findQueueByTenantIdAndName(TenantId tenantId, String name); - Queue createDefaultMainQueue(TenantProfile tenantProfile, TenantId tenantId); - void deleteQueuesByTenantId(TenantId tenantId); } \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java index 32b6ba88ba..308500a6bd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java @@ -57,11 +57,7 @@ public class TenantProfile extends SearchTextBased implements H @ApiModelProperty(position = 7, value = "If enabled, will push all messages related to this tenant and processed by the rule engine into separate queue. " + "Useful for complex microservices deployments, to isolate processing of the data for specific tenants", example = "true") private boolean isolatedTbRuleEngine; - @ApiModelProperty(position = 8, value = "Max amount of queues configurable by isolated tenant.", example = "5") - private Integer maxNumberOfQueues; - @ApiModelProperty(position = 9, value = "Max amount of partitions per queue configurable by isolated tenant.", example = "10") - private Integer maxNumberOfPartitionsPerQueue; - @ApiModelProperty(position = 10, value = "Complex JSON object that contains profile settings: max devices, max assets, rate limits, etc.") + @ApiModelProperty(position = 8, value = "Complex JSON object that contains profile settings: queue configs, max devices, max assets, rate limits, etc.") private transient TenantProfileData profileData; @JsonIgnore private byte[] profileDataBytes; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index c9f89ccc91..d122f712ca 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -128,8 +128,6 @@ public class ModelConstants { public static final String TENANT_PROFILE_IS_DEFAULT_PROPERTY = "is_default"; public static final String TENANT_PROFILE_ISOLATED_TB_CORE = "isolated_tb_core"; public static final String TENANT_PROFILE_ISOLATED_TB_RULE_ENGINE = "isolated_tb_rule_engine"; - public static final String TENANT_PROFILE_MAX_NUMBER_OF_QUEUES = "max_number_of_queues"; - public static final String TENANT_PROFILE_MAX_NUMBER_OF_PARTITIONS_PER_QUEUE = "max_number_of_partitions_per_queue"; /** * Cassandra customer constants. diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java index 2b62588c76..6f7b094464 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java @@ -59,12 +59,6 @@ public final class TenantProfileEntity extends BaseSqlEntity impl @Column(name = ModelConstants.TENANT_PROFILE_ISOLATED_TB_RULE_ENGINE) private boolean isolatedTbRuleEngine; - @Column(name = ModelConstants.TENANT_PROFILE_MAX_NUMBER_OF_QUEUES) - private Integer maxNumberOfQueues; - - @Column(name = ModelConstants.TENANT_PROFILE_MAX_NUMBER_OF_PARTITIONS_PER_QUEUE) - private Integer maxNumberOfPartitionsPerQueue; - @Type(type = "jsonb") @Column(name = ModelConstants.TENANT_PROFILE_PROFILE_DATA_PROPERTY, columnDefinition = "jsonb") private JsonNode profileData; @@ -83,8 +77,6 @@ public final class TenantProfileEntity extends BaseSqlEntity impl this.isDefault = tenantProfile.isDefault(); this.isolatedTbCore = tenantProfile.isIsolatedTbCore(); this.isolatedTbRuleEngine = tenantProfile.isIsolatedTbRuleEngine(); - this.maxNumberOfPartitionsPerQueue = tenantProfile.getMaxNumberOfPartitionsPerQueue(); - this.maxNumberOfQueues = tenantProfile.getMaxNumberOfQueues(); this.profileData = JacksonUtil.convertValue(tenantProfile.getProfileData(), ObjectNode.class); } @@ -111,8 +103,6 @@ public final class TenantProfileEntity extends BaseSqlEntity impl tenantProfile.setDefault(isDefault); tenantProfile.setIsolatedTbCore(isolatedTbCore); tenantProfile.setIsolatedTbRuleEngine(isolatedTbRuleEngine); - tenantProfile.setMaxNumberOfPartitionsPerQueue(maxNumberOfPartitionsPerQueue); - tenantProfile.setMaxNumberOfQueues(maxNumberOfQueues); tenantProfile.setProfileData(JacksonUtil.convertValue(profileData, TenantProfileData.class)); return tenantProfile; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java index 28c0330267..a0ad3c3a4a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java @@ -21,14 +21,12 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.queue.ProcessingStrategy; -import org.thingsboard.server.common.data.queue.ProcessingStrategyType; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.queue.SubmitStrategy; import org.thingsboard.server.common.data.queue.SubmitStrategyType; @@ -202,30 +200,6 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ tenantQueuesRemover.removeEntities(tenantId, tenantId); } - @Override - @Transactional - public Queue createDefaultMainQueue(TenantProfile tenantProfile, TenantId tenantId) { - Queue mainQueue = new Queue(); - mainQueue.setTenantId(tenantId); - mainQueue.setName("Main"); - mainQueue.setTopic("tb_rule_engine.main"); - mainQueue.setPollInterval(25); - mainQueue.setPartitions(Math.max(tenantProfile.getMaxNumberOfPartitionsPerQueue(), 1)); - mainQueue.setPackProcessingTimeout(60000); - SubmitStrategy mainQueueSubmitStrategy = new SubmitStrategy(); - mainQueueSubmitStrategy.setType(SubmitStrategyType.BURST); - mainQueueSubmitStrategy.setBatchSize(1000); - mainQueue.setSubmitStrategy(mainQueueSubmitStrategy); - ProcessingStrategy mainQueueProcessingStrategy = new ProcessingStrategy(); - mainQueueProcessingStrategy.setType(ProcessingStrategyType.SKIP_ALL_FAILURES); - mainQueueProcessingStrategy.setRetries(3); - mainQueueProcessingStrategy.setFailurePercentage(0); - mainQueueProcessingStrategy.setPauseBetweenRetries(3); - mainQueueProcessingStrategy.setMaxPauseBetweenRetries(3); - mainQueue.setProcessingStrategy(mainQueueProcessingStrategy); - return saveQueue(mainQueue); - } - private DataValidator queueValidator = new DataValidator<>() { @@ -261,17 +235,6 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ if (!tenantProfile.isIsolatedTbRuleEngine()) { throw new DataValidationException("Tenant should be isolated!"); } - - if (queue.getId() == null) { - List existingQueues = findQueuesByTenantId(tenantId); - if (existingQueues.size() >= tenantProfile.getMaxNumberOfQueues()) { - throw new DataValidationException("The limit for creating new queue has been exceeded!"); - } - } - - if (queue.getPartitions() > tenantProfile.getMaxNumberOfPartitionsPerQueue()) { - throw new DataValidationException(String.format("Queue partitions can't be more then %d", tenantProfile.getMaxNumberOfPartitionsPerQueue())); - } } if (StringUtils.isEmpty(queue.getName())) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java index a4f9c65807..2b8611846a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java @@ -29,8 +29,12 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.queue.ProcessingStrategy; +import org.thingsboard.server.common.data.queue.SubmitStrategy; +import org.thingsboard.server.common.data.queue.SubmitStrategyType; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; @@ -39,6 +43,10 @@ import org.thingsboard.server.dao.service.Validator; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; import static org.thingsboard.server.common.data.CacheConstants.TENANT_PROFILE_CACHE; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -228,6 +236,35 @@ public class TenantProfileServiceImpl extends AbstractEntityService implements T throw new DataValidationException("Another default tenant profile is present!"); } } + + if (tenantProfile.isIsolatedTbRuleEngine()) { + List queueConfiguration = tenantProfile.getProfileData().getQueueConfiguration(); + if (queueConfiguration == null) { + throw new DataValidationException("Tenant profile data queue configuration should be specified!"); + } + + Optional mainQueueConfig = + queueConfiguration + .stream() + .filter(q -> q.getName().equals("Main")) + .findAny(); + if (mainQueueConfig.isEmpty()) { + throw new DataValidationException("Main queue configuration should be specified!"); + } + + queueConfiguration.forEach(this::validateQueueConfiguration); + + Set queueNames = new HashSet<>(queueConfiguration.size()); + + queueConfiguration.forEach(q -> { + String name = q.getName(); + if (queueNames.contains(name)) { + throw new DataValidationException(String.format("Queue configuration name '%s' already present!", name)); + } else { + queueNames.add(name); + } + }); + } } @Override @@ -241,6 +278,54 @@ public class TenantProfileServiceImpl extends AbstractEntityService implements T throw new DataValidationException("Can't update isolatedTbCore property!"); } } + + private void validateQueueConfiguration(TenantProfileQueueConfiguration queue) { + if (StringUtils.isEmpty(queue.getName())) { + throw new DataValidationException("Queue name should be specified!"); + } + if (StringUtils.isBlank(queue.getTopic())) { + throw new DataValidationException("Queue topic should be non empty and without spaces!"); + } + if (queue.getPollInterval() < 1) { + throw new DataValidationException("Queue poll interval should be more then 0!"); + } + if (queue.getPartitions() < 1) { + throw new DataValidationException("Queue partitions should be more then 0!"); + } + if (queue.getPackProcessingTimeout() < 1) { + throw new DataValidationException("Queue pack processing timeout should be more then 0!"); + } + + SubmitStrategy submitStrategy = queue.getSubmitStrategy(); + if (submitStrategy == null) { + throw new DataValidationException("Queue submit strategy can't be null!"); + } + if (submitStrategy.getType() == null) { + throw new DataValidationException("Queue submit strategy type can't be null!"); + } + if (submitStrategy.getType() == SubmitStrategyType.BATCH && submitStrategy.getBatchSize() < 1) { + throw new DataValidationException("Queue submit strategy batch size should be more then 0!"); + } + ProcessingStrategy processingStrategy = queue.getProcessingStrategy(); + if (processingStrategy == null) { + throw new DataValidationException("Queue processing strategy can't be null!"); + } + if (processingStrategy.getType() == null) { + throw new DataValidationException("Queue processing strategy type can't be null!"); + } + if (processingStrategy.getRetries() < 0) { + throw new DataValidationException("Queue processing strategy retries can't be less then 0!"); + } + if (processingStrategy.getFailurePercentage() < 0 || processingStrategy.getFailurePercentage() > 100) { + throw new DataValidationException("Queue processing strategy failure percentage should be in a range from 0 to 100!"); + } + if (processingStrategy.getPauseBetweenRetries() < 0) { + throw new DataValidationException("Queue processing strategy pause between retries can't be less then 0!"); + } + if (processingStrategy.getMaxPauseBetweenRetries() < processingStrategy.getPauseBetweenRetries()) { + throw new DataValidationException("Queue processing strategy MAX pause between retries can't be less then pause between retries!"); + } + } }; private PaginatedRemover tenantProfilesRemover = diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index b85171efee..1f3717429b 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -352,8 +352,6 @@ CREATE TABLE IF NOT EXISTS tenant_profile ( is_default boolean, isolated_tb_core boolean, isolated_tb_rule_engine boolean, - max_number_of_queues int , - max_number_of_partitions_per_queue int, CONSTRAINT tenant_profile_name_unq_key UNIQUE (name) ); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseQueueServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseQueueServiceTest.java index 8899224bd5..577193ff94 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseQueueServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseQueueServiceTest.java @@ -30,10 +30,10 @@ import org.thingsboard.server.common.data.queue.ProcessingStrategyType; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.queue.SubmitStrategy; import org.thingsboard.server.common.data.queue.SubmitStrategyType; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.dao.tenant.TenantServiceImpl; -import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -47,18 +47,34 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { @Before public void before() throws NoSuchFieldException, IllegalAccessException { - Field zkEnabled = TenantServiceImpl.class.getDeclaredField("zkEnabled"); - zkEnabled.setAccessible(true); - zkEnabled.set(tenantService, Boolean.TRUE); - TenantProfile tenantProfile = new TenantProfile(); tenantProfile.setDefault(false); tenantProfile.setName("Isolated TB Rule Engine"); tenantProfile.setDescription("Isolated TB Rule Engine tenant profile"); tenantProfile.setIsolatedTbCore(false); tenantProfile.setIsolatedTbRuleEngine(true); - tenantProfile.setMaxNumberOfQueues(10); - tenantProfile.setMaxNumberOfPartitionsPerQueue(10); + + TenantProfileQueueConfiguration mainQueueConfiguration = new TenantProfileQueueConfiguration(); + mainQueueConfiguration.setName("Main"); + mainQueueConfiguration.setTopic("tb_rule_engine.main"); + mainQueueConfiguration.setPollInterval(25); + mainQueueConfiguration.setPartitions(10); + mainQueueConfiguration.setConsumerPerPartition(true); + mainQueueConfiguration.setPackProcessingTimeout(2000); + SubmitStrategy mainQueueSubmitStrategy = new SubmitStrategy(); + mainQueueSubmitStrategy.setType(SubmitStrategyType.BURST); + mainQueueSubmitStrategy.setBatchSize(1000); + mainQueueConfiguration.setSubmitStrategy(mainQueueSubmitStrategy); + ProcessingStrategy mainQueueProcessingStrategy = new ProcessingStrategy(); + mainQueueProcessingStrategy.setType(ProcessingStrategyType.SKIP_ALL_FAILURES); + mainQueueProcessingStrategy.setRetries(3); + mainQueueProcessingStrategy.setFailurePercentage(0); + mainQueueProcessingStrategy.setPauseBetweenRetries(3); + mainQueueProcessingStrategy.setMaxPauseBetweenRetries(3); + mainQueueConfiguration.setProcessingStrategy(mainQueueProcessingStrategy); + TenantProfileData profileData = tenantProfile.getProfileData(); + profileData.setQueueConfiguration(Collections.singletonList(mainQueueConfiguration)); + tenantProfile.setProfileData(profileData); TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); Assert.assertNotNull(savedTenantProfile); @@ -343,23 +359,6 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { } } - @Test(expected = DataValidationException.class) - public void testSaveQueueWithExceededLimitPerTenant() { - for (int i = 1; i <= 10; i++) { - //main queue created automatically - Queue queue = new Queue(); - queue.setTenantId(tenantId); - queue.setName("Test" + i); - queue.setTopic("tb_rule_engine.test" + i); - queue.setPollInterval(25); - queue.setPartitions(1); - queue.setPackProcessingTimeout(2000); - queue.setSubmitStrategy(createTestSubmitStrategy()); - queue.setProcessingStrategy(createTestProcessingStrategy()); - queueService.saveQueue(queue); - } - } - @Test public void testUpdateQueue() { Queue queue = new Queue(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantProfileServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantProfileServiceTest.java index b655a8439c..ae9459ccae 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantProfileServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantProfileServiceTest.java @@ -25,8 +25,13 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.queue.ProcessingStrategy; +import org.thingsboard.server.common.data.queue.ProcessingStrategyType; +import org.thingsboard.server.common.data.queue.SubmitStrategy; +import org.thingsboard.server.common.data.queue.SubmitStrategyType; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; import org.thingsboard.server.dao.exception.DataValidationException; import java.util.ArrayList; @@ -47,6 +52,29 @@ public abstract class BaseTenantProfileServiceTest extends AbstractServiceTest { @Test public void testSaveTenantProfile() { TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); + + tenantProfile.setIsolatedTbRuleEngine(true); + + TenantProfileQueueConfiguration mainQueueConfiguration = new TenantProfileQueueConfiguration(); + mainQueueConfiguration.setName("Main"); + mainQueueConfiguration.setTopic("tb_rule_engine.main"); + mainQueueConfiguration.setPollInterval(25); + mainQueueConfiguration.setPartitions(10); + mainQueueConfiguration.setConsumerPerPartition(true); + mainQueueConfiguration.setPackProcessingTimeout(2000); + SubmitStrategy mainQueueSubmitStrategy = new SubmitStrategy(); + mainQueueSubmitStrategy.setType(SubmitStrategyType.BURST); + mainQueueSubmitStrategy.setBatchSize(1000); + mainQueueConfiguration.setSubmitStrategy(mainQueueSubmitStrategy); + ProcessingStrategy mainQueueProcessingStrategy = new ProcessingStrategy(); + mainQueueProcessingStrategy.setType(ProcessingStrategyType.SKIP_ALL_FAILURES); + mainQueueProcessingStrategy.setRetries(3); + mainQueueProcessingStrategy.setFailurePercentage(0); + mainQueueProcessingStrategy.setPauseBetweenRetries(3); + mainQueueProcessingStrategy.setMaxPauseBetweenRetries(3); + mainQueueConfiguration.setProcessingStrategy(mainQueueProcessingStrategy); + tenantProfile.getProfileData().setQueueConfiguration(Collections.singletonList(mainQueueConfiguration)); + TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); Assert.assertNotNull(savedTenantProfile); Assert.assertNotNull(savedTenantProfile.getId()); diff --git a/ui-ngx/src/app/core/http/queue.service.ts b/ui-ngx/src/app/core/http/queue.service.ts index bf2183449d..a5967eef97 100644 --- a/ui-ngx/src/app/core/http/queue.service.ts +++ b/ui-ngx/src/app/core/http/queue.service.ts @@ -32,26 +32,26 @@ export class QueueService { ) { } public getTenantQueuesNamesByServiceType(serviceType: ServiceType, config?: RequestConfig): Observable> { - return this.http.get>(`/api/tenant/queues?serviceType=${serviceType}`, + return this.http.get>(`/api/queues?serviceType=${serviceType}`, defaultHttpOptionsFromConfig(config)); } public getQueueById(queueId: string, config?: RequestConfig): Observable { - return this.http.get(`/api/tenant/queues/${queueId}`, defaultHttpOptionsFromConfig(config)); + return this.http.get(`/api/queues/${queueId}`, defaultHttpOptionsFromConfig(config)); } public getTenantQueuesByServiceType(pageLink: PageLink, serviceType: ServiceType, config?: RequestConfig): Observable> { - return this.http.get>(`/api/tenant/queues${pageLink.toQuery()}&serviceType=${serviceType}`, + return this.http.get>(`/api/queues${pageLink.toQuery()}&serviceType=${serviceType}`, defaultHttpOptionsFromConfig(config)); } public saveQueue(queue: QueueInfo, serviceType: ServiceType, config?: RequestConfig): Observable { - return this.http.post(`/api/tenant/queues?serviceType=${serviceType}`, queue, defaultHttpOptionsFromConfig(config)); + return this.http.post(`/api/queues?serviceType=${serviceType}`, queue, defaultHttpOptionsFromConfig(config)); } public deleteQueue(queueId: string) { - return this.http.delete(`/api/tenant/queues/${queueId}`); + return this.http.delete(`/api/queues/${queueId}`); } } diff --git a/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts b/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts index 78507cb121..2bddd229bc 100644 --- a/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts +++ b/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts @@ -567,9 +567,7 @@ export class ImportExportService { return isDefined(tenantProfile.name) && isDefined(tenantProfile.profileData) && isDefined(tenantProfile.isolatedTbCore) - && isDefined(tenantProfile.isolatedTbRuleEngine) - && isDefined(tenantProfile.maxNumberOfQueues) - && isDefined(tenantProfile.maxNumberOfPartitionsPerQueue); + && isDefined(tenantProfile.isolatedTbRuleEngine); } private sumObject(obj1: any, obj2: any): any { diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.html index 81ca768911..115a224374 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.html @@ -16,6 +16,11 @@ -->
+ + + + + diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html index 6377e59a45..e2eb776e5b 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html @@ -67,38 +67,6 @@
{{ 'tenant.isolated-tb-rule-engine' | translate }}
{{'tenant.isolated-tb-rule-engine-details' | translate}}
-
- - tenant.max-number-of-queues - - - {{ 'tenant.max-number-of-queues-required' | translate }} - - - {{ 'tenant.max-number-of-queues-min-length' | translate }} - - - - tenant.max-number-of-partitions-per-queue - - - {{ 'tenant.max-number-of-partitions-per-queue-required' | translate }} - - - {{ 'tenant.max-number-of-partitions-per-queue-min-length' | translate }} - - -
{ name: [entity ? entity.name : '', [Validators.required, Validators.maxLength(255)]], isolatedTbCore: [entity ? entity.isolatedTbCore : false, []], isolatedTbRuleEngine: [entity ? entity.isolatedTbRuleEngine : false, []], - maxNumberOfQueues: [entity ? entity.maxNumberOfQueues : 1, [Validators.required, Validators.min(1)]], - maxNumberOfPartitionsPerQueue: [entity ? entity.maxNumberOfPartitionsPerQueue : 1, [Validators.required, Validators.min(1)]], profileData: [entity && !this.isAdd ? entity.profileData : { configuration: createTenantProfileConfiguration(TenantProfileType.DEFAULT) } as TenantProfileData, []], @@ -81,8 +79,6 @@ export class TenantProfileComponent extends EntityComponent { this.entityForm.patchValue({name: entity.name}); this.entityForm.patchValue({isolatedTbCore: entity.isolatedTbCore}); this.entityForm.patchValue({isolatedTbRuleEngine: entity.isolatedTbRuleEngine}); - this.entityForm.patchValue({maxNumberOfQueues: entity.maxNumberOfQueues}); - this.entityForm.patchValue({maxNumberOfPartitionsPerQueue: entity.maxNumberOfPartitionsPerQueue}); this.entityForm.patchValue({profileData: !this.isAdd ? entity.profileData : { configuration: createTenantProfileConfiguration(TenantProfileType.DEFAULT) } as TenantProfileData}); @@ -92,11 +88,9 @@ export class TenantProfileComponent extends EntityComponent { showQueueParams(): boolean { let isolatedTbRuleEngine: boolean = this.entityForm.get('isolatedTbRuleEngine').value; if (isolatedTbRuleEngine) { - this.entityForm.get('maxNumberOfQueues').enable(); - this.entityForm.get('maxNumberOfPartitionsPerQueue').enable(); +//enable } else { - this.entityForm.get('maxNumberOfQueues').disable(); - this.entityForm.get('maxNumberOfPartitionsPerQueue').disable(); +//disable } return isolatedTbRuleEngine; } @@ -108,8 +102,6 @@ export class TenantProfileComponent extends EntityComponent { if (!this.isAdd) { this.entityForm.get('isolatedTbCore').disable({emitEvent: false}); this.entityForm.get('isolatedTbRuleEngine').disable({emitEvent: false}); - this.entityForm.get('maxNumberOfQueues').disable({emitEvent: false}); - this.entityForm.get('maxNumberOfPartitionsPerQueue').disable({emitEvent: false}); } } else { this.entityForm.disable({emitEvent: false}); diff --git a/ui-ngx/src/app/shared/models/tenant.model.ts b/ui-ngx/src/app/shared/models/tenant.model.ts index 311529a42a..cceb1cc7d7 100644 --- a/ui-ngx/src/app/shared/models/tenant.model.ts +++ b/ui-ngx/src/app/shared/models/tenant.model.ts @@ -107,8 +107,6 @@ export interface TenantProfile extends BaseData { default?: boolean; isolatedTbCore?: boolean; isolatedTbRuleEngine?: boolean; - maxNumberOfQueues?: number; - maxNumberOfPartitionsPerQueue?: number; profileData?: TenantProfileData; } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 4c93854125..4414e71847 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2759,13 +2759,7 @@ "isolated-tb-core": "Processing in isolated ThingsBoard Core container", "isolated-tb-rule-engine": "Processing in isolated ThingsBoard Rule Engine container", "isolated-tb-core-details": "Requires separate microservice(s) per isolated Tenant", - "isolated-tb-rule-engine-details": "Requires separate microservice(s) per isolated Tenant", - "max-number-of-queues": "Max number of ThingsBoard Rule Engine queues", - "max-number-of-queues-required": "Max number of queues is required", - "max-number-of-queues-min-length": "Max number of queues can't be less then 1", - "max-number-of-partitions-per-queue": "Max number of partitions per ThingsBoard Rule Engine queue", - "max-number-of-partitions-per-queue-required": "Max number of partitions per queue is required", - "max-number-of-partitions-per-queue-min-length": "Max number of partitions per queue can't be less then 1" + "isolated-tb-rule-engine-details": "Requires separate microservice(s) per isolated Tenant" }, "tenant-profile": { "tenant-profile": "Tenant profile", From 1cb4b6076e52a59e7132c0c057633c6655c096a6 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 22 Apr 2022 12:45:05 +0300 Subject: [PATCH 055/276] UI: Implement persistent RPC table widget settings form --- .../widget_bundles/control_widgets.json | 3 +- ...stent-table-widget-settings.component.html | 111 +++++++++ ...sistent-table-widget-settings.component.ts | 221 ++++++++++++++++++ .../lib/settings/widget-settings.module.ts | 12 +- .../assets/locale/locale.constant-en_US.json | 14 +- 5 files changed, 356 insertions(+), 5 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/control_widgets.json b/application/src/main/data/json/system/widget_bundles/control_widgets.json index 6722827622..1cf8902b9c 100644 --- a/application/src/main/data/json/system/widget_bundles/control_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/control_widgets.json @@ -165,8 +165,9 @@ "templateHtml": "", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"PersistentTableSettings\",\n \"properties\": {\n \"enableStickyHeader\": {\n \"title\": \"Display header while scrolling\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableFilter\": {\n \"title\": \"Enable filter\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowSendRequest\": {\n \"title\": \"Allow send RPC request\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyAction\": {\n \"title\": \"Display actions column while scrolling\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayDetails\": {\n \"title\": \"Display request details\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowDelete\": {\n \"title\": \"Allow delete request\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"-createdTime\"\n },\n \"displayColumns\": {\n \"title\": \"Columns for display\",\n \"type\": \"array\",\n \"minItems\": 1\n }\n },\n \"required\": [\"displayColumns\"]\n },\n \"uiSchema\": {\n \"type\": \"VerticalLayout\",\n \"elements\": [\n {\n \"type\": \"Control\",\n \"scope\": \"#/schema/properties/enableStickyHeader\"\n },\n {\n \"type\": \"Control\",\n \"scope\": \"#/schema/properties/enableFilter\"\n }\n ]\n },\n \"form\": [\n [\n \"enableStickyHeader\",\n \"enableFilter\",\n \"allowSendRequest\",\n \"enableStickyAction\",\n \"displayDetails\",\n \"allowDelete\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ],\n [\n {\n \"key\": \"displayColumns\",\n \"type\": \"rc-select\",\n \"multiple\": true,\n \"default\": [\"rpcId\", \"messageType\", \"status\", \"method\", \"createdTime\", \"expirationTime\"],\n \"items\": [\n {\n \"value\": \"rpcId\",\n \"label\": \"RPC ID\"\n },\n {\n \"value\": \"messageType\",\n \"label\": \"Message type\"\n },\n {\n \"value\": \"status\",\n \"label\": \"Status\"\n },\n {\n \"value\": \"method\",\n \"label\": \"Method\"\n },\n {\n \"value\": \"createdTime\",\n \"label\": \"Created time\"\n },\n {\n \"value\": \"expirationTime\",\n \"label\": \"Expiration time\"\n }\n ]\n }\n ]\n ],\n \"groupInfoes\": [{\n \"formIndex\": 0,\n \"GroupTitle\": \"General settings\"\n }, {\n \"formIndex\": 1,\n \"GroupTitle\": \"Columns settings\"\n }]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-persistent-table-widget-settings", "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"enableStickyAction\":true,\"enableFilter\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"enableStickyHeader\":true,\"displayColumns\":[\"rpcId\",\"messageType\",\"status\",\"method\",\"createdTime\",\"expirationTime\"],\"displayDetails\":true,\"defaultSortOrder\":\"-createdTime\",\"allowSendRequest\":true,\"allowDelete\":true},\"title\":\"Persistent RPC table\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px\"},\"targetDeviceAliasIds\":[]}" } }, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html new file mode 100644 index 0000000000..7961dddf1f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html @@ -0,0 +1,111 @@ + +
+
+ widgets.persistent-table.general-settings +
+
+ + {{ 'widgets.persistent-table.enable-filter' | translate }} + +
+
+ + {{ 'widgets.persistent-table.enable-sticky-header' | translate }} + + + {{ 'widgets.persistent-table.enable-sticky-action' | translate }} + +
+
+
+ + {{ 'widgets.persistent-table.display-request-details' | translate }} + + + {{ 'widgets.persistent-table.allow-send-request' | translate }} + + + {{ 'widgets.persistent-table.allow-delete-request' | translate }} + +
+ + {{ 'widgets.table.display-pagination' | translate }} + + + widgets.table.default-page-size + + +
+ + widgets.table.default-sort-order + + +
+
+
+ widgets.persistent-table.columns-settings + + widgets.persistent-table.display-columns + + + {{ displayColumnFromValue(column)?.name }} + cancel + + + + + + + + +
+
+ widgets.persistent-table.no-columns-found +
+ + + {{ translate.get('widgets.persistent-table.no-columns-matching', + {column: truncate.transform(columnSearchText, true, 6, '...')}) | async }} + + +
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.ts new file mode 100644 index 0000000000..288a133cae --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.ts @@ -0,0 +1,221 @@ +/// +/// Copyright © 2016-2022 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 { Component, ElementRef, ViewChild } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; +import { TranslateService } from '@ngx-translate/core'; +import { Observable, of, Subject } from 'rxjs'; +import { TruncatePipe } from '@shared/pipe/truncate.pipe'; +import { MatChipInputEvent, MatChipList } from '@angular/material/chips'; +import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { map, mergeMap, share, startWith } from 'rxjs/operators'; +import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; + +interface DisplayColumn { + name: string; + value: string; +} + +@Component({ + selector: 'tb-persistent-table-widget-settings', + templateUrl: './persistent-table-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class PersistentTableWidgetSettingsComponent extends WidgetSettingsComponent { + + @ViewChild('columnsChipList') columnsChipList: MatChipList; + @ViewChild('columnAutocomplete') columnAutocomplete: MatAutocomplete; + @ViewChild('columnInput') columnInput: ElementRef; + + displayColumns: Array = [ + {value: 'rpcId', name: this.translate.instant('widgets.persistent-table.rpc-id')}, + {value: 'messageType', name: this.translate.instant('widgets.persistent-table.message-type')}, + {value: 'status', name: this.translate.instant('widgets.persistent-table.status')}, + {value: 'method', name: this.translate.instant('widgets.persistent-table.method')}, + {value: 'createdTime', name: this.translate.instant('widgets.persistent-table.created-time')}, + {value: 'expirationTime', name: this.translate.instant('widgets.persistent-table.expiration-time')}, + ]; + + separatorKeysCodes = [ENTER, COMMA, SEMICOLON]; + + persistentTableWidgetSettingsForm: FormGroup; + + filteredDisplayColumns: Observable>; + + columnSearchText = ''; + + columnInputChange = new Subject(); + + constructor(protected store: Store, + public translate: TranslateService, + public truncate: TruncatePipe, + private fb: FormBuilder) { + super(store); + this.filteredDisplayColumns = this.columnInputChange + .pipe( + startWith(''), + map((value) => value ? value : ''), + mergeMap(name => this.fetchColumns(name) ), + share() + ); + } + + protected settingsForm(): FormGroup { + return this.persistentTableWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + enableFilter: true, + + enableStickyHeader: true, + enableStickyAction: true, + + displayDetails: true, + allowSendRequest: true, + allowDelete: true, + + displayPagination: true, + defaultPageSize: 10, + + defaultSortOrder: '-createdTime', + displayColumns: ['rpcId', 'messageType', 'status', 'method', 'createdTime', 'expirationTime'] + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.persistentTableWidgetSettingsForm = this.fb.group({ + enableFilter: [settings.enableFilter, []], + enableStickyHeader: [settings.enableStickyHeader, []], + enableStickyAction: [settings.enableStickyAction, []], + allowSendRequest: [settings.allowSendRequest, []], + allowDelete: [settings.allowDelete, []], + displayDetails: [settings.displayDetails, []], + displayPagination: [settings.displayPagination, []], + defaultPageSize: [settings.defaultPageSize, [Validators.min(1)]], + defaultSortOrder: [settings.defaultSortOrder, []], + displayColumns: [settings.displayColumns, [Validators.required]] + }); + } + + protected validateSettings(): boolean { + const displayColumns: string[] = this.persistentTableWidgetSettingsForm.get('displayColumns').value; + this.columnsChipList.errorState = !displayColumns?.length; + return super.validateSettings(); + } + + protected validatorTriggers(): string[] { + return ['displayPagination']; + } + + protected updateValidators(emitEvent: boolean) { + const displayPagination: boolean = this.persistentTableWidgetSettingsForm.get('displayPagination').value; + if (displayPagination) { + this.persistentTableWidgetSettingsForm.get('defaultPageSize').enable(); + } else { + this.persistentTableWidgetSettingsForm.get('defaultPageSize').disable(); + } + this.persistentTableWidgetSettingsForm.get('defaultPageSize').updateValueAndValidity({emitEvent}); + } + + private fetchColumns(searchText?: string): Observable> { + this.columnSearchText = searchText; + if (this.columnSearchText && this.columnSearchText.length) { + const search = this.columnSearchText.toUpperCase(); + return of(this.displayColumns.filter(column => column.name.toUpperCase().includes(search))); + } else { + return of(this.displayColumns); + } + } + + private addColumn(existingColumn: DisplayColumn): boolean { + if (existingColumn) { + const displayColumns: string[] = this.persistentTableWidgetSettingsForm.get('displayColumns').value; + const index = displayColumns.indexOf(existingColumn.value); + if (index === -1) { + displayColumns.push(existingColumn.value); + this.persistentTableWidgetSettingsForm.get('displayColumns').setValue(displayColumns); + this.persistentTableWidgetSettingsForm.get('displayColumns').markAsDirty(); + this.columnsChipList.errorState = false; + return true; + } + } + return false; + } + + displayColumnFromValue(columnValue: string): DisplayColumn | undefined { + return this.displayColumns.find((column) => column.value === columnValue); + } + + displayColumnFn(column?: DisplayColumn): string | undefined { + return column ? column.name : undefined; + } + + textIsNotEmpty(text: string): boolean { + return (text && text.length > 0); + } + + columnDrop(event: CdkDragDrop): void { + const displayColumns: string[] = this.persistentTableWidgetSettingsForm.get('displayColumns').value; + moveItemInArray(displayColumns, event.previousIndex, event.currentIndex); + this.persistentTableWidgetSettingsForm.get('displayColumns').setValue(displayColumns); + this.persistentTableWidgetSettingsForm.get('displayColumns').markAsDirty(); + } + + onColumnRemoved(column: string): void { + const displayColumns: string[] = this.persistentTableWidgetSettingsForm.get('displayColumns').value; + const index = displayColumns.indexOf(column); + if (index > -1) { + displayColumns.splice(index, 1); + this.persistentTableWidgetSettingsForm.get('displayColumns').setValue(displayColumns); + this.persistentTableWidgetSettingsForm.get('displayColumns').markAsDirty(); + this.columnsChipList.errorState = !displayColumns.length; + } + } + + onColumnInputFocus() { + this.columnInputChange.next(this.columnInput.nativeElement.value); + } + + addColumnFromChipInput(event: MatChipInputEvent): void { + const value = event.value; + if ((value || '').trim()) { + const columnName = value.trim().toUpperCase(); + const existingColumn = this.displayColumns.find(column => column.name.toUpperCase() === columnName); + if (this.addColumn(existingColumn)) { + this.clearColumnInput(''); + } + } + } + + columnSelected(event: MatAutocompleteSelectedEvent): void { + this.addColumn(event.option.value); + this.clearColumnInput(''); + } + + clearColumnInput(value: string = '') { + this.columnInput.nativeElement.value = value; + this.columnInputChange.next(null); + setTimeout(() => { + this.columnInput.nativeElement.blur(); + this.columnInput.nativeElement.focus(); + }, 0); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index c5591106ad..e893b74b7e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -116,6 +116,9 @@ import { import { SlideToggleWidgetSettingsComponent } from '@home/components/widget/lib/settings/control/slide-toggle-widget-settings.component'; +import { + PersistentTableWidgetSettingsComponent +} from '@home/components/widget/lib/settings/control/persistent-table-widget-settings.component'; @NgModule({ declarations: [ @@ -159,7 +162,8 @@ import { SwitchRpcSettingsComponent, RoundSwitchWidgetSettingsComponent, SwitchControlWidgetSettingsComponent, - SlideToggleWidgetSettingsComponent + SlideToggleWidgetSettingsComponent, + PersistentTableWidgetSettingsComponent ], imports: [ CommonModule, @@ -207,7 +211,8 @@ import { SwitchRpcSettingsComponent, RoundSwitchWidgetSettingsComponent, SwitchControlWidgetSettingsComponent, - SlideToggleWidgetSettingsComponent + SlideToggleWidgetSettingsComponent, + PersistentTableWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -243,5 +248,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type Date: Fri, 22 Apr 2022 18:24:55 +0300 Subject: [PATCH 056/276] UI: Implement rpc control widgets settings forms --- .../widget_bundles/control_widgets.json | 18 +- .../device-key-autocomplete.component.html | 41 ++++ .../device-key-autocomplete.component.ts | 216 ++++++++++++++++++ ...nob-control-widget-settings.component.html | 81 +++++++ .../knob-control-widget-settings.component.ts | 95 ++++++++ ...d-indicator-widget-settings.component.html | 104 +++++++++ ...led-indicator-widget-settings.component.ts | 133 +++++++++++ .../control/rpc-button-style.component.html | 40 ++++ .../control/rpc-button-style.component.ts | 98 ++++++++ .../rpc-shell-widget-settings.component.html | 27 +++ .../rpc-shell-widget-settings.component.ts | 53 +++++ ...pc-terminal-widget-settings.component.html | 49 ++++ .../rpc-terminal-widget-settings.component.ts | 75 ++++++ .../send-rpc-widget-settings.component.html | 79 +++++++ .../send-rpc-widget-settings.component.ts | 99 ++++++++ .../switch-rpc-settings.component.html | 33 +-- .../control/switch-rpc-settings.component.ts | 97 +------- ...e-attribute-widget-settings.component.html | 51 +++++ ...ice-attribute-widget-settings.component.ts | 68 ++++++ .../lib/settings/widget-settings.module.ts | 50 +++- .../components/json-content.component.html | 2 +- .../components/json-content.component.scss | 57 ++--- .../components/json-content.component.ts | 22 +- .../assets/locale/locale.constant-en_US.json | 31 ++- 24 files changed, 1456 insertions(+), 163 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/control/knob-control-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/control/knob-control-widget-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/control_widgets.json b/application/src/main/data/json/system/widget_bundles/control_widgets.json index 1cf8902b9c..2dd348c84c 100644 --- a/application/src/main/data/json/system/widget_bundles/control_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/control_widgets.json @@ -19,8 +19,9 @@ "templateHtml": "
", "templateCss": ".cmd .cursor.blink {\n -webkit-animation-name: terminal-underline;\n -moz-animation-name: terminal-underline;\n -ms-animation-name: terminal-underline;\n animation-name: terminal-underline;\n}\n.terminal .inverted, .cmd .inverted {\n border-bottom-color: #aaa;\n}\n\n", "controllerScript": "var requestTimeout = 500;\nvar requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetDeviceName && subscription.targetDeviceName.length) {\n deviceName = subscription.targetDeviceName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\n }\n var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = command.trim();\n var requestUUID = uuidv4();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var spaceIndex = localCommand.indexOf(' ');\n if (spaceIndex === -1 && !localCommand.length) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (spaceIndex === -1) {\n spaceIndex = localCommand.length;\n }\n var name = localCommand.substr(0, spaceIndex);\n var args = localCommand.substr(spaceIndex + 1);\n if (args.length) {\n try {\n params = JSON.parse(args);\n } catch (e) {\n params = args;\n }\n }\n performRpc(this, name, params, requestUUID);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt,\n enabled: rpcEnabled\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' [params body]]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestPersistent, persistentPollingInterval, requestUUID).subscribe(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n\nfunction uuidv4() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n \nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"requestTimeout\": {\n \"title\": \"RPC request timeout (ms)\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"requestPersistent\": {\n \"title\": \"RPC request persistent\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"persistentPollingInterval\": {\n \"title\": \"Polling interval in milliseconds to get persistent RPC command response\",\n \"type\": \"number\",\n \"default\": 5000,\n \"minimum\": 1000\n }\n },\n \"required\": [\"requestTimeout\"]\n },\n \"form\": [\n \"requestTimeout\",\n \"requestPersistent\",\n {\n \"key\": \"persistentPollingInterval\",\n \"condition\": \"model.requestPersistent === true\"\n }\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-rpc-terminal-widget-settings", "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":true,\"backgroundColor\":\"#010101\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"parseGpioStatusFunction\":\"return body[pin] === true;\",\"gpioStatusChangeRequest\":{\"method\":\"setGpioStatus\",\"paramsBody\":\"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"},\"requestTimeout\":500,\"switchPanelBackgroundColor\":\"#b71c1c\",\"gpioStatusRequest\":{\"method\":\"getGpioStatus\",\"paramsBody\":\"{}\"},\"gpioList\":[{\"pin\":1,\"label\":\"GPIO 1\",\"row\":0,\"col\":0,\"_uniqueKey\":0},{\"pin\":2,\"label\":\"GPIO 2\",\"row\":0,\"col\":1,\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 3\",\"row\":1,\"col\":0,\"_uniqueKey\":2}]},\"title\":\"RPC debug terminal\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -37,8 +38,9 @@ "templateHtml": "
", "templateCss": ".cmd .cursor.blink {\n -webkit-animation-name: terminal-underline;\n -moz-animation-name: terminal-underline;\n -ms-animation-name: terminal-underline;\n animation-name: terminal-underline;\n}\n.terminal .inverted, .cmd .inverted {\n border-bottom-color: #aaa;\n}\n", "controllerScript": "var requestTimeout = 500;\nvar commandStatusPollingInterval = 200;\n\nvar welcome = 'Welcome to ThingsBoard RPC remote shell.\\n';\n\nvar terminal, rpcEnabled, simulated, deviceName, cwd;\nvar commandExecuting = false;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n rpcEnabled = subscription.rpcEnabled;\n if (subscription.targetDeviceName && subscription.targetDeviceName.length) {\n deviceName = subscription.targetDeviceName;\n } else {\n deviceName = 'Simulated';\n simulated = true;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n \n terminal = $('#device-terminal', self.ctx.$container).terminal(\n function (command) {\n if (command && command.trim().length) {\n try {\n if (simulated) {\n this.echo(command);\n } else {\n sendCommand(this, command);\n }\n } catch(e) {\n this.error(e + '');\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: false,\n enabled: rpcEnabled,\n prompt: rpcEnabled ? currentPrompt : '',\n name: 'shell',\n pauseEvents: false,\n keydown: function (e, term) {\n if ((e.which == 67 || e.which == 68) && e.ctrlKey) { // CTRL+C || CTRL+D\n if (commandExecuting) {\n terminateCommand(term);\n return false;\n }\n }\n },\n onInit: initTerm\n }\n );\n \n};\n\nfunction initTerm(terminal) {\n terminal.echo(welcome);\n if (!rpcEnabled) {\n terminal.error('Target device is not set!\\n');\n } else {\n terminal.echo('Current target device for RPC terminal: [[b;#fff;]' + deviceName + ']\\n');\n if (!simulated) {\n terminal.pause();\n getTermInfo(terminal,\n function (remoteTermInfo) {\n if (remoteTermInfo) {\n terminal.echo('Remote platform info:');\n terminal.echo('OS: [[b;#fff;]' + remoteTermInfo.platform + ']');\n if (remoteTermInfo.release) {\n terminal.echo('OS release: [[b;#fff;]' + remoteTermInfo.release + ']');\n }\n terminal.echo('\\r');\n } else {\n terminal.echo('[[;#f00;]Unable to get remote platform info.\\nDevice is not responding.]\\n');\n }\n terminal.resume();\n });\n }\n }\n}\n\nfunction currentPrompt(callback) {\n if (cwd) {\n callback('[[b;#2196f3;]' + deviceName +']: [[b;#8bc34a;]' + cwd +']> ');\n } else {\n callback('[[b;#8bc34a;]' + deviceName +']> ');\n }\n}\n\nfunction getTermInfo(terminal, callback) {\n self.ctx.controlApi.sendTwoWayCommand('getTermInfo', null, requestTimeout).subscribe(\n function (termInfo) {\n cwd = termInfo.cwd;\n if (callback) {\n callback(termInfo);\n } \n },\n function () {\n if (callback) {\n callback(null);\n }\n }\n );\n}\n\nfunction sendCommand(terminal, command) {\n terminal.pause();\n var sendCommandRequest = {\n command: command,\n cwd: cwd\n };\n self.ctx.controlApi.sendTwoWayCommand('sendCommand', sendCommandRequest, requestTimeout).subscribe(\n function (responseBody) {\n if (responseBody && responseBody.ok) {\n commandExecuting = true;\n setTimeout( pollCommandStatus.bind(null,terminal), commandStatusPollingInterval );\n } else {\n var error = responseBody ? responseBody.error : 'Unhandled error.';\n terminal.error(error);\n terminal.resume();\n }\n },\n function () {\n onRpcError(terminal);\n }\n );\n}\n\nfunction terminateCommand(terminal) {\n self.ctx.controlApi.sendTwoWayCommand('terminateCommand', null, requestTimeout).subscribe(\n function (responseBody) {\n if (!responseBody.ok) {\n commandExecuting = false;\n terminal.error(responseBody.error);\n terminal.resume();\n } \n },\n function () {\n onRpcError(terminal);\n }\n ); \n}\n\nfunction onRpcError(terminal) {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.resume();\n}\n\nfunction pollCommandStatus(terminal) {\n self.ctx.controlApi.sendTwoWayCommand('getCommandStatus', null, requestTimeout).subscribe(\n function (commandStatusResponse) {\n for (var i=0;i", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\",\n \"default\": 100\n },\n \"initialValue\": {\n \"title\": \"Initial value\",\n \"type\": \"number\",\n \"default\": 50\n },\n \"title\": {\n \"title\": \"Knob title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"getValueMethod\": {\n \"title\": \"Get value method\",\n \"type\": \"string\",\n \"default\": \"getValue\"\n },\n \"setValueMethod\": {\n \"title\": \"Set value method\",\n \"type\": \"string\",\n \"default\": \"setValue\"\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"requestPersistent\": {\n \"title\": \"RPC request persistent\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"persistentPollingInterval\": {\n \"title\": \"Polling interval in milliseconds to get persistent RPC command response\",\n \"type\": \"number\",\n \"default\": 5000,\n \"minimum\": 1000\n }\n },\n \"required\": [\"minValue\", \"maxValue\", \"getValueMethod\", \"setValueMethod\", \"requestTimeout\"]\n },\n \"form\": [\n \"minValue\",\n \"maxValue\",\n \"initialValue\",\n \"title\",\n \"getValueMethod\",\n \"setValueMethod\",\n \"requestTimeout\",\n \"requestPersistent\",\n {\n \"key\": \"persistentPollingInterval\",\n \"condition\": \"model.requestPersistent === true\"\n }\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-knob-control-widget-settings", "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":500,\"maxValue\":100,\"initialValue\":50,\"minValue\":0,\"title\":\"Knob control\",\"getValueMethod\":\"getValue\",\"setValueMethod\":\"setValue\"},\"title\":\"Knob Control\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2}" } }, @@ -111,8 +114,9 @@ "templateHtml": "", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"initialValue\": {\n \"title\": \"Initial value\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"title\": {\n \"title\": \"LED title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"ledColor\": {\n \"title\": \"LED Color\",\n \"type\": \"string\",\n \"default\": \"green\"\n },\n \"performCheckStatus\": {\n \"title\": \"Perform RPC device status check\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"checkStatusMethod\": {\n \"title\": \"RPC check device status method\",\n \"type\": \"string\",\n \"default\": \"checkStatus\"\n },\n \"retrieveValueMethod\": {\n \"title\": \"Retrieve led status value using method\",\n \"type\": \"string\",\n \"default\": \"attribute\"\n },\n \"valueAttribute\": {\n \"title\": \"Device attribute/timeseries containing led status value\",\n \"type\": \"string\",\n \"default\": \"value\"\n },\n \"parseValueFunction\": {\n \"title\": \"Parse led status value function, f(data), returns boolean\",\n \"type\": \"string\",\n \"default\": \"return data ? true : false;\"\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout (ms)\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"requestPersistent\": {\n \"title\": \"RPC request persistent\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"persistentPollingInterval\": {\n \"title\": \"Polling interval in milliseconds to get persistent RPC command response\",\n \"type\": \"number\",\n \"default\": 5000,\n \"minimum\": 1000\n }\n },\n \"required\": [\"valueAttribute\", \"requestTimeout\"]\n },\n \"form\": [\n \"initialValue\",\n \"title\",\n {\n \"key\": \"ledColor\",\n \"type\": \"color\"\n },\n \"performCheckStatus\",\n \"checkStatusMethod\",\n {\n \"key\": \"retrieveValueMethod\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"attribute\",\n \"label\": \"Subscribe for attribute\"\n },\n {\n \"value\": \"timeseries\",\n \"label\": \"Subscribe for timeseries\"\n }\n ]\n },\n \"valueAttribute\",\n {\n \"key\": \"parseValueFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/rpc/parse_value_fn\"\n },\n \"requestTimeout\",\n \"requestPersistent\",\n {\n \"key\": \"persistentPollingInterval\",\n \"condition\": \"model.requestPersistent === true\"\n }\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-led-indicator-widget-settings", "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":500,\"initialValue\":true,\"title\":\"Led indicator\",\"ledColor\":\"#4caf50\",\"valueAttribute\":\"value\",\"retrieveValueMethod\":\"attribute\",\"parseValueFunction\":\"return data ? true : false;\",\"performCheckStatus\":true,\"checkStatusMethod\":\"checkStatus\"},\"title\":\"Led indicator\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2}" } }, @@ -129,8 +133,9 @@ "templateHtml": "
\n
\n {{title}}\n
\n
\n
\n \n
\n
\n
\n {{ error }}\n
\n
", "templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .mat-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}", "controllerScript": "var requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\n }\n \n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges();\n });\n};\n\nfunction init() {\n let rpcEnabled = self.ctx.defaultSubscription.rpcEnabled;\n\n self.ctx.$scope.buttonLable = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton.bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n if (!rpcEnabled) {\n self.ctx.$scope.error =\n 'Target device is not set!';\n }\n\n self.ctx.$scope.sendCommand = function() {\n var rpcMethod = self.ctx.settings.methodName;\n var rpcParams = self.ctx.settings.methodParams;\n if (rpcParams.length) {\n try {\n rpcParams = JSON.parse(rpcParams);\n } catch (e) {}\n }\n var timeout = self.ctx.settings.requestTimeout;\n var oneWayElseTwoWay = self.ctx.settings.oneWayElseTwoWay ?\n true : false;\n\n var commandPromise;\n if (oneWayElseTwoWay) {\n commandPromise = self.ctx.controlApi.sendOneWayCommand(\n rpcMethod, rpcParams, timeout, requestPersistent, persistentPollingInterval);\n } else {\n commandPromise = self.ctx.controlApi.sendTwoWayCommand(\n rpcMethod, rpcParams, timeout, requestPersistent, persistentPollingInterval);\n }\n commandPromise.subscribe(\n function success() {\n self.ctx.$scope.error = \"\";\n self.ctx.detectChanges();\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n self.ctx.detectChanges();\n }\n }\n );\n };\n}\n\nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"title\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"buttonText\": {\n \"title\": \"Button label\",\n \"type\": \"string\",\n \"default\": \"Send RPC\"\n },\n \"oneWayElseTwoWay\": {\n \"title\": \"Is One Way Command\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showError\": {\n \"title\": \"Show RPC command execution error\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"methodName\": {\n \"title\": \"RPC method\",\n \"type\": \"string\",\n \"default\": \"rpcCommand\"\n },\n \"methodParams\": {\n \"title\": \"RPC method params\",\n \"type\": \"string\",\n \"default\": \"{}\"\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 5000\n },\n \"requestPersistent\": {\n \"title\": \"RPC request persistent\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"persistentPollingInterval\": {\n \"title\": \"Polling interval in milliseconds to get persistent RPC command response\",\n \"type\": \"number\",\n \"default\": 5000,\n \"minimum\": 1000\n },\n \"styleButton\": {\n \"type\": \"object\",\n \"title\": \"Button Style\",\n \"properties\": {\n \"isRaised\": {\n \"type\": \"boolean\",\n \"title\": \"Raised\",\n \"default\": true\n },\n \"isPrimary\": {\n \"type\": \"boolean\",\n \"title\": \"Primary color\",\n \"default\": false\n },\n \"bgColor\": {\n \"type\": \"string\",\n \"title\": \"Button background color\",\n \"default\": null\n },\n \"textColor\": {\n \"type\": \"string\",\n \"title\": \"Button text color\",\n \"default\": null\n }\n }\n },\n \"required\": []\n }\n },\n \"form\": [\n \"title\",\n \"buttonText\",\n \"oneWayElseTwoWay\",\n \"showError\",\n \"methodName\",\n {\n \"key\": \"methodParams\",\n \"type\": \"json\"\n },\n \"requestTimeout\",\n \"requestPersistent\",\n {\n \"key\": \"persistentPollingInterval\",\n \"condition\": \"model.requestPersistent === true\"\n },\n {\n \"key\": \"styleButton\",\n \"items\": [\n \"styleButton.isRaised\",\n \"styleButton.isPrimary\",\n {\n \"key\": \"styleButton.bgColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"styleButton.textColor\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-send-rpc-widget-settings", "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":5000,\"oneWayElseTwoWay\":true,\"buttonText\":\"Send RPC\",\"styleButton\":{\"isRaised\":true,\"isPrimary\":false},\"methodName\":\"rpcCommand\",\"methodParams\":\"{}\"},\"title\":\"RPC Button\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -147,8 +152,9 @@ "templateHtml": "
\n
\n {{title}}\n
\n
\n
\n \n
\n
\n
\n {{ error }}\n
\n
", "templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .mat-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}", "controllerScript": "self.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges();\n });\n};\n\nfunction init() {\n self.ctx.$scope.buttonLable = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n let entityAttributeType = self.ctx.settings.entityAttributeType;\n let entityParameters = JSON.parse(self.ctx.settings.entityParameters);\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton\n .bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n let attributeService = self.ctx.$scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n\n self.ctx.$scope.sendUpdate = function() {\n let attributes = [];\n for (let key in entityParameters) {\n attributes.push({\n \"key\": key,\n \"value\": entityParameters[key]\n });\n }\n \n let entityId = {\n entityType: \"DEVICE\",\n id: self.ctx.defaultSubscription.targetDeviceId\n };\n attributeService.saveEntityAttributes(entityId,\n entityAttributeType, attributes).subscribe(\n function success() {\n self.ctx.$scope.error = \"\";\n self.ctx.detectChanges();\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n self.ctx.detectChanges();\n }\n }\n\n );\n };\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"title\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"buttonText\": {\n \"title\": \"Button label\",\n \"type\": \"string\",\n \"default\": \"Update device attribute\"\n },\n \"entityAttributeType\": {\n \"title\": \"Device attribute scope\",\n \"type\": \"string\",\n \"default\": \"SERVER_SCOPE\"\n },\n \"entityParameters\": {\n \"title\": \"Device attribute parameters\",\n \"type\": \"string\",\n \"default\": \"{}\"\n },\n \"styleButton\": {\n \"type\": \"object\",\n \"title\": \"Button Style\",\n \"properties\": {\n \"isRaised\": {\n \"type\": \"boolean\",\n \"title\": \"Raised\",\n \"default\": true\n },\n \"isPrimary\": {\n \"type\": \"boolean\",\n \"title\": \"Primary color\",\n \"default\": false\n },\n \"bgColor\": {\n \"type\": \"string\",\n \"title\": \"Button background color\",\n \"default\": null\n },\n \"textColor\": {\n \"type\": \"string\",\n \"title\": \"Button text color\",\n \"default\": null\n }\n }\n },\n \"required\": []\n }\n },\n \"form\": [\n \"title\",\n \"buttonText\",\n {\n \"key\": \"entityAttributeType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [{\n \"value\": \"SERVER_SCOPE\",\n \"label\": \"Server attribute\"\n }, {\n \"value\": \"SHARED_SCOPE\",\n \"label\": \"Shared attribute\"\n }]\n }, {\n \"key\": \"entityParameters\",\n \"type\": \"json\"\n },\n {\n \"key\": \"styleButton\",\n \"items\": [\n \"styleButton.isRaised\",\n \"styleButton.isPrimary\",\n {\n \"key\": \"styleButton.bgColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"styleButton.textColor\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-device-attribute-widget-settings", "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"styleButton\":{\"isRaised\":true,\"isPrimary\":false},\"entityParameters\":\"{}\",\"entityAttributeType\":\"SERVER_SCOPE\",\"buttonText\":\"Update device attribute\"},\"title\":\"Update device attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"targetDeviceAliases\":[]}" } }, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html new file mode 100644 index 0000000000..d60481fc8c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html @@ -0,0 +1,41 @@ + + + {{ (keyType === dataKeyType.attribute + ? attributeLabel + : timeseriesLabel) | translate }} + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts new file mode 100644 index 0000000000..46ce0034ee --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts @@ -0,0 +1,216 @@ +/// +/// Copyright © 2016-2022 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 { Component, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { Observable, of } from 'rxjs'; +import { IAliasController } from '@core/api/widget-api.models'; +import { catchError, map, mergeMap, publishReplay, refCount, tap } from 'rxjs/operators'; +import { DataKey } from '@shared/models/widget.models'; +import { EntityService } from '@core/http/entity.service'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; + +@Component({ + selector: 'tb-device-key-autocomplete', + templateUrl: './device-key-autocomplete.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceKeyAutocompleteComponent), + multi: true + } + ] +}) +export class DeviceKeyAutocompleteComponent extends PageComponent implements OnInit, ControlValueAccessor, OnChanges { + + @ViewChild('keyInput') keyInput: ElementRef; + + @Input() + disabled: boolean; + + @Input() + aliasController: IAliasController; + + @Input() + targetDeviceAliasId: string; + + @Input() + keyType: DataKeyType; + + @Input() + attributeLabel = 'widgets.rpc.attribute-value-key'; + + @Input() + timeseriesLabel = 'widgets.rpc.timeseries-value-key'; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + dataKeyType = DataKeyType; + + private modelValue: string; + + private propagateChange = null; + + public deviceKeyFormGroup: FormGroup; + + filteredKeys: Observable>; + keySearchText = ''; + + private latestKeySearchResult: Array = null; + private keysFetchObservable$: Observable> = null; + + constructor(protected store: Store, + private translate: TranslateService, + private entityService: EntityService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.deviceKeyFormGroup = this.fb.group({ + key: [null, this.required ? [Validators.required] : []] + }); + this.deviceKeyFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + + this.filteredKeys = this.deviceKeyFormGroup.get('key').valueChanges + .pipe( + map(value => value ? value : ''), + mergeMap(name => this.fetchKeys(name) ) + ); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'targetDeviceAliasId' || propName === 'keyType') { + this.clearKeysCache(); + } + } + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.deviceKeyFormGroup.disable({emitEvent: false}); + } else { + this.deviceKeyFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: string): void { + this.modelValue = value; + this.deviceKeyFormGroup.patchValue( + {key: value}, {emitEvent: false} + ); + } + + clearKey() { + this.deviceKeyFormGroup.get('key').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.keyInput.nativeElement.blur(); + this.keyInput.nativeElement.focus(); + }, 0); + } + + onFocus() { + this.deviceKeyFormGroup.get('key').updateValueAndValidity({onlySelf: true, emitEvent: true}); + } + + private updateModel() { + const value: string = this.deviceKeyFormGroup.get('key').value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } + + private clearKeysCache(): void { + this.latestKeySearchResult = null; + this.keysFetchObservable$ = null; + } + + private fetchKeys(searchText?: string): Observable> { + if (this.keySearchText !== searchText || this.latestKeySearchResult === null) { + this.keySearchText = searchText; + const dataKeyFilter = this.createKeyFilter(this.keySearchText); + return this.getKeys().pipe( + map(name => name.filter(dataKeyFilter)), + tap(res => this.latestKeySearchResult = res) + ); + } + return of(this.latestKeySearchResult); + } + + private getKeys() { + if (this.keysFetchObservable$ === null) { + let fetchObservable: Observable>; + if (this.targetDeviceAliasId) { + const dataKeyTypes = [this.keyType]; + fetchObservable = this.fetchEntityKeys(this.targetDeviceAliasId, dataKeyTypes); + } else { + fetchObservable = of([]); + } + this.keysFetchObservable$ = fetchObservable.pipe( + map((dataKeys) => dataKeys.map((dataKey) => dataKey.name)), + publishReplay(1), + refCount() + ); + } + return this.keysFetchObservable$; + } + + private fetchEntityKeys(entityAliasId: string, dataKeyTypes: Array): Observable> { + return this.aliasController.getAliasInfo(entityAliasId).pipe( + mergeMap((aliasInfo) => { + return this.entityService.getEntityKeysByEntityFilter( + aliasInfo.entityFilter, + dataKeyTypes, + {ignoreLoading: true, ignoreErrors: true} + ).pipe( + catchError(() => of([])) + ); + }), + catchError(() => of([] as Array)) + ); + } + + private createKeyFilter(query: string): (key: string) => boolean { + const lowercaseQuery = query.toLowerCase(); + return key => key.toLowerCase().startsWith(lowercaseQuery); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/knob-control-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/knob-control-widget-settings.component.html new file mode 100644 index 0000000000..1788a74446 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/knob-control-widget-settings.component.html @@ -0,0 +1,81 @@ + + +
+
+ widgets.rpc.common-settings + + widgets.rpc.knob-title + + +
+
+ widgets.rpc.value-settings + + widgets.rpc.initial-value + + +
+ + widgets.rpc.min-value + + + + widgets.rpc.max-value + + +
+ + widgets.rpc.get-value-method + + + + widgets.rpc.set-value-method + + +
+
+ widgets.rpc.rpc-settings + + widgets.rpc.request-timeout + + +
+ widgets.rpc.persistent-rpc-settings + + + + + {{ 'widgets.rpc.request-persistent' | translate }} + + + + widget-config.advanced-settings + + + + + widgets.rpc.persistent-polling-interval + + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/knob-control-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/knob-control-widget-settings.component.ts new file mode 100644 index 0000000000..dae3045110 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/knob-control-widget-settings.component.ts @@ -0,0 +1,95 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-knob-control-widget-settings', + templateUrl: './knob-control-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class KnobControlWidgetSettingsComponent extends WidgetSettingsComponent { + + knobControlWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.knobControlWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + title: '', + minValue: 0, + maxValue: 100, + initialValue: 50, + getValueMethod: 'getValue', + setValueMethod: 'setValue', + requestTimeout: 500, + requestPersistent: false, + persistentPollingInterval: 5000 + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.knobControlWidgetSettingsForm = this.fb.group({ + + // Common settings + + title: [settings.title, []], + + // Value settings + + initialValue: [settings.initialValue, []], + minValue: [settings.minValue, [Validators.required]], + maxValue: [settings.maxValue, [Validators.required]], + + getValueMethod: [settings.getValueMethod, [Validators.required]], + setValueMethod: [settings.setValueMethod, [Validators.required]], + + // RPC settings + + requestTimeout: [settings.requestTimeout, [Validators.min(0), Validators.required]], + + // --> Persistent RPC settings + + requestPersistent: [settings.requestPersistent, []], + persistentPollingInterval: [settings.persistentPollingInterval, [Validators.min(1000)]] + }); + } + + protected validatorTriggers(): string[] { + return ['requestPersistent']; + } + + protected updateValidators(emitEvent: boolean): void { + const requestPersistent: boolean = this.knobControlWidgetSettingsForm.get('requestPersistent').value; + if (requestPersistent) { + this.knobControlWidgetSettingsForm.get('persistentPollingInterval').enable({emitEvent}); + } else { + this.knobControlWidgetSettingsForm.get('persistentPollingInterval').disable({emitEvent}); + } + this.knobControlWidgetSettingsForm.get('persistentPollingInterval').updateValueAndValidity({emitEvent: false}); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.html new file mode 100644 index 0000000000..c4e934eabe --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.html @@ -0,0 +1,104 @@ + + +
+
+ widgets.rpc.common-settings + + widgets.rpc.led-title + + + + +
+
+ widgets.rpc.value-settings + + {{ 'widgets.rpc.initial-value' | translate }} + +
+ widgets.rpc.check-status-settings + + {{ 'widgets.rpc.perform-rpc-status-check' | translate }} + + + widgets.rpc.retrieve-led-status-value-method + + + {{ 'widgets.rpc.retrieve-value-method-attribute' | translate }} + + + {{ 'widgets.rpc.retrieve-value-method-timeseries' | translate }} + + + + + + + widgets.rpc.check-status-method + + + + +
+
+
+ widgets.rpc.rpc-settings + + widgets.rpc.request-timeout + + +
+ widgets.rpc.persistent-rpc-settings + + + + + {{ 'widgets.rpc.request-persistent' | translate }} + + + + widget-config.advanced-settings + + + + + widgets.rpc.persistent-polling-interval + + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.ts new file mode 100644 index 0000000000..d940e0f64a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.ts @@ -0,0 +1,133 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { WidgetService } from '@core/http/widget.service'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; + +@Component({ + selector: 'tb-led-indicator-widget-settings', + templateUrl: './led-indicator-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class LedIndicatorWidgetSettingsComponent extends WidgetSettingsComponent { + + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + + get targetDeviceAliasId(): string { + const aliasIds = this.widget.config.targetDeviceAliasIds; + if (aliasIds && aliasIds.length) { + return aliasIds[0]; + } + return null; + } + + dataKeyType = DataKeyType; + + ledIndicatorWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private widgetService: WidgetService, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.ledIndicatorWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + title: '', + ledColor: 'green', + initialValue: false, + performCheckStatus: true, + checkStatusMethod: 'checkStatus', + retrieveValueMethod: 'attribute', + valueAttribute: 'value', + parseValueFunction: 'return data ? true : false;', + requestTimeout: 500, + requestPersistent: false, + persistentPollingInterval: 5000 + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.ledIndicatorWidgetSettingsForm = this.fb.group({ + + // Common settings + + title: [settings.title, []], + ledColor: [settings.ledColor, []], + + // Value settings + + initialValue: [settings.initialValue, []], + + // --> Check status settings + + performCheckStatus: [settings.performCheckStatus, []], + retrieveValueMethod: [settings.retrieveValueMethod, []], + checkStatusMethod: [settings.checkStatusMethod, [Validators.required]], + valueAttribute: [settings.valueAttribute, [Validators.required]], + parseValueFunction: [settings.parseValueFunction, []], + + // RPC settings + + requestTimeout: [settings.requestTimeout, [Validators.min(0)]], + + // --> Persistent RPC settings + + requestPersistent: [settings.requestPersistent, []], + persistentPollingInterval: [settings.persistentPollingInterval, [Validators.min(1000)]] + }); + } + + protected validatorTriggers(): string[] { + return ['performCheckStatus', 'requestPersistent']; + } + + protected updateValidators(emitEvent: boolean): void { + const performCheckStatus: boolean = this.ledIndicatorWidgetSettingsForm.get('performCheckStatus').value; + const requestPersistent: boolean = this.ledIndicatorWidgetSettingsForm.get('requestPersistent').value; + if (performCheckStatus) { + this.ledIndicatorWidgetSettingsForm.get('valueAttribute').disable({emitEvent}); + this.ledIndicatorWidgetSettingsForm.get('checkStatusMethod').enable({emitEvent}); + this.ledIndicatorWidgetSettingsForm.get('requestTimeout').enable({emitEvent}); + this.ledIndicatorWidgetSettingsForm.get('requestPersistent').enable({emitEvent: false}); + if (requestPersistent) { + this.ledIndicatorWidgetSettingsForm.get('persistentPollingInterval').enable({emitEvent}); + } else { + this.ledIndicatorWidgetSettingsForm.get('persistentPollingInterval').disable({emitEvent}); + } + } else { + this.ledIndicatorWidgetSettingsForm.get('valueAttribute').enable({emitEvent}); + this.ledIndicatorWidgetSettingsForm.get('checkStatusMethod').disable({emitEvent}); + this.ledIndicatorWidgetSettingsForm.get('requestTimeout').disable({emitEvent}); + this.ledIndicatorWidgetSettingsForm.get('requestPersistent').disable({emitEvent: false}); + this.ledIndicatorWidgetSettingsForm.get('persistentPollingInterval').disable({emitEvent}); + } + this.ledIndicatorWidgetSettingsForm.get('valueAttribute').updateValueAndValidity({emitEvent: false}); + this.ledIndicatorWidgetSettingsForm.get('checkStatusMethod').updateValueAndValidity({emitEvent: false}); + this.ledIndicatorWidgetSettingsForm.get('requestTimeout').updateValueAndValidity({emitEvent: false}); + this.ledIndicatorWidgetSettingsForm.get('requestPersistent').updateValueAndValidity({emitEvent: false}); + this.ledIndicatorWidgetSettingsForm.get('persistentPollingInterval').updateValueAndValidity({emitEvent: false}); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.html new file mode 100644 index 0000000000..1f027eb8b2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.html @@ -0,0 +1,40 @@ + +
+
+ widgets.rpc.button-style + + {{ 'widgets.rpc.button-raised' | translate }} + + + {{ 'widgets.rpc.button-primary' | translate }} + +
+ + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.ts new file mode 100644 index 0000000000..e01c4df9e0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.ts @@ -0,0 +1,98 @@ +/// +/// Copyright © 2016-2022 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 { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +export interface RpcButtonStyle { + isRaised: boolean; + isPrimary: boolean; + bgColor: string; + textColor: string; +} + +@Component({ + selector: 'tb-rpc-button-style', + templateUrl: './rpc-button-style.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RpcButtonStyleComponent), + multi: true + } + ] +}) +export class RpcButtonStyleComponent extends PageComponent implements OnInit, ControlValueAccessor { + + @Input() + disabled: boolean; + + private modelValue: RpcButtonStyle; + + private propagateChange = null; + + public rpcButtonStyleFormGroup: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.rpcButtonStyleFormGroup = this.fb.group({ + isRaised: [true, []], + isPrimary: [false, []], + bgColor: [null, []], + textColor: [null, []] + }); + this.rpcButtonStyleFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.rpcButtonStyleFormGroup.disable({emitEvent: false}); + } else { + this.rpcButtonStyleFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: RpcButtonStyle): void { + this.modelValue = value; + this.rpcButtonStyleFormGroup.patchValue( + value, {emitEvent: false} + ); + } + + private updateModel() { + this.modelValue = this.rpcButtonStyleFormGroup.value; + this.propagateChange(this.modelValue); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.html new file mode 100644 index 0000000000..67d7caa29f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.html @@ -0,0 +1,27 @@ + + +
+
+ widgets.rpc.rpc-settings + + widgets.rpc.request-timeout + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.ts new file mode 100644 index 0000000000..d901ad2636 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.ts @@ -0,0 +1,53 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-rpc-shell-widget-settings', + templateUrl: './rpc-shell-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class RpcShellWidgetSettingsComponent extends WidgetSettingsComponent { + + rpcShellWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.rpcShellWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + requestTimeout: 500 + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.rpcShellWidgetSettingsForm = this.fb.group({ + // RPC settings + requestTimeout: [settings.requestTimeout, [Validators.min(0), Validators.required]] + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.html new file mode 100644 index 0000000000..79c34bfdea --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.html @@ -0,0 +1,49 @@ + + +
+
+ widgets.rpc.rpc-settings + + widgets.rpc.request-timeout + + +
+ widgets.rpc.persistent-rpc-settings + + + + + {{ 'widgets.rpc.request-persistent' | translate }} + + + + widget-config.advanced-settings + + + + + widgets.rpc.persistent-polling-interval + + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.ts new file mode 100644 index 0000000000..906c0c71b3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.ts @@ -0,0 +1,75 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-rpc-terminal-widget-settings', + templateUrl: './rpc-terminal-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class RpcTerminalWidgetSettingsComponent extends WidgetSettingsComponent { + + rpcTerminalWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.rpcTerminalWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + requestTimeout: 500, + requestPersistent: false, + persistentPollingInterval: 5000 + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.rpcTerminalWidgetSettingsForm = this.fb.group({ + // RPC settings + + requestTimeout: [settings.requestTimeout, [Validators.min(0), Validators.required]], + + // --> Persistent RPC settings + + requestPersistent: [settings.requestPersistent, []], + persistentPollingInterval: [settings.persistentPollingInterval, [Validators.min(1000)]] + }); + } + + protected validatorTriggers(): string[] { + return ['requestPersistent']; + } + + protected updateValidators(emitEvent: boolean): void { + const requestPersistent: boolean = this.rpcTerminalWidgetSettingsForm.get('requestPersistent').value; + if (requestPersistent) { + this.rpcTerminalWidgetSettingsForm.get('persistentPollingInterval').enable({emitEvent}); + } else { + this.rpcTerminalWidgetSettingsForm.get('persistentPollingInterval').disable({emitEvent}); + } + this.rpcTerminalWidgetSettingsForm.get('persistentPollingInterval').updateValueAndValidity({emitEvent: false}); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.html new file mode 100644 index 0000000000..c36a21c349 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.html @@ -0,0 +1,79 @@ + + +
+
+ widgets.rpc.common-settings + + widgets.rpc.widget-title + + + + widgets.rpc.button-label + + +
+
+ widgets.rpc.rpc-settings + + {{ 'widgets.rpc.is-one-way-command' | translate }} + + + widgets.rpc.rpc-method + + + + + + widgets.rpc.request-timeout + + + + {{ 'widgets.rpc.show-rpc-error' | translate }} + +
+ widgets.rpc.persistent-rpc-settings + + + + + {{ 'widgets.rpc.request-persistent' | translate }} + + + + widget-config.advanced-settings + + + + + widgets.rpc.persistent-polling-interval + + + + +
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.ts new file mode 100644 index 0000000000..051b780ca9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.ts @@ -0,0 +1,99 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ContentType } from '@shared/models/constants'; + +@Component({ + selector: 'tb-send-rpc-widget-settings', + templateUrl: './send-rpc-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class SendRpcWidgetSettingsComponent extends WidgetSettingsComponent { + + sendRpcWidgetSettingsForm: FormGroup; + + contentTypes = ContentType; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.sendRpcWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + title: '', + buttonText: 'Send RPC', + oneWayElseTwoWay: true, + showError: false, + methodName: 'rpcCommand', + methodParams: '{}', + requestTimeout: 5000, + requestPersistent: false, + persistentPollingInterval: 5000, + styleButton: { + isRaised: true, + isPrimary: false, + bgColor: null, + textColor: null + } + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.sendRpcWidgetSettingsForm = this.fb.group({ + title: [settings.title, []], + buttonText: [settings.buttonText, []], + + // RPC settings + + oneWayElseTwoWay: [settings.oneWayElseTwoWay, []], + methodName: [settings.methodName, []], + methodParams: [settings.methodParams, []], + requestTimeout: [settings.requestTimeout, [Validators.min(0)]], + showError: [settings.showError, []], + + // Persistent RPC settings + + requestPersistent: [settings.requestPersistent, []], + persistentPollingInterval: [settings.persistentPollingInterval, [Validators.min(1000)]], + + styleButton: [settings.styleButton, []] + }); + } + + protected validatorTriggers(): string[] { + return ['requestPersistent']; + } + + protected updateValidators(emitEvent: boolean): void { + const requestPersistent: boolean = this.sendRpcWidgetSettingsForm.get('requestPersistent').value; + if (requestPersistent) { + this.sendRpcWidgetSettingsForm.get('persistentPollingInterval').enable({emitEvent}); + } else { + this.sendRpcWidgetSettingsForm.get('persistentPollingInterval').disable({emitEvent}); + } + this.sendRpcWidgetSettingsForm.get('persistentPollingInterval').updateValueAndValidity({emitEvent: false}); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.html index 3bb4e01573..ec2228cc78 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.html @@ -40,30 +40,15 @@ - - {{ (switchRpcSettingsFormGroup.get('retrieveValueMethod').value === 'attribute' - ? 'widgets.rpc.attribute-value-key' - : 'widgets.rpc.timeseries-value-key') | translate }} - - - - - - - - + + widgets.rpc.get-value-method diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.ts index 6be84b4241..711d0a0275 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; +import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; import { ControlValueAccessor, FormBuilder, @@ -31,10 +31,7 @@ import { AppState } from '@core/core.state'; import { TranslateService } from '@ngx-translate/core'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { WidgetService } from '@core/http/widget.service'; -import { Observable, of } from 'rxjs'; import { IAliasController } from '@core/api/widget-api.models'; -import { catchError, map, mergeMap, publishReplay, refCount, tap } from 'rxjs/operators'; -import { DataKey } from '@shared/models/widget.models'; import { EntityService } from '@core/http/entity.service'; export declare type RpcRetrieveValueMethod = 'none' | 'rpc' | 'attribute' | 'timeseries'; @@ -84,7 +81,7 @@ export function switchRpcDefaultSettings(): SwitchRpcSettings { } ] }) -export class SwitchRpcSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator, OnChanges { +export class SwitchRpcSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { @ViewChild('keyInput') keyInput: ElementRef; @@ -97,6 +94,8 @@ export class SwitchRpcSettingsComponent extends PageComponent implements OnInit, @Input() targetDeviceAliasId: string; + dataKeyType = DataKeyType; + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); private modelValue: SwitchRpcSettings; @@ -105,12 +104,6 @@ export class SwitchRpcSettingsComponent extends PageComponent implements OnInit, public switchRpcSettingsFormGroup: FormGroup; - filteredKeys: Observable>; - keySearchText = ''; - - private latestKeySearchResult: Array = null; - private keysFetchObservable$: Observable> = null; - constructor(protected store: Store, private translate: TranslateService, private widgetService: WidgetService, @@ -148,7 +141,6 @@ export class SwitchRpcSettingsComponent extends PageComponent implements OnInit, persistentPollingInterval: [5000, [Validators.min(1000)]], }); this.switchRpcSettingsFormGroup.get('retrieveValueMethod').valueChanges.subscribe(() => { - this.clearKeysCache(); this.updateValidators(true); }); this.switchRpcSettingsFormGroup.get('requestPersistent').valueChanges.subscribe(() => { @@ -158,23 +150,6 @@ export class SwitchRpcSettingsComponent extends PageComponent implements OnInit, this.updateModel(); }); this.updateValidators(false); - - this.filteredKeys = this.switchRpcSettingsFormGroup.get('valueKey').valueChanges - .pipe( - map(value => value ? value : ''), - mergeMap(name => this.fetchKeys(name) ) - ); - } - - ngOnChanges(changes: SimpleChanges): void { - for (const propName of Object.keys(changes)) { - const change = changes[propName]; - if (!change.firstChange && change.currentValue !== change.previousValue) { - if (propName === 'targetDeviceAliasId') { - this.clearKeysCache(); - } - } - } } registerOnChange(fn: any): void { @@ -201,14 +176,6 @@ export class SwitchRpcSettingsComponent extends PageComponent implements OnInit, this.updateValidators(false); } - clearKey() { - this.switchRpcSettingsFormGroup.get('valueKey').patchValue(null, {emitEvent: true}); - setTimeout(() => { - this.keyInput.nativeElement.blur(); - this.keyInput.nativeElement.focus(); - }, 0); - } - public validate(c: FormControl) { return this.switchRpcSettingsFormGroup.valid ? null : { switchRpcSettings: { @@ -249,60 +216,4 @@ export class SwitchRpcSettingsComponent extends PageComponent implements OnInit, this.switchRpcSettingsFormGroup.get('parseValueFunction').updateValueAndValidity({emitEvent: false}); this.switchRpcSettingsFormGroup.get('persistentPollingInterval').updateValueAndValidity({emitEvent: false}); } - - private clearKeysCache(): void { - this.latestKeySearchResult = null; - this.keysFetchObservable$ = null; - } - - private fetchKeys(searchText?: string): Observable> { - if (this.keySearchText !== searchText || this.latestKeySearchResult === null) { - this.keySearchText = searchText; - const dataKeyFilter = this.createKeyFilter(this.keySearchText); - return this.getKeys().pipe( - map(name => name.filter(dataKeyFilter)), - tap(res => this.latestKeySearchResult = res) - ); - } - return of(this.latestKeySearchResult); - } - - private getKeys() { - if (this.keysFetchObservable$ === null) { - let fetchObservable: Observable>; - if (this.targetDeviceAliasId) { - const retrieveValueMethod: RpcRetrieveValueMethod = this.switchRpcSettingsFormGroup.get('retrieveValueMethod').value; - const dataKeyTypes = retrieveValueMethod === 'attribute' ? [DataKeyType.attribute] : [DataKeyType.timeseries]; - fetchObservable = this.fetchEntityKeys(this.targetDeviceAliasId, dataKeyTypes); - } else { - fetchObservable = of([]); - } - this.keysFetchObservable$ = fetchObservable.pipe( - map((dataKeys) => dataKeys.map((dataKey) => dataKey.name)), - publishReplay(1), - refCount() - ); - } - return this.keysFetchObservable$; - } - - private fetchEntityKeys(entityAliasId: string, dataKeyTypes: Array): Observable> { - return this.aliasController.getAliasInfo(entityAliasId).pipe( - mergeMap((aliasInfo) => { - return this.entityService.getEntityKeysByEntityFilter( - aliasInfo.entityFilter, - dataKeyTypes, - {ignoreLoading: true, ignoreErrors: true} - ).pipe( - catchError(() => of([])) - ); - }), - catchError(() => of([] as Array)) - ); - } - - private createKeyFilter(query: string): (key: string) => boolean { - const lowercaseQuery = query.toLowerCase(); - return key => key.toLowerCase().startsWith(lowercaseQuery); - } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.html new file mode 100644 index 0000000000..ef9ee32174 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.html @@ -0,0 +1,51 @@ + + +
+
+ widgets.rpc.common-settings + + widgets.rpc.widget-title + + + + widgets.rpc.button-label + + + + widgets.rpc.device-attribute-scope + + + {{ 'widgets.rpc.server-attribute' | translate }} + + + {{ 'widgets.rpc.shared-attribute' | translate }} + + + + + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.ts new file mode 100644 index 0000000000..6689112bf7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.ts @@ -0,0 +1,68 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ContentType } from '@shared/models/constants'; + +@Component({ + selector: 'tb-update-device-attribute-widget-settings', + templateUrl: './update-device-attribute-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class UpdateDeviceAttributeWidgetSettingsComponent extends WidgetSettingsComponent { + + updateDeviceAttributeWidgetSettingsForm: FormGroup; + + contentTypes = ContentType; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.updateDeviceAttributeWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + title: '', + buttonText: 'Update device attribute', + entityAttributeType: 'SERVER_SCOPE', + entityParameters: '{}', + styleButton: { + isRaised: true, + isPrimary: false, + bgColor: null, + textColor: null + } + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.updateDeviceAttributeWidgetSettingsForm = this.fb.group({ + title: [settings.title, []], + buttonText: [settings.buttonText, []], + entityAttributeType: [settings.entityAttributeType, []], + entityParameters: [settings.entityParameters, []], + styleButton: [settings.styleButton, []] + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index e893b74b7e..c7b3948427 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -119,6 +119,28 @@ import { import { PersistentTableWidgetSettingsComponent } from '@home/components/widget/lib/settings/control/persistent-table-widget-settings.component'; +import { RpcButtonStyleComponent } from '@home/components/widget/lib/settings/control/rpc-button-style.component'; +import { + UpdateDeviceAttributeWidgetSettingsComponent +} from '@home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component'; +import { + SendRpcWidgetSettingsComponent +} from '@home/components/widget/lib/settings/control/send-rpc-widget-settings.component'; +import { + DeviceKeyAutocompleteComponent +} from '@home/components/widget/lib/settings/control/device-key-autocomplete.component'; +import { + LedIndicatorWidgetSettingsComponent +} from '@home/components/widget/lib/settings/control/led-indicator-widget-settings.component'; +import { + KnobControlWidgetSettingsComponent +} from '@home/components/widget/lib/settings/control/knob-control-widget-settings.component'; +import { + RpcTerminalWidgetSettingsComponent +} from '@home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component'; +import { + RpcShellWidgetSettingsComponent +} from '@home/components/widget/lib/settings/control/rpc-shell-widget-settings.component'; @NgModule({ declarations: [ @@ -159,11 +181,19 @@ import { FlotPieKeySettingsComponent, ChartWidgetSettingsComponent, DoughnutChartWidgetSettingsComponent, + DeviceKeyAutocompleteComponent, SwitchRpcSettingsComponent, RoundSwitchWidgetSettingsComponent, SwitchControlWidgetSettingsComponent, SlideToggleWidgetSettingsComponent, - PersistentTableWidgetSettingsComponent + PersistentTableWidgetSettingsComponent, + RpcButtonStyleComponent, + UpdateDeviceAttributeWidgetSettingsComponent, + SendRpcWidgetSettingsComponent, + LedIndicatorWidgetSettingsComponent, + KnobControlWidgetSettingsComponent, + RpcTerminalWidgetSettingsComponent, + RpcShellWidgetSettingsComponent ], imports: [ CommonModule, @@ -208,11 +238,19 @@ import { FlotPieKeySettingsComponent, ChartWidgetSettingsComponent, DoughnutChartWidgetSettingsComponent, + DeviceKeyAutocompleteComponent, SwitchRpcSettingsComponent, RoundSwitchWidgetSettingsComponent, SwitchControlWidgetSettingsComponent, SlideToggleWidgetSettingsComponent, - PersistentTableWidgetSettingsComponent + PersistentTableWidgetSettingsComponent, + RpcButtonStyleComponent, + UpdateDeviceAttributeWidgetSettingsComponent, + SendRpcWidgetSettingsComponent, + LedIndicatorWidgetSettingsComponent, + KnobControlWidgetSettingsComponent, + RpcTerminalWidgetSettingsComponent, + RpcShellWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -249,5 +287,11 @@ export const widgetSettingsComponentsMap: {[key: string]: Type -
diff --git a/ui-ngx/src/app/shared/components/json-content.component.scss b/ui-ngx/src/app/shared/components/json-content.component.scss index fd2afd9e72..124092718a 100644 --- a/ui-ngx/src/app/shared/components/json-content.component.scss +++ b/ui-ngx/src/app/shared/components/json-content.component.scss @@ -13,44 +13,47 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -:host { +.tb-json-content { position: relative; .fill-height { height: 100%; } -} -.tb-json-content-toolbar { - button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 { - align-items: center; - vertical-align: middle; - min-width: 32px; - min-height: 15px; - padding: 4px; - margin: 0; - font-size: .8rem; - line-height: 15px; - color: #7b7b7b; - background: rgba(220, 220, 220, .35); - &:not(:last-child) { - margin-right: 4px; - } + &:not(.tb-fullscreen) { + padding-bottom: 15px; } -} -.tb-json-content-panel { - height: 100%; - margin-left: 15px; - border: 1px solid #c0c0c0; + .tb-json-content-toolbar { + button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 { + align-items: center; + vertical-align: middle; + min-width: 32px; + min-height: 15px; + padding: 4px; + margin: 0; + font-size: .8rem; + line-height: 15px; + color: #7b7b7b; + background: rgba(220, 220, 220, .35); + &:not(:last-child) { + margin-right: 4px; + } + } + } - #tb-json-input { - width: 100%; - min-width: 200px; + .tb-json-content-panel { height: 100%; + border: 1px solid #c0c0c0; + + #tb-json-input { + width: 100%; + min-width: 200px; + height: 100%; - &:not(.fill-height) { - min-height: 200px; + &:not(.fill-height) { + min-height: 200px; + } } } } diff --git a/ui-ngx/src/app/shared/components/json-content.component.ts b/ui-ngx/src/app/shared/components/json-content.component.ts index 89d1a85c25..edf6e51ac8 100644 --- a/ui-ngx/src/app/shared/components/json-content.component.ts +++ b/ui-ngx/src/app/shared/components/json-content.component.ts @@ -15,6 +15,7 @@ /// import { + ChangeDetectorRef, Component, ElementRef, forwardRef, @@ -23,7 +24,7 @@ import { OnDestroy, OnInit, SimpleChanges, - ViewChild + ViewChild, ViewEncapsulation } from '@angular/core'; import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; import { Ace } from 'ace-builds'; @@ -53,7 +54,8 @@ import { beautifyJs } from '@shared/models/beautify.models'; useExisting: forwardRef(() => JsonContentComponent), multi: true, } - ] + ], + encapsulation: ViewEncapsulation.None }) export class JsonContentComponent implements OnInit, ControlValueAccessor, Validator, OnChanges, OnDestroy { @@ -116,7 +118,8 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid constructor(public elementRef: ElementRef, protected store: Store, - private raf: RafService) { + private raf: RafService, + private cd: ChangeDetectorRef) { } ngOnInit(): void { @@ -151,9 +154,12 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid this.updateView(); } }); - this.jsonEditor.on('blur', () => { - this.contentValid = !this.validateContent || this.doValidate(true); - }); + if (this.validateContent) { + this.jsonEditor.on('blur', () => { + this.contentValid = this.doValidate(true); + this.cd.markForCheck(); + }); + } this.editorResize$ = new ResizeObserver(() => { this.onAceEditorResize(); }); @@ -225,12 +231,13 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid this.propagateChange(this.contentBody); this.contentValid = this.doValidate(true); this.propagateChange(this.contentBody); + this.cd.markForCheck(); } } private doValidate(showErrorToast = false): boolean { try { - if (this.validateContent && this.contentType === ContentType.JSON) { + if (this.contentType === ContentType.JSON) { JSON.parse(this.contentBody); } return true; @@ -283,6 +290,7 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid this.contentBody = editorValue; this.contentValid = !this.validateOnChange || this.doValidate(); this.propagateChange(this.contentBody); + this.cd.markForCheck(); } } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 6f8117c77d..38b693b4a8 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3628,7 +3628,7 @@ "set-value-method": "RPC set value method", "convert-value-function": "Convert value function", "rpc-settings": "RPC settings", - "request-timeout": "RPC request timeout", + "request-timeout": "RPC request timeout (ms)", "persistent-rpc-settings": "Persistent RPC settings", "request-persistent": "RPC request persistent", "persistent-polling-interval": "Polling interval (ms) to get persistent RPC command response", @@ -3642,7 +3642,34 @@ "slider-color": "Slider color", "slider-color-primary": "Primary", "slider-color-accent": "Accent", - "slider-color-warn": "Warn" + "slider-color-warn": "Warn", + "button-style": "Button style", + "button-raised": "Raised button", + "button-primary": "Primary color", + "button-background-color": "Button background color", + "button-text-color": "Button text color", + "widget-title": "Widget title", + "button-label": "Button label", + "device-attribute-scope": "Device attribute scope", + "server-attribute": "Server attribute", + "shared-attribute": "Shared attribute", + "device-attribute-parameters": "Device attribute parameters", + "is-one-way-command": "Is one way command", + "rpc-method": "RPC method", + "rpc-method-params": "RPC method params", + "show-rpc-error": "Show RPC command execution error", + "led-title": "LED title", + "led-color": "LED color", + "check-status-settings": "Check status settings", + "perform-rpc-status-check": "Perform RPC device status check", + "retrieve-led-status-value-method": "Retrieve led status value using method", + "led-status-value-attribute": "Device attribute containing led status value", + "led-status-value-timeseries": "Device timeseries containing led status value", + "check-status-method": "RPC check device status method", + "parse-led-status-value-function": "Parse led status value function", + "knob-title": "Knob title", + "min-value": "Minimum value", + "max-value": "Maximum value" }, "maps": { "select-entity": "Select entity", From 7c7b3eade066293ff97f0103bb81cdd5f00838b3 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 26 Apr 2022 12:47:47 +0200 Subject: [PATCH 057/276] created TbDeviceService --- .../server/controller/BaseController.java | 8 +- .../server/controller/DeviceController.java | 324 ++++-------------- .../service/asset/AssetBulkImportService.java | 2 +- .../device/DeviceBulkImportService.java | 7 +- .../service/edge/EdgeBulkImportService.java | 2 +- .../entitiy/AbstractTbEntityService.java | 243 +++++++++++++ .../entitiy/DefaultTbDeviceService.java | 299 ++++++++++++++++ .../service/entitiy/TbDeviceService.java | 50 +++ .../importing/AbstractBulkImportService.java | 4 +- 9 files changed, 668 insertions(+), 271 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbDeviceService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/entitiy/TbDeviceService.java 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 f945cc30b7..1daefb7a39 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -349,8 +349,12 @@ public abstract class BaseController { } } - UUID toUUID(String id) { - return UUID.fromString(id); + UUID toUUID(String id) throws ThingsboardException { + try { + return UUID.fromString(id); + } catch (IllegalArgumentException e) { + throw handleException(e, false); + } } PageLink createPageLink(int pageSize, int page, String textSearch, String sortProperty, String sortOrder) throws ThingsboardException { diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index 85a21e912c..b755a31f4c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -37,20 +37,16 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; -import org.thingsboard.rule.engine.api.msg.DeviceCredentialsUpdateNotificationMsg; import org.thingsboard.server.common.data.ClaimRequest; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.EntitySubtype; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.device.DeviceSearchQuery; -import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; @@ -63,9 +59,6 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.security.DeviceCredentials; -import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.TbMsgDataType; -import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.device.claim.ClaimResponse; import org.thingsboard.server.dao.device.claim.ClaimResult; import org.thingsboard.server.dao.device.claim.ReclaimResult; @@ -73,7 +66,7 @@ import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.device.DeviceBulkImportService; -import org.thingsboard.server.service.gateway_device.GatewayNotificationsService; +import org.thingsboard.server.service.entitiy.TbDeviceService; import org.thingsboard.server.service.importing.BulkImportRequest; import org.thingsboard.server.service.importing.BulkImportResult; import org.thingsboard.server.service.security.model.SecurityUser; @@ -128,7 +121,7 @@ public class DeviceController extends BaseController { private final DeviceBulkImportService deviceBulkImportService; - private final GatewayNotificationsService gatewayNotificationsService; + private final TbDeviceService tbDeviceService; @ApiOperation(value = "Get Device (getDeviceById)", notes = "Fetch the Device object based on the provided Device Id. " + @@ -141,12 +134,8 @@ public class DeviceController extends BaseController { public Device getDeviceById(@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION) @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { checkParameter(DEVICE_ID, strDeviceId); - try { - DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); - return checkDeviceId(deviceId, Operation.READ); - } catch (Exception e) { - throw handleException(e); - } + DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); + return checkDeviceId(deviceId, Operation.READ); } @ApiOperation(value = "Get Device Info (getDeviceInfoById)", @@ -160,12 +149,8 @@ public class DeviceController extends BaseController { public DeviceInfo getDeviceInfoById(@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION) @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { checkParameter(DEVICE_ID, strDeviceId); - try { - DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); - return checkDeviceInfoId(deviceId, Operation.READ); - } catch (Exception e) { - throw handleException(e); - } + DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); + return checkDeviceInfoId(deviceId, Operation.READ); } @ApiOperation(value = "Create Or Update Device (saveDevice)", @@ -182,27 +167,14 @@ public class DeviceController extends BaseController { public Device saveDevice(@ApiParam(value = "A JSON value representing the device.") @RequestBody Device device, @ApiParam(value = "Optional value of the device credentials to be used during device creation. " + "If omitted, access token will be auto-generated.") @RequestParam(name = "accessToken", required = false) String accessToken) throws ThingsboardException { - boolean created = device.getId() == null; - try { - device.setTenantId(getCurrentUser().getTenantId()); - - Device oldDevice = null; - if (!created) { - oldDevice = checkDeviceId(device.getId(), Operation.WRITE); - } else { - checkEntity(null, device, Resource.DEVICE); - } - - Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken)); - - onDeviceCreatedOrUpdated(savedDevice, oldDevice, !created, getCurrentUser()); - - return savedDevice; - } catch (Exception e) { - logEntityAction(emptyId(EntityType.DEVICE), device, - null, created ? ActionType.ADDED : ActionType.UPDATED, e); - throw handleException(e); + device.setTenantId(getCurrentUser().getTenantId()); + Device oldDevice = null; + if (device.getId() != null) { + oldDevice = checkDeviceId(device.getId(), Operation.WRITE); + } else { + checkEntity(null, device, Resource.DEVICE); } + return tbDeviceService.save(getCurrentUser(), getTenantId(), device, oldDevice, accessToken); } @ApiOperation(value = "Create Device (saveDevice) with credentials ", @@ -218,36 +190,11 @@ public class DeviceController extends BaseController { @RequestBody SaveDeviceWithCredentialsRequest deviceAndCredentials) throws ThingsboardException { Device device = checkNotNull(deviceAndCredentials.getDevice()); DeviceCredentials credentials = checkNotNull(deviceAndCredentials.getCredentials()); - boolean created = device.getId() == null; - try { - device.setTenantId(getCurrentUser().getTenantId()); - checkEntity(device.getId(), device, Resource.DEVICE); - Device savedDevice = deviceService.saveDeviceWithCredentials(device, credentials); - checkNotNull(savedDevice); - tbClusterService.onDeviceUpdated(savedDevice, device); - logEntityAction(savedDevice.getId(), savedDevice, - savedDevice.getCustomerId(), - device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); - - return savedDevice; - } catch (Exception e) { - logEntityAction(emptyId(EntityType.DEVICE), device, - null, created ? ActionType.ADDED : ActionType.UPDATED, e); - throw handleException(e); - } + device.setTenantId(getCurrentUser().getTenantId()); + checkEntity(device.getId(), device, Resource.DEVICE); + return tbDeviceService.saveDeviceWithCredentials(getCurrentUser(), getTenantId(), device, credentials); } - private void onDeviceCreatedOrUpdated(Device savedDevice, Device oldDevice, boolean updated, SecurityUser user) { - tbClusterService.onDeviceUpdated(savedDevice, oldDevice); - - try { - logEntityAction(user, savedDevice.getId(), savedDevice, - savedDevice.getCustomerId(), - updated ? ActionType.UPDATED : ActionType.ADDED, null); - } catch (ThingsboardException e) { - log.error("Failed to log entity action", e); - } - } @ApiOperation(value = "Delete device (deleteDevice)", notes = "Deletes the device, it's credentials and all the relations (from and to the device). Referencing non-existing device Id will cause an error." + TENANT_AUTHORITY_PARAGRAPH) @@ -257,29 +204,9 @@ public class DeviceController extends BaseController { public void deleteDevice(@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION) @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { checkParameter(DEVICE_ID, strDeviceId); - try { - DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); - Device device = checkDeviceId(deviceId, Operation.DELETE); - - List relatedEdgeIds = findRelatedEdgeIds(getTenantId(), deviceId); - - deviceService.deleteDevice(getCurrentUser().getTenantId(), deviceId); - - gatewayNotificationsService.onDeviceDeleted(device); - tbClusterService.onDeviceDeleted(device, null); - - logEntityAction(deviceId, device, - device.getCustomerId(), - ActionType.DELETED, null, strDeviceId); - - sendDeleteNotificationMsg(getTenantId(), deviceId, relatedEdgeIds); - } catch (Exception e) { - logEntityAction(emptyId(EntityType.DEVICE), - null, - null, - ActionType.DELETED, e, strDeviceId); - throw handleException(e); - } + DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); + checkDeviceId(deviceId, Operation.DELETE); + tbDeviceService.deleteDevice(getCurrentUser(), getTenantId(), deviceId); } @ApiOperation(value = "Assign device to customer (assignDeviceToCustomer)", @@ -293,29 +220,11 @@ public class DeviceController extends BaseController { @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { checkParameter("customerId", strCustomerId); checkParameter(DEVICE_ID, strDeviceId); - try { - CustomerId customerId = new CustomerId(toUUID(strCustomerId)); - Customer customer = checkCustomerId(customerId, Operation.READ); - - DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); - checkDeviceId(deviceId, Operation.ASSIGN_TO_CUSTOMER); - - Device savedDevice = checkNotNull(deviceService.assignDeviceToCustomer(getCurrentUser().getTenantId(), deviceId, customerId)); - - logEntityAction(deviceId, savedDevice, - savedDevice.getCustomerId(), - ActionType.ASSIGNED_TO_CUSTOMER, null, strDeviceId, strCustomerId, customer.getName()); - - sendEntityAssignToCustomerNotificationMsg(savedDevice.getTenantId(), savedDevice.getId(), - customerId, EdgeEventActionType.ASSIGNED_TO_CUSTOMER); - - return savedDevice; - } catch (Exception e) { - logEntityAction(emptyId(EntityType.DEVICE), null, - null, - ActionType.ASSIGNED_TO_CUSTOMER, e, strDeviceId, strCustomerId); - throw handleException(e); - } + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + checkCustomerId(customerId, Operation.READ); + DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); + checkDeviceId(deviceId, Operation.ASSIGN_TO_CUSTOMER); + return tbDeviceService.assignDeviceToCustomer(getCurrentUser(), getTenantId(), deviceId, customerId); } @ApiOperation(value = "Unassign device from customer (unassignDeviceFromCustomer)", @@ -332,22 +241,11 @@ public class DeviceController extends BaseController { if (device.getCustomerId() == null || device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { throw new IncorrectParameterException("Device isn't assigned to any customer!"); } - Customer customer = checkCustomerId(device.getCustomerId(), Operation.READ); - Device savedDevice = checkNotNull(deviceService.unassignDeviceFromCustomer(getCurrentUser().getTenantId(), deviceId)); + checkCustomerId(device.getCustomerId(), Operation.READ); - logEntityAction(deviceId, device, - device.getCustomerId(), - ActionType.UNASSIGNED_FROM_CUSTOMER, null, strDeviceId, customer.getId().toString(), customer.getName()); - - sendEntityAssignToCustomerNotificationMsg(savedDevice.getTenantId(), savedDevice.getId(), - customer.getId(), EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER); - - return savedDevice; + return tbDeviceService.unassignDeviceFromCustomer(getCurrentUser(), getTenantId(), deviceId); } catch (Exception e) { - logEntityAction(emptyId(EntityType.DEVICE), null, - null, - ActionType.UNASSIGNED_FROM_CUSTOMER, e, strDeviceId); throw handleException(e); } } @@ -362,23 +260,9 @@ public class DeviceController extends BaseController { public Device assignDeviceToPublicCustomer(@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION) @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { checkParameter(DEVICE_ID, strDeviceId); - try { - DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); - Device device = checkDeviceId(deviceId, Operation.ASSIGN_TO_CUSTOMER); - Customer publicCustomer = customerService.findOrCreatePublicCustomer(device.getTenantId()); - Device savedDevice = checkNotNull(deviceService.assignDeviceToCustomer(getCurrentUser().getTenantId(), deviceId, publicCustomer.getId())); - - logEntityAction(deviceId, savedDevice, - savedDevice.getCustomerId(), - ActionType.ASSIGNED_TO_CUSTOMER, null, strDeviceId, publicCustomer.getId().toString(), publicCustomer.getName()); - - return savedDevice; - } catch (Exception e) { - logEntityAction(emptyId(EntityType.DEVICE), null, - null, - ActionType.ASSIGNED_TO_CUSTOMER, e, strDeviceId); - throw handleException(e); - } + DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); + checkDeviceId(deviceId, Operation.ASSIGN_TO_CUSTOMER); + return tbDeviceService.assignDeviceToPublicCustomer(getCurrentUser(), getTenantId(), deviceId); } @ApiOperation(value = "Get Device Credentials (getDeviceCredentialsByDeviceId)", @@ -389,20 +273,9 @@ public class DeviceController extends BaseController { public DeviceCredentials getDeviceCredentialsByDeviceId(@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION) @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { checkParameter(DEVICE_ID, strDeviceId); - try { - DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); - Device device = checkDeviceId(deviceId, Operation.READ_CREDENTIALS); - DeviceCredentials deviceCredentials = checkNotNull(deviceCredentialsService.findDeviceCredentialsByDeviceId(getCurrentUser().getTenantId(), deviceId)); - logEntityAction(deviceId, device, - device.getCustomerId(), - ActionType.CREDENTIALS_READ, null, strDeviceId); - return deviceCredentials; - } catch (Exception e) { - logEntityAction(emptyId(EntityType.DEVICE), null, - null, - ActionType.CREDENTIALS_READ, e, strDeviceId); - throw handleException(e); - } + DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); + checkDeviceId(deviceId, Operation.READ_CREDENTIALS); + return tbDeviceService.getDeviceCredentialsByDeviceId(getCurrentUser(), getTenantId(), deviceId); } @ApiOperation(value = "Update device credentials (updateDeviceCredentials)", notes = "During device creation, platform generates random 'ACCESS_TOKEN' credentials. " + @@ -416,23 +289,8 @@ public class DeviceController extends BaseController { @ApiParam(value = "A JSON value representing the device credentials.") @RequestBody DeviceCredentials deviceCredentials) throws ThingsboardException { checkNotNull(deviceCredentials); - try { - Device device = checkDeviceId(deviceCredentials.getDeviceId(), Operation.WRITE_CREDENTIALS); - DeviceCredentials result = checkNotNull(deviceCredentialsService.updateDeviceCredentials(getCurrentUser().getTenantId(), deviceCredentials)); - tbClusterService.pushMsgToCore(new DeviceCredentialsUpdateNotificationMsg(getCurrentUser().getTenantId(), deviceCredentials.getDeviceId(), result), null); - - sendEntityNotificationMsg(getTenantId(), device.getId(), EdgeEventActionType.CREDENTIALS_UPDATED); - - logEntityAction(device.getId(), device, - device.getCustomerId(), - ActionType.CREDENTIALS_UPDATED, null, deviceCredentials); - return result; - } catch (Exception e) { - logEntityAction(emptyId(EntityType.DEVICE), null, - null, - ActionType.CREDENTIALS_UPDATED, e, deviceCredentials); - throw handleException(e); - } + checkDeviceId(deviceCredentials.getDeviceId(), Operation.WRITE_CREDENTIALS); + return tbDeviceService.updateDeviceCredentials(getCurrentUser(), getTenantId(), deviceCredentials); } @ApiOperation(value = "Get Tenant Devices (getTenantDevices)", @@ -806,47 +664,16 @@ public class DeviceController extends BaseController { @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { checkParameter(TENANT_ID, strTenantId); checkParameter(DEVICE_ID, strDeviceId); - try { - DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); - Device device = checkDeviceId(deviceId, Operation.ASSIGN_TO_TENANT); - - TenantId newTenantId = TenantId.fromUUID(toUUID(strTenantId)); - Tenant newTenant = tenantService.findTenantById(newTenantId); - if (newTenant == null) { - throw new ThingsboardException("Could not find the specified Tenant!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); - } - - Device assignedDevice = deviceService.assignDeviceToTenant(newTenantId, device); - - logEntityAction(getCurrentUser(), deviceId, assignedDevice, - assignedDevice.getCustomerId(), - ActionType.ASSIGNED_TO_TENANT, null, strTenantId, newTenant.getName()); - - Tenant currentTenant = tenantService.findTenantById(getTenantId()); - pushAssignedFromNotification(currentTenant, newTenantId, assignedDevice); - - return assignedDevice; - } catch (Exception e) { - logEntityAction(getCurrentUser(), emptyId(EntityType.DEVICE), null, - null, - ActionType.ASSIGNED_TO_TENANT, e, strTenantId); - throw handleException(e); - } - } - - private void pushAssignedFromNotification(Tenant currentTenant, TenantId newTenantId, Device assignedDevice) { - String data = entityToStr(assignedDevice); - if (data != null) { - TbMsg tbMsg = TbMsg.newMsg(DataConstants.ENTITY_ASSIGNED_FROM_TENANT, assignedDevice.getId(), assignedDevice.getCustomerId(), getMetaDataForAssignedFrom(currentTenant), TbMsgDataType.JSON, data); - tbClusterService.pushMsgToRuleEngine(newTenantId, assignedDevice.getId(), tbMsg, null); + DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); + checkDeviceId(deviceId, Operation.ASSIGN_TO_TENANT); + + //TODO: use checkTenantId + TenantId newTenantId = TenantId.fromUUID(toUUID(strTenantId)); + Tenant newTenant = tenantService.findTenantById(newTenantId); + if (newTenant == null) { + throw new ThingsboardException("Could not find the specified Tenant!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } - } - - private TbMsgMetaData getMetaDataForAssignedFrom(Tenant tenant) { - TbMsgMetaData metaData = new TbMsgMetaData(); - metaData.putValue("assignedFromTenantId", tenant.getId().getId().toString()); - metaData.putValue("assignedFromTenantName", tenant.getName()); - return metaData; + return tbDeviceService.assignDeviceToTenant(getCurrentUser(), getTenantId(), newTenantId, deviceId); } @ApiOperation(value = "Assign device to edge (assignDeviceToEdge)", @@ -865,28 +692,13 @@ public class DeviceController extends BaseController { @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); checkParameter(DEVICE_ID, strDeviceId); - try { - EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); - Edge edge = checkEdgeId(edgeId, Operation.READ); - - DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); - checkDeviceId(deviceId, Operation.READ); + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + checkEdgeId(edgeId, Operation.READ); - Device savedDevice = checkNotNull(deviceService.assignDeviceToEdge(getCurrentUser().getTenantId(), deviceId, edgeId)); + DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); + checkDeviceId(deviceId, Operation.READ); - logEntityAction(deviceId, savedDevice, - savedDevice.getCustomerId(), - ActionType.ASSIGNED_TO_EDGE, null, strDeviceId, strEdgeId, edge.getName()); - - sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedDevice.getId(), EdgeEventActionType.ASSIGNED_TO_EDGE); - - return savedDevice; - } catch (Exception e) { - logEntityAction(emptyId(EntityType.DEVICE), null, - null, - ActionType.ASSIGNED_TO_EDGE, e, strDeviceId, strEdgeId); - throw handleException(e); - } + return tbDeviceService.assignDeviceToEdge(getCurrentUser(), getTenantId(), deviceId, edgeId); } @ApiOperation(value = "Unassign device from edge (unassignDeviceFromEdge)", @@ -905,28 +717,12 @@ public class DeviceController extends BaseController { @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); checkParameter(DEVICE_ID, strDeviceId); - try { - EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); - Edge edge = checkEdgeId(edgeId, Operation.READ); - - DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); - Device device = checkDeviceId(deviceId, Operation.READ); + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + checkEdgeId(edgeId, Operation.READ); - Device savedDevice = checkNotNull(deviceService.unassignDeviceFromEdge(getCurrentUser().getTenantId(), deviceId, edgeId)); - - logEntityAction(deviceId, device, - device.getCustomerId(), - ActionType.UNASSIGNED_FROM_EDGE, null, strDeviceId, strEdgeId, edge.getName()); - - sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedDevice.getId(), EdgeEventActionType.UNASSIGNED_FROM_EDGE); - - return savedDevice; - } catch (Exception e) { - logEntityAction(emptyId(EntityType.DEVICE), null, - null, - ActionType.UNASSIGNED_FROM_EDGE, e, strDeviceId, strEdgeId); - throw handleException(e); - } + DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); + checkDeviceId(deviceId, Operation.READ); + return tbDeviceService.unassignDeviceFromEdge(getCurrentUser(), getTenantId(), deviceId, edgeId); } @ApiOperation(value = "Get devices assigned to edge (getEdgeDevices)", @@ -992,10 +788,11 @@ public class DeviceController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/devices/count/{otaPackageType}/{deviceProfileId}", method = RequestMethod.GET) @ResponseBody - public Long countByDeviceProfileAndEmptyOtaPackage(@ApiParam(value = "OTA package type", allowableValues = "FIRMWARE, SOFTWARE") - @PathVariable("otaPackageType") String otaPackageType, - @ApiParam(value = "Device Profile Id. I.g. '784f394c-42b6-435a-983c-b7beff2784f9'") - @PathVariable("deviceProfileId") String deviceProfileId) throws ThingsboardException { + public Long countByDeviceProfileAndEmptyOtaPackage + (@ApiParam(value = "OTA package type", allowableValues = "FIRMWARE, SOFTWARE") + @PathVariable("otaPackageType") String otaPackageType, + @ApiParam(value = "Device Profile Id. I.g. '784f394c-42b6-435a-983c-b7beff2784f9'") + @PathVariable("deviceProfileId") String deviceProfileId) throws ThingsboardException { checkParameter("OtaPackageType", otaPackageType); checkParameter("DeviceProfileId", deviceProfileId); try { @@ -1012,10 +809,11 @@ public class DeviceController extends BaseController { notes = "There's an ability to import the bulk of devices using the only .csv file." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PostMapping("/device/bulk_import") - public BulkImportResult processDevicesBulkImport(@RequestBody BulkImportRequest request) throws Exception { + public BulkImportResult processDevicesBulkImport(@RequestBody BulkImportRequest request) throws + Exception { SecurityUser user = getCurrentUser(); return deviceBulkImportService.processBulkImport(request, user, importedDeviceInfo -> { - onDeviceCreatedOrUpdated(importedDeviceInfo.getEntity(), importedDeviceInfo.getOldEntity(), importedDeviceInfo.isUpdated(), user); +// onDeviceCreatedOrUpdated(importedDeviceInfo.getEntity(), importedDeviceInfo.getOldEntity(), importedDeviceInfo.isUpdated(), user); }); } diff --git a/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java index 4c75f7d45d..11adad4509 100644 --- a/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java @@ -61,7 +61,7 @@ public class AssetBulkImportService extends AbstractBulkImportService { } @Override - protected Asset saveEntity(Asset entity, Map fields) { + protected Asset saveEntity(SecurityUser user, Asset entity, Map fields) { return assetService.saveAsset(entity); } diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java index cd655a2467..00d81719e2 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java @@ -49,6 +49,7 @@ import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.TbDeviceService; import org.thingsboard.server.service.importing.AbstractBulkImportService; import org.thingsboard.server.service.importing.BulkImportColumnType; import org.thingsboard.server.service.security.model.SecurityUser; @@ -68,6 +69,7 @@ import java.util.concurrent.locks.ReentrantLock; @RequiredArgsConstructor public class DeviceBulkImportService extends AbstractBulkImportService { protected final DeviceService deviceService; + protected final TbDeviceService tbDeviceService; protected final DeviceCredentialsService deviceCredentialsService; protected final DeviceProfileService deviceProfileService; @@ -99,7 +101,8 @@ public class DeviceBulkImportService extends AbstractBulkImportService { } @Override - protected Device saveEntity(Device entity, Map fields) { + @SneakyThrows + protected Device saveEntity(SecurityUser user, Device entity, Map fields) { DeviceCredentials deviceCredentials; try { deviceCredentials = createDeviceCredentials(fields); @@ -118,7 +121,7 @@ public class DeviceBulkImportService extends AbstractBulkImportService { } entity.setDeviceProfileId(deviceProfile.getId()); - return deviceService.saveDeviceWithCredentials(entity, deviceCredentials); + return tbDeviceService.saveDeviceWithCredentials(user, user.getTenantId(), entity, deviceCredentials); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java index 615a7cddd2..117b06d224 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java @@ -67,7 +67,7 @@ public class EdgeBulkImportService extends AbstractBulkImportService { } @Override - protected Edge saveEntity(Edge entity, Map fields) { + protected Edge saveEntity(SecurityUser user, Edge entity, Map fields) { return edgeService.saveEdge(entity); } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java new file mode 100644 index 0000000000..0499e281dc --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java @@ -0,0 +1,243 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.entitiy; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.thingsboard.common.util.DonAsynchron; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmQuery; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageDataIterableByTenantIdEntityId; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.dao.alarm.AlarmOperationResult; +import org.thingsboard.server.dao.alarm.AlarmService; +import org.thingsboard.server.dao.customer.CustomerService; +import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.edge.EdgeService; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.exception.IncorrectParameterException; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.service.action.EntityActionService; +import org.thingsboard.server.service.executors.DbCallbackExecutorService; + +import javax.mail.MessagingException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Slf4j +public abstract class AbstractTbEntityService { + + protected static final int DEFAULT_PAGE_SIZE = 1000; + + private static final ObjectMapper json = new ObjectMapper(); + + @Value("${server.log_controller_error_stack_trace}") + @Getter + private boolean logControllerErrorStackTrace; + @Value("${edges.enabled}") + @Getter + protected boolean edgesEnabled; + + @Autowired + protected DbCallbackExecutorService dbExecutor; + @Autowired(required = false) + protected EdgeService edgeService; + @Autowired + protected AlarmService alarmService; + @Autowired + protected EntityActionService entityActionService; + @Autowired + protected TbClusterService tbClusterService; + @Autowired + protected DeviceService deviceService; + @Autowired + protected DeviceCredentialsService deviceCredentialsService; + @Autowired + protected TenantService tenantService; + @Autowired + protected CustomerService customerService; + + protected void removeAlarmsByEntityId(TenantId tenantId, EntityId entityId, TbCallback callback) { + ListenableFuture> alarmsFuture = + alarmService.findAlarms(tenantId, new AlarmQuery(entityId, new TimePageLink(Integer.MAX_VALUE), null, null, false)); + + ListenableFuture> alarmIdsFuture = Futures.transform(alarmsFuture, page -> + page.getData().stream().map(AlarmInfo::getId).collect(Collectors.toList()), dbExecutor); + + ListenableFuture> resultFuture = Futures.transform(alarmIdsFuture, ids -> + ids.stream().map(alarmId -> alarmService.deleteAlarm(tenantId, alarmId)).collect(Collectors.toList()), + dbExecutor); + + DonAsynchron.withCallback(resultFuture, result -> callback.onSuccess(), callback::onFailure, dbExecutor); + } + + protected void logEntityAction(User user, TenantId tenantId, I entityId, E entity, CustomerId customerId, + ActionType actionType, Exception e, Object... additionalInfo) throws ThingsboardException { + if (user != null) { + entityActionService.logEntityAction(user, entityId, entity, customerId, actionType, e, additionalInfo); + } else if (e == null) { + entityActionService.pushEntityActionToRuleEngine(entityId, entity, tenantId, customerId, actionType, null, additionalInfo); + } + } + + protected T checkNotNull(T reference) throws ThingsboardException { + return checkNotNull(reference, "Requested item wasn't found!"); + } + + protected T checkNotNull(T reference, String notFoundMessage) throws ThingsboardException { + if (reference == null) { + throw new ThingsboardException(notFoundMessage, ThingsboardErrorCode.ITEM_NOT_FOUND); + } + return reference; + } + + protected T checkNotNull(Optional reference) throws ThingsboardException { + return checkNotNull(reference, "Requested item wasn't found!"); + } + + protected T checkNotNull(Optional reference, String notFoundMessage) throws ThingsboardException { + if (reference.isPresent()) { + return reference.get(); + } else { + throw new ThingsboardException(notFoundMessage, ThingsboardErrorCode.ITEM_NOT_FOUND); + } + } + + protected ThingsboardException handleException(Exception exception) { + return handleException(exception, true); + } + + protected ThingsboardException handleException(Exception exception, boolean logException) { + if (logException && logControllerErrorStackTrace) { + log.error("Error [{}]", exception.getMessage(), exception); + } + + String cause = ""; + if (exception.getCause() != null) { + cause = exception.getCause().getClass().getCanonicalName(); + } + + if (exception instanceof ThingsboardException) { + return (ThingsboardException) exception; + } else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException + || exception instanceof DataValidationException || cause.contains("IncorrectParameterException")) { + return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } else if (exception instanceof MessagingException) { + return new ThingsboardException("Unable to send mail: " + exception.getMessage(), ThingsboardErrorCode.GENERAL); + } else { + return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.GENERAL); + } + } + + protected void sendEntityAssignToCustomerNotificationMsg(TenantId tenantId, EntityId entityId, CustomerId customerId, EdgeEventActionType action) { + try { + sendNotificationMsgToEdgeService(tenantId, null, entityId, json.writeValueAsString(customerId), null, action); + } catch (Exception e) { + log.warn("Failed to push assign/unassign to/from customer to core: {}", customerId, e); + } + } + + protected void sendDeleteNotificationMsg(TenantId tenantId, EntityId entityId, List edgeIds) { + sendDeleteNotificationMsg(tenantId, entityId, edgeIds, null); + } + + protected void sendDeleteNotificationMsg(TenantId tenantId, EntityId entityId, List edgeIds, String body) { + if (edgeIds != null && !edgeIds.isEmpty()) { + for (EdgeId edgeId : edgeIds) { + sendNotificationMsgToEdgeService(tenantId, edgeId, entityId, body, null, EdgeEventActionType.DELETED); + } + } + } + + protected void sendEntityNotificationMsg(TenantId tenantId, EntityId entityId, EdgeEventActionType action) { + sendNotificationMsgToEdgeService(tenantId, null, entityId, null, null, action); + } + + protected void sendEntityAssignToEdgeNotificationMsg(TenantId tenantId, EdgeId edgeId, EntityId entityId, EdgeEventActionType action) { + sendNotificationMsgToEdgeService(tenantId, edgeId, entityId, null, null, action); + } + + private void sendNotificationMsgToEdgeService(TenantId tenantId, EdgeId edgeId, EntityId entityId, String body, EdgeEventType type, EdgeEventActionType action) { + tbClusterService.sendNotificationMsgToEdgeService(tenantId, edgeId, entityId, body, type, action); + } + + protected void sendAlarmDeleteNotificationMsg(TenantId tenantId, EntityId entityId, List edgeIds, Alarm alarm) { + try { + sendDeleteNotificationMsg(tenantId, entityId, edgeIds, json.writeValueAsString(alarm)); + } catch (Exception e) { + log.warn("Failed to push delete alarm msg to core: {}", alarm, e); + } + } + + @SuppressWarnings("unchecked") + protected I emptyId(EntityType entityType) { + return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID); + } + + protected List findRelatedEdgeIds(TenantId tenantId, EntityId entityId) { + if (!edgesEnabled) { + return null; + } + if (EntityType.EDGE.equals(entityId.getEntityType())) { + return Collections.singletonList(new EdgeId(entityId.getId())); + } + PageDataIterableByTenantIdEntityId relatedEdgeIdsIterator = + new PageDataIterableByTenantIdEntityId<>(edgeService::findRelatedEdgeIdsByEntityId, tenantId, entityId, DEFAULT_PAGE_SIZE); + List result = new ArrayList<>(); + for (EdgeId edgeId : relatedEdgeIdsIterator) { + result.add(edgeId); + } + return result; + } + + protected String entityToStr(E entity) { + try { + return json.writeValueAsString(json.valueToTree(entity)); + } catch (JsonProcessingException e) { + log.warn("[{}] Failed to convert entity to string!", entity, e); + } + return null; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbDeviceService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbDeviceService.java new file mode 100644 index 0000000000..3e64462d2f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbDeviceService.java @@ -0,0 +1,299 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.entitiy; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.rule.engine.api.msg.DeviceCredentialsUpdateNotificationMsg; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgDataType; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.gateway_device.GatewayNotificationsService; +import org.thingsboard.server.service.security.model.SecurityUser; + +import java.util.List; + +@AllArgsConstructor +@TbCoreComponent +@Service +@Slf4j +public class DefaultTbDeviceService extends AbstractTbEntityService implements TbDeviceService { + + private final GatewayNotificationsService gatewayNotificationsService; + + @Override + public Device save(SecurityUser user, TenantId tenantId, Device device, Device oldDevice, String accessToken) throws ThingsboardException { + boolean created = device.getId() == null; + try { + Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken)); + tbClusterService.onDeviceUpdated(savedDevice, oldDevice); + + logEntityAction(user, tenantId, savedDevice.getId(), savedDevice, + savedDevice.getCustomerId(), + created ? ActionType.ADDED : ActionType.UPDATED, null); + + return savedDevice; + } catch (Exception e) { + logEntityAction(user, tenantId, emptyId(EntityType.DEVICE), device, + null, created ? ActionType.ADDED : ActionType.UPDATED, e); + throw handleException(e); + } + } + + @Override + public Device saveDeviceWithCredentials(SecurityUser user, TenantId tenantId, Device device, DeviceCredentials credentials) throws ThingsboardException { + boolean created = device.getId() == null; + try { + Device savedDevice = checkNotNull(deviceService.saveDeviceWithCredentials(device, credentials)); + tbClusterService.onDeviceUpdated(savedDevice, device); + logEntityAction(user, tenantId, savedDevice.getId(), savedDevice, + savedDevice.getCustomerId(), + created ? ActionType.ADDED : ActionType.UPDATED, null); + return savedDevice; + } catch (Exception e) { + logEntityAction(user, tenantId, emptyId(EntityType.DEVICE), device, + null, created ? ActionType.ADDED : ActionType.UPDATED, e); + throw handleException(e); + } + } + + @Override + public void deleteDevice(SecurityUser user, TenantId tenantId, DeviceId deviceId) throws ThingsboardException { + try { + Device device = deviceService.findDeviceById(tenantId, deviceId); + List relatedEdgeIds = findRelatedEdgeIds(tenantId, deviceId); + + deviceService.deleteDevice(tenantId, deviceId); + + gatewayNotificationsService.onDeviceDeleted(device); + tbClusterService.onDeviceDeleted(device, null); + + logEntityAction(user, tenantId, deviceId, device, + device.getCustomerId(), + ActionType.DELETED, null, deviceId.toString()); + + sendDeleteNotificationMsg(tenantId, deviceId, relatedEdgeIds); + } catch (Exception e) { + logEntityAction(user, tenantId, emptyId(EntityType.DEVICE), + null, + null, + ActionType.DELETED, e, deviceId.toString()); + throw handleException(e); + } + } + + @Override + public Device assignDeviceToCustomer(SecurityUser user, TenantId tenantId, DeviceId deviceId, CustomerId customerId) throws ThingsboardException { + try { + Device savedDevice = checkNotNull(deviceService.assignDeviceToCustomer(user.getTenantId(), deviceId, customerId)); + + Customer customer = customerService.findCustomerById(user.getTenantId(), customerId); + + logEntityAction(user, tenantId, deviceId, savedDevice, + savedDevice.getCustomerId(), + ActionType.ASSIGNED_TO_CUSTOMER, null, deviceId.toString(), customerId.toString(), customer.getName()); + + sendEntityAssignToCustomerNotificationMsg(savedDevice.getTenantId(), savedDevice.getId(), + customerId, EdgeEventActionType.ASSIGNED_TO_CUSTOMER); + + return savedDevice; + } catch (Exception e) { + logEntityAction(user, tenantId, emptyId(EntityType.DEVICE), null, + null, + ActionType.ASSIGNED_TO_CUSTOMER, e, deviceId.toString(), customerId.toString()); + throw handleException(e); + } + } + + @Override + public Device unassignDeviceFromCustomer(SecurityUser user, TenantId tenantId, DeviceId deviceId) throws ThingsboardException { + try { + Device device = deviceService.findDeviceById(tenantId, deviceId); + Customer customer = customerService.findCustomerById(tenantId, device.getCustomerId()); + Device savedDevice = checkNotNull(deviceService.unassignDeviceFromCustomer(tenantId, deviceId)); + + logEntityAction(user, tenantId, deviceId, device, + device.getCustomerId(), + ActionType.UNASSIGNED_FROM_CUSTOMER, null, deviceId.toString(), customer.getId().toString(), customer.getName()); + + sendEntityAssignToCustomerNotificationMsg(savedDevice.getTenantId(), savedDevice.getId(), + customer.getId(), EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER); + + return savedDevice; + } catch (Exception e) { + logEntityAction(user, tenantId, emptyId(EntityType.DEVICE), null, + null, + ActionType.UNASSIGNED_FROM_CUSTOMER, e, deviceId.toString()); + throw handleException(e); + } + } + + @Override + public Device assignDeviceToPublicCustomer(SecurityUser user, TenantId tenantId, DeviceId deviceId) throws ThingsboardException { + try { + Customer publicCustomer = customerService.findOrCreatePublicCustomer(tenantId); + Device savedDevice = checkNotNull(deviceService.assignDeviceToCustomer(tenantId, deviceId, publicCustomer.getId())); + + logEntityAction(user, tenantId, deviceId, savedDevice, + savedDevice.getCustomerId(), + ActionType.ASSIGNED_TO_CUSTOMER, null, deviceId.toString(), publicCustomer.getId().toString(), publicCustomer.getName()); + + return savedDevice; + } catch (Exception e) { + logEntityAction(user, tenantId, emptyId(EntityType.DEVICE), null, + null, + ActionType.ASSIGNED_TO_CUSTOMER, e, deviceId.toString()); + throw handleException(e); + } + } + + @Override + public DeviceCredentials getDeviceCredentialsByDeviceId(SecurityUser user, TenantId tenantId, DeviceId deviceId) throws ThingsboardException { + try { + Device device = deviceService.findDeviceById(tenantId, deviceId); + DeviceCredentials deviceCredentials = checkNotNull(deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, deviceId)); + logEntityAction(user, tenantId, deviceId, device, + device.getCustomerId(), + ActionType.CREDENTIALS_READ, null, deviceId.toString()); + return deviceCredentials; + } catch (Exception e) { + logEntityAction(user, tenantId, emptyId(EntityType.DEVICE), null, + null, + ActionType.CREDENTIALS_READ, e, deviceId.toString()); + throw handleException(e); + } + } + + @Override + public DeviceCredentials updateDeviceCredentials(SecurityUser user, TenantId tenantId, DeviceCredentials deviceCredentials) throws ThingsboardException { + try { + DeviceId deviceId = deviceCredentials.getDeviceId(); + Device device = deviceService.findDeviceById(tenantId, deviceId); + DeviceCredentials result = checkNotNull(deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials)); + tbClusterService.pushMsgToCore(new DeviceCredentialsUpdateNotificationMsg(tenantId, deviceCredentials.getDeviceId(), result), null); + + sendEntityNotificationMsg(tenantId, device.getId(), EdgeEventActionType.CREDENTIALS_UPDATED); + + logEntityAction(user, tenantId, deviceId, device, + device.getCustomerId(), + ActionType.CREDENTIALS_UPDATED, null, deviceCredentials); + return result; + } catch (Exception e) { + logEntityAction(user, tenantId, emptyId(EntityType.DEVICE), null, + null, + ActionType.CREDENTIALS_UPDATED, e, deviceCredentials); + throw handleException(e); + } + } + + @Override + public Device assignDeviceToTenant(SecurityUser user, TenantId tenantId, TenantId newTenantId, DeviceId deviceId) throws ThingsboardException { + try { + Tenant newTenant = tenantService.findTenantById(newTenantId); + Device device = deviceService.findDeviceById(tenantId, deviceId); + + Device assignedDevice = deviceService.assignDeviceToTenant(newTenantId, device); + + logEntityAction(user, tenantId, deviceId, assignedDevice, + assignedDevice.getCustomerId(), + ActionType.ASSIGNED_TO_TENANT, null, newTenantId.toString(), newTenant.getName()); + + Tenant currentTenant = tenantService.findTenantById(tenantId); + pushAssignedFromNotification(currentTenant, newTenantId, assignedDevice); + + return assignedDevice; + } catch (Exception e) { + logEntityAction(user, tenantId, emptyId(EntityType.DEVICE), null, + null, + ActionType.ASSIGNED_TO_TENANT, e, newTenantId.toString()); + throw handleException(e); + } + } + + @Override + public Device assignDeviceToEdge(SecurityUser user, TenantId tenantId, DeviceId deviceId, EdgeId edgeId) throws ThingsboardException { + try { + Device savedDevice = checkNotNull(deviceService.assignDeviceToEdge(tenantId, deviceId, edgeId)); + Edge edge = edgeService.findEdgeById(tenantId, edgeId); + + logEntityAction(user, tenantId, deviceId, savedDevice, + savedDevice.getCustomerId(), + ActionType.ASSIGNED_TO_EDGE, null, deviceId.toString(), edgeId.toString(), edge.getName()); + + sendEntityAssignToEdgeNotificationMsg(tenantId, edgeId, savedDevice.getId(), EdgeEventActionType.ASSIGNED_TO_EDGE); + + return savedDevice; + } catch (Exception e) { + logEntityAction(user, tenantId, emptyId(EntityType.DEVICE), null, + null, + ActionType.ASSIGNED_TO_EDGE, e, deviceId.toString(), edgeId.toString()); + throw handleException(e); + } + } + + @Override + public Device unassignDeviceFromEdge(SecurityUser user, TenantId tenantId, DeviceId deviceId, EdgeId edgeId) throws ThingsboardException { + try { + Device device = deviceService.findDeviceById(tenantId, deviceId); + Device savedDevice = checkNotNull(deviceService.unassignDeviceFromEdge(tenantId, deviceId, edgeId)); + Edge edge = edgeService.findEdgeById(tenantId, edgeId); + + logEntityAction(user, tenantId, deviceId, device, + device.getCustomerId(), + ActionType.UNASSIGNED_FROM_EDGE, null, deviceId.toString(), edgeId.toString(), edge.getName()); + + sendEntityAssignToEdgeNotificationMsg(tenantId, edgeId, savedDevice.getId(), EdgeEventActionType.UNASSIGNED_FROM_EDGE); + + return savedDevice; + } catch (Exception e) { + logEntityAction(user, tenantId, emptyId(EntityType.DEVICE), null, + null, + ActionType.UNASSIGNED_FROM_EDGE, e, deviceId.toString(), edgeId.toString()); + throw handleException(e); + } + } + + private void pushAssignedFromNotification(Tenant currentTenant, TenantId newTenantId, Device assignedDevice) { + String data = entityToStr(assignedDevice); + if (data != null) { + TbMsg tbMsg = TbMsg.newMsg(DataConstants.ENTITY_ASSIGNED_FROM_TENANT, assignedDevice.getId(), assignedDevice.getCustomerId(), getMetaDataForAssignedFrom(currentTenant), TbMsgDataType.JSON, data); + tbClusterService.pushMsgToRuleEngine(newTenantId, assignedDevice.getId(), tbMsg, null); + } + } + + private TbMsgMetaData getMetaDataForAssignedFrom(Tenant tenant) { + TbMsgMetaData metaData = new TbMsgMetaData(); + metaData.putValue("assignedFromTenantId", tenant.getId().getId().toString()); + metaData.putValue("assignedFromTenantName", tenant.getName()); + return metaData; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/TbDeviceService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/TbDeviceService.java new file mode 100644 index 0000000000..d4466d28a0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/TbDeviceService.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.entitiy; + +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.service.security.model.SecurityUser; + +public interface TbDeviceService { + + Device save(SecurityUser user, TenantId tenantId, Device device, Device oldDevice, String accessToken) throws ThingsboardException; + + Device saveDeviceWithCredentials(SecurityUser user, TenantId tenantId, Device device, DeviceCredentials deviceCredentials) throws ThingsboardException; + + void deleteDevice(SecurityUser user, TenantId tenantId, DeviceId deviceId) throws ThingsboardException; + + Device assignDeviceToCustomer(SecurityUser user, TenantId tenantId, DeviceId deviceId, CustomerId customerId) throws ThingsboardException; + + Device unassignDeviceFromCustomer(SecurityUser user, TenantId tenantId, DeviceId deviceId) throws ThingsboardException; + + Device assignDeviceToPublicCustomer(SecurityUser user, TenantId tenantId, DeviceId deviceId) throws ThingsboardException; + + DeviceCredentials getDeviceCredentialsByDeviceId(SecurityUser user, TenantId tenantId, DeviceId deviceId) throws ThingsboardException; + + DeviceCredentials updateDeviceCredentials(SecurityUser user, TenantId tenantId, DeviceCredentials deviceCredentials) throws ThingsboardException; + + Device assignDeviceToTenant(SecurityUser user, TenantId tenantId, TenantId newTenantId, DeviceId deviceId) throws ThingsboardException; + + Device assignDeviceToEdge(SecurityUser user, TenantId tenantId, DeviceId deviceId, EdgeId edgeId) throws ThingsboardException; + + Device unassignDeviceFromEdge(SecurityUser user, TenantId tenantId, DeviceId deviceId, EdgeId edgeId) throws ThingsboardException; +} diff --git a/application/src/main/java/org/thingsboard/server/service/importing/AbstractBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/importing/AbstractBulkImportService.java index b7b740f190..f2d8ded680 100644 --- a/application/src/main/java/org/thingsboard/server/service/importing/AbstractBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/importing/AbstractBulkImportService.java @@ -148,7 +148,7 @@ public abstract class AbstractBulkImportService fields); - protected abstract E saveEntity(E entity, Map fields); + protected abstract E saveEntity(SecurityUser user, E entity, Map fields); protected abstract EntityType getEntityType(); From 3e7d3625cbd4d9999077b84817d64b3f563c4690 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 27 Apr 2022 15:00:45 +0300 Subject: [PATCH 058/276] UI: Add date range navigator widget settings form --- .../data/json/system/widget_bundles/date.json | 3 +- ...e-navigator-widget-settings.component.html | 146 ++++++++++++++++++ ...nge-navigator-widget-settings.component.ts | 120 ++++++++++++++ .../lib/settings/widget-settings.module.ts | 12 +- .../assets/locale/locale.constant-en_US.json | 21 +++ 5 files changed, 298 insertions(+), 4 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/date/date-range-navigator-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/date/date-range-navigator-widget-settings.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/date.json b/application/src/main/data/json/system/widget_bundles/date.json index b137f66c9d..dc868235df 100644 --- a/application/src/main/data/json/system/widget_bundles/date.json +++ b/application/src/main/data/json/system/widget_bundles/date.json @@ -19,8 +19,9 @@ "templateHtml": "", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"hidePicker\": {\n \"title\": \"Hide date range picker\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"onePanel\": {\n \"title\": \"Date range picker one panel\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"autoConfirm\": {\n \"title\": \"Date range picker auto confirm\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showTemplate\": {\n \"title\": \"Date range picker show template\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"firstDayOfWeek\": {\n \"title\": \"First day of the week\",\n \"type\": \"number\",\n \"default\": 1\n },\n \"hideInterval\": {\n \"title\": \"Hide interval\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"initialInterval\": {\n\t\t\t\t\"title\": \"Initial interval\",\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"default\": \"week\"\n\t\t\t},\n \"hideStepSize\": {\n \"title\": \"Hide step size\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"stepSize\": {\n\t\t\t\t\"title\": \"Initial step size\",\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"default\": \"day\"\n\t\t\t},\n \"hideLabels\": {\n \"title\": \"Hide labels\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"useSessionStorage\": {\n \"title\": \"Use session storage\",\n \"type\": \"boolean\",\n \"default\": true\n }\n }\n },\n \"form\": [\n \"hidePicker\",\n\t\t\"onePanel\",\n\t\t\"autoConfirm\",\n\t\t\"showTemplate\",\n\t\t\"firstDayOfWeek\",\n \"hideInterval\",\n {\n\t\t\t\"key\": \"initialInterval\",\n\t\t\t\"type\": \"rc-select\",\n\t\t\t\"multiple\": false,\n\t\t\t\"items\": [\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"hour\",\n\t\t\t\t\t\"label\": \"Hour\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"day\",\n\t\t\t\t\t\"label\": \"Day\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"week\",\n\t\t\t\t\t\"label\": \"Week\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"twoWeeks\",\n\t\t\t\t\t\"label\": \"2 weeks\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"month\",\n\t\t\t\t\t\"label\": \"Month\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"threeMonths\",\n\t\t\t\t\t\"label\": \"3 months\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"sixMonths\",\n\t\t\t\t\t\"label\": \"6 months\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n \"hideStepSize\",\n {\n\t\t\t\"key\": \"stepSize\",\n\t\t\t\"type\": \"rc-select\",\n\t\t\t\"multiple\": false,\n\t\t\t\"items\": [\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"hour\",\n\t\t\t\t\t\"label\": \"Hour\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"day\",\n\t\t\t\t\t\"label\": \"Day\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"week\",\n\t\t\t\t\t\"label\": \"Week\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"twoWeeks\",\n\t\t\t\t\t\"label\": \"2 weeks\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"month\",\n\t\t\t\t\t\"label\": \"Month\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"threeMonths\",\n\t\t\t\t\t\"label\": \"3 months\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"sixMonths\",\n\t\t\t\t\t\"label\": \"6 months\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"hideLabels\",\n\t\t\"useSessionStorage\"\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-date-range-navigator-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"defaultInterval\":\"week\",\"stepSize\":\"day\"},\"title\":\"Date-range-navigator\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/date/date-range-navigator-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/date/date-range-navigator-widget-settings.component.html new file mode 100644 index 0000000000..519ef6bafa --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/date/date-range-navigator-widget-settings.component.html @@ -0,0 +1,146 @@ + +
+
+ widgets.date-range-navigator.date-range-picker-settings + + + + + {{ 'widgets.date-range-navigator.hide-date-range-picker' | translate }} + + + + widget-config.advanced-settings + + + +
+ + {{ 'widgets.date-range-navigator.picker-one-panel' | translate }} + + + {{ 'widgets.date-range-navigator.picker-auto-confirm' | translate }} + + + {{ 'widgets.date-range-navigator.picker-show-template' | translate }} + + + widgets.date-range-navigator.first-day-of-week + + +
+
+
+
+
+ widgets.date-range-navigator.interval-settings + + + + + {{ 'widgets.date-range-navigator.hide-interval' | translate }} + + + + widget-config.advanced-settings + + + + + widgets.date-range-navigator.initial-interval + + + {{ 'widgets.date-range-navigator.interval-hour' | translate }} + + + {{ 'widgets.date-range-navigator.interval-day' | translate }} + + + {{ 'widgets.date-range-navigator.interval-week' | translate }} + + + {{ 'widgets.date-range-navigator.interval-two-weeks' | translate }} + + + {{ 'widgets.date-range-navigator.interval-month' | translate }} + + + {{ 'widgets.date-range-navigator.interval-three-months' | translate }} + + + {{ 'widgets.date-range-navigator.interval-six-months' | translate }} + + + + + +
+
+ widgets.date-range-navigator.step-settings + + + + + {{ 'widgets.date-range-navigator.hide-step-size' | translate }} + + + + widget-config.advanced-settings + + + + + widgets.date-range-navigator.initial-step-size + + + {{ 'widgets.date-range-navigator.interval-hour' | translate }} + + + {{ 'widgets.date-range-navigator.interval-day' | translate }} + + + {{ 'widgets.date-range-navigator.interval-week' | translate }} + + + {{ 'widgets.date-range-navigator.interval-two-weeks' | translate }} + + + {{ 'widgets.date-range-navigator.interval-month' | translate }} + + + {{ 'widgets.date-range-navigator.interval-three-months' | translate }} + + + {{ 'widgets.date-range-navigator.interval-six-months' | translate }} + + + + + +
+ + {{ 'widgets.date-range-navigator.hide-labels' | translate }} + + + {{ 'widgets.date-range-navigator.use-session-storage' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/date/date-range-navigator-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/date/date-range-navigator-widget-settings.component.ts new file mode 100644 index 0000000000..b218fd7004 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/date/date-range-navigator-widget-settings.component.ts @@ -0,0 +1,120 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-date-range-navigator-widget-settings', + templateUrl: './date-range-navigator-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class DateRangeNavigatorWidgetSettingsComponent extends WidgetSettingsComponent { + + dateRangeNavigatorWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.dateRangeNavigatorWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + hidePicker: false, + onePanel: false, + autoConfirm: false, + showTemplate: false, + firstDayOfWeek: 1, + hideInterval: false, + initialInterval: 'week', + hideStepSize: false, + stepSize: 'day', + hideLabels: false, + useSessionStorage: true + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.dateRangeNavigatorWidgetSettingsForm = this.fb.group({ + + // Date range picker settings + + hidePicker: [settings.hidePicker, []], + onePanel: [settings.onePanel, []], + autoConfirm: [settings.autoConfirm, []], + showTemplate: [settings.showTemplate, []], + firstDayOfWeek: [settings.firstDayOfWeek, [Validators.min(1), Validators.max(7)]], + + // Interval settings + + hideInterval: [settings.hideInterval, []], + initialInterval: [settings.initialInterval, []], + + // Step settings + + hideStepSize: [settings.hideStepSize, []], + stepSize: [settings.stepSize, []], + + hideLabels: [settings.hideLabels, []], + useSessionStorage: [settings.useSessionStorage, []], + }); + } + + protected validatorTriggers(): string[] { + return ['hidePicker', 'hideInterval', 'hideStepSize']; + } + + protected updateValidators(emitEvent: boolean) { + const hidePicker: boolean = this.dateRangeNavigatorWidgetSettingsForm.get('hidePicker').value; + const hideInterval: boolean = this.dateRangeNavigatorWidgetSettingsForm.get('hideInterval').value; + const hideStepSize: boolean = this.dateRangeNavigatorWidgetSettingsForm.get('hideStepSize').value; + if (hidePicker) { + this.dateRangeNavigatorWidgetSettingsForm.get('onePanel').disable(); + this.dateRangeNavigatorWidgetSettingsForm.get('autoConfirm').disable(); + this.dateRangeNavigatorWidgetSettingsForm.get('showTemplate').disable(); + this.dateRangeNavigatorWidgetSettingsForm.get('firstDayOfWeek').disable(); + } else { + this.dateRangeNavigatorWidgetSettingsForm.get('onePanel').enable(); + this.dateRangeNavigatorWidgetSettingsForm.get('autoConfirm').enable(); + this.dateRangeNavigatorWidgetSettingsForm.get('showTemplate').enable(); + this.dateRangeNavigatorWidgetSettingsForm.get('firstDayOfWeek').enable(); + } + if (hideInterval) { + this.dateRangeNavigatorWidgetSettingsForm.get('initialInterval').disable(); + } else { + this.dateRangeNavigatorWidgetSettingsForm.get('initialInterval').enable(); + } + if (hideStepSize) { + this.dateRangeNavigatorWidgetSettingsForm.get('stepSize').disable(); + } else { + this.dateRangeNavigatorWidgetSettingsForm.get('stepSize').enable(); + } + this.dateRangeNavigatorWidgetSettingsForm.get('onePanel').updateValueAndValidity({emitEvent}); + this.dateRangeNavigatorWidgetSettingsForm.get('autoConfirm').updateValueAndValidity({emitEvent}); + this.dateRangeNavigatorWidgetSettingsForm.get('showTemplate').updateValueAndValidity({emitEvent}); + this.dateRangeNavigatorWidgetSettingsForm.get('firstDayOfWeek').updateValueAndValidity({emitEvent}); + this.dateRangeNavigatorWidgetSettingsForm.get('initialInterval').updateValueAndValidity({emitEvent}); + this.dateRangeNavigatorWidgetSettingsForm.get('stepSize').updateValueAndValidity({emitEvent}); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index c7b3948427..6dd8000b80 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -141,6 +141,9 @@ import { import { RpcShellWidgetSettingsComponent } from '@home/components/widget/lib/settings/control/rpc-shell-widget-settings.component'; +import { + DateRangeNavigatorWidgetSettingsComponent +} from '@home/components/widget/lib/settings/date/date-range-navigator-widget-settings.component'; @NgModule({ declarations: [ @@ -193,7 +196,8 @@ import { LedIndicatorWidgetSettingsComponent, KnobControlWidgetSettingsComponent, RpcTerminalWidgetSettingsComponent, - RpcShellWidgetSettingsComponent + RpcShellWidgetSettingsComponent, + DateRangeNavigatorWidgetSettingsComponent ], imports: [ CommonModule, @@ -250,7 +254,8 @@ import { LedIndicatorWidgetSettingsComponent, KnobControlWidgetSettingsComponent, RpcTerminalWidgetSettingsComponent, - RpcShellWidgetSettingsComponent + RpcShellWidgetSettingsComponent, + DateRangeNavigatorWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -293,5 +298,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type Date: Wed, 27 Apr 2022 14:55:47 +0200 Subject: [PATCH 059/276] added TbNotificationEntityService --- .../server/controller/DeviceController.java | 99 +++--- .../device/DeviceBulkImportService.java | 2 +- .../entitiy/AbstractTbEntityService.java | 72 +---- .../entitiy/DefaultTbDeviceService.java | 299 ------------------ .../DefaultTbNotificationEntityService.java | 220 +++++++++++++ .../entitiy/TbNotificationEntityService.java | 56 ++++ .../device/DefaultTbDeviceService.java | 273 ++++++++++++++++ .../entitiy/{ => device}/TbDeviceService.java | 11 +- 8 files changed, 608 insertions(+), 424 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbDeviceService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java rename application/src/main/java/org/thingsboard/server/service/entitiy/{ => device}/TbDeviceService.java (79%) diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index b755a31f4c..467a883129 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -38,14 +38,12 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; import org.thingsboard.server.common.data.ClaimRequest; -import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.device.DeviceSearchQuery; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -66,7 +64,7 @@ import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.device.DeviceBulkImportService; -import org.thingsboard.server.service.entitiy.TbDeviceService; +import org.thingsboard.server.service.entitiy.device.TbDeviceService; import org.thingsboard.server.service.importing.BulkImportRequest; import org.thingsboard.server.service.importing.BulkImportResult; import org.thingsboard.server.service.security.model.SecurityUser; @@ -74,7 +72,6 @@ import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; import javax.annotation.Nullable; -import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -206,7 +203,11 @@ public class DeviceController extends BaseController { checkParameter(DEVICE_ID, strDeviceId); DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); checkDeviceId(deviceId, Operation.DELETE); - tbDeviceService.deleteDevice(getCurrentUser(), getTenantId(), deviceId); + try { + tbDeviceService.deleteDevice(getCurrentUser(), getTenantId(), deviceId).get(); + } catch (Exception e) { + throw handleException(e); + } } @ApiOperation(value = "Assign device to customer (assignDeviceToCustomer)", @@ -550,52 +551,42 @@ public class DeviceController extends BaseController { @ApiParam(value = "Claiming request which can optionally contain secret key") @RequestBody(required = false) ClaimRequest claimRequest) throws ThingsboardException { checkParameter(DEVICE_NAME, deviceName); - try { - final DeferredResult deferredResult = new DeferredResult<>(); + final DeferredResult deferredResult = new DeferredResult<>(); - SecurityUser user = getCurrentUser(); - TenantId tenantId = user.getTenantId(); - CustomerId customerId = user.getCustomerId(); - - Device device = checkNotNull(deviceService.findDeviceByTenantIdAndName(tenantId, deviceName)); - accessControlService.checkPermission(user, Resource.DEVICE, Operation.CLAIM_DEVICES, - device.getId(), device); - String secretKey = getSecretKey(claimRequest); - - ListenableFuture future = claimDevicesService.claimDevice(device, customerId, secretKey); - Futures.addCallback(future, new FutureCallback() { - @Override - public void onSuccess(@Nullable ClaimResult result) { - HttpStatus status; - if (result != null) { - if (result.getResponse().equals(ClaimResponse.SUCCESS)) { - status = HttpStatus.OK; - deferredResult.setResult(new ResponseEntity<>(result, status)); - - try { - logEntityAction(user, device.getId(), result.getDevice(), customerId, ActionType.ASSIGNED_TO_CUSTOMER, null, - device.getId().toString(), customerId.toString(), customerService.findCustomerById(tenantId, customerId).getName()); - } catch (ThingsboardException e) { - throw new RuntimeException(e); - } - } else { - status = HttpStatus.BAD_REQUEST; - deferredResult.setResult(new ResponseEntity<>(result.getResponse(), status)); - } + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + CustomerId customerId = user.getCustomerId(); + + Device device = checkNotNull(deviceService.findDeviceByTenantIdAndName(tenantId, deviceName)); + accessControlService.checkPermission(user, Resource.DEVICE, Operation.CLAIM_DEVICES, + device.getId(), device); + String secretKey = getSecretKey(claimRequest); + + ListenableFuture future = tbDeviceService.claimDevice(tenantId, device, customerId, secretKey, user); + + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable ClaimResult result) { + HttpStatus status; + if (result != null) { + if (result.getResponse().equals(ClaimResponse.SUCCESS)) { + status = HttpStatus.OK; + deferredResult.setResult(new ResponseEntity<>(result, status)); } else { - deferredResult.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST)); + status = HttpStatus.BAD_REQUEST; + deferredResult.setResult(new ResponseEntity<>(result.getResponse(), status)); } + } else { + deferredResult.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST)); } + } - @Override - public void onFailure(Throwable t) { - deferredResult.setErrorResult(t); - } - }, MoreExecutors.directExecutor()); - return deferredResult; - } catch (Exception e) { - throw handleException(e); - } + @Override + public void onFailure(Throwable t) { + deferredResult.setErrorResult(t); + } + }, MoreExecutors.directExecutor()); + return deferredResult; } @ApiOperation(value = "Reclaim device (reClaimDevice)", @@ -617,21 +608,11 @@ public class DeviceController extends BaseController { accessControlService.checkPermission(user, Resource.DEVICE, Operation.CLAIM_DEVICES, device.getId(), device); - ListenableFuture result = claimDevicesService.reClaimDevice(tenantId, device); + ListenableFuture result = tbDeviceService.reclaimDevice(tenantId, device, user); Futures.addCallback(result, new FutureCallback<>() { @Override public void onSuccess(ReclaimResult reclaimResult) { deferredResult.setResult(new ResponseEntity(HttpStatus.OK)); - - Customer unassignedCustomer = reclaimResult.getUnassignedCustomer(); - if (unassignedCustomer != null) { - try { - logEntityAction(user, device.getId(), device, device.getCustomerId(), ActionType.UNASSIGNED_FROM_CUSTOMER, null, - device.getId().toString(), unassignedCustomer.getId().toString(), unassignedCustomer.getName()); - } catch (ThingsboardException e) { - throw new RuntimeException(e); - } - } } @Override @@ -645,7 +626,7 @@ public class DeviceController extends BaseController { } } - private String getSecretKey(ClaimRequest claimRequest) throws IOException { + private String getSecretKey(ClaimRequest claimRequest) { String secretKey = claimRequest.getSecretKey(); if (secretKey != null) { return secretKey; @@ -667,7 +648,6 @@ public class DeviceController extends BaseController { DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); checkDeviceId(deviceId, Operation.ASSIGN_TO_TENANT); - //TODO: use checkTenantId TenantId newTenantId = TenantId.fromUUID(toUUID(strTenantId)); Tenant newTenant = tenantService.findTenantById(newTenantId); if (newTenant == null) { @@ -813,7 +793,6 @@ public class DeviceController extends BaseController { Exception { SecurityUser user = getCurrentUser(); return deviceBulkImportService.processBulkImport(request, user, importedDeviceInfo -> { -// onDeviceCreatedOrUpdated(importedDeviceInfo.getEntity(), importedDeviceInfo.getOldEntity(), importedDeviceInfo.isUpdated(), user); }); } diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java index 00d81719e2..7b8fdc325a 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java @@ -49,7 +49,7 @@ import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.entitiy.TbDeviceService; +import org.thingsboard.server.service.entitiy.device.TbDeviceService; import org.thingsboard.server.service.importing.AbstractBulkImportService; import org.thingsboard.server.service.importing.BulkImportColumnType; import org.thingsboard.server.service.security.model.SecurityUser; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java index 0499e281dc..97c6907740 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.entitiy; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -23,17 +22,13 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.audit.ActionType; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; -import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AlarmId; @@ -45,10 +40,9 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageDataIterableByTenantIdEntityId; import org.thingsboard.server.common.data.page.TimePageLink; -import org.thingsboard.server.common.msg.queue.TbCallback; -import org.thingsboard.server.dao.alarm.AlarmOperationResult; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.customer.CustomerService; +import org.thingsboard.server.dao.device.ClaimDevicesService; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.edge.EdgeService; @@ -82,6 +76,8 @@ public abstract class AbstractTbEntityService { @Autowired protected DbCallbackExecutorService dbExecutor; + @Autowired + protected TbNotificationEntityService notificationEntityService; @Autowired(required = false) protected EdgeService edgeService; @Autowired @@ -98,19 +94,20 @@ public abstract class AbstractTbEntityService { protected TenantService tenantService; @Autowired protected CustomerService customerService; + @Autowired + protected ClaimDevicesService claimDevicesService; - protected void removeAlarmsByEntityId(TenantId tenantId, EntityId entityId, TbCallback callback) { + protected ListenableFuture removeAlarmsByEntityId(TenantId tenantId, EntityId entityId) { ListenableFuture> alarmsFuture = alarmService.findAlarms(tenantId, new AlarmQuery(entityId, new TimePageLink(Integer.MAX_VALUE), null, null, false)); ListenableFuture> alarmIdsFuture = Futures.transform(alarmsFuture, page -> page.getData().stream().map(AlarmInfo::getId).collect(Collectors.toList()), dbExecutor); - ListenableFuture> resultFuture = Futures.transform(alarmIdsFuture, ids -> - ids.stream().map(alarmId -> alarmService.deleteAlarm(tenantId, alarmId)).collect(Collectors.toList()), - dbExecutor); - - DonAsynchron.withCallback(resultFuture, result -> callback.onSuccess(), callback::onFailure, dbExecutor); + return Futures.transform(alarmIdsFuture, ids -> { + ids.stream().map(alarmId -> alarmService.deleteAlarm(tenantId, alarmId)).collect(Collectors.toList()); + return null; + }, dbExecutor); } protected void logEntityAction(User user, TenantId tenantId, I entityId, E entity, CustomerId customerId, @@ -171,46 +168,6 @@ public abstract class AbstractTbEntityService { } } - protected void sendEntityAssignToCustomerNotificationMsg(TenantId tenantId, EntityId entityId, CustomerId customerId, EdgeEventActionType action) { - try { - sendNotificationMsgToEdgeService(tenantId, null, entityId, json.writeValueAsString(customerId), null, action); - } catch (Exception e) { - log.warn("Failed to push assign/unassign to/from customer to core: {}", customerId, e); - } - } - - protected void sendDeleteNotificationMsg(TenantId tenantId, EntityId entityId, List edgeIds) { - sendDeleteNotificationMsg(tenantId, entityId, edgeIds, null); - } - - protected void sendDeleteNotificationMsg(TenantId tenantId, EntityId entityId, List edgeIds, String body) { - if (edgeIds != null && !edgeIds.isEmpty()) { - for (EdgeId edgeId : edgeIds) { - sendNotificationMsgToEdgeService(tenantId, edgeId, entityId, body, null, EdgeEventActionType.DELETED); - } - } - } - - protected void sendEntityNotificationMsg(TenantId tenantId, EntityId entityId, EdgeEventActionType action) { - sendNotificationMsgToEdgeService(tenantId, null, entityId, null, null, action); - } - - protected void sendEntityAssignToEdgeNotificationMsg(TenantId tenantId, EdgeId edgeId, EntityId entityId, EdgeEventActionType action) { - sendNotificationMsgToEdgeService(tenantId, edgeId, entityId, null, null, action); - } - - private void sendNotificationMsgToEdgeService(TenantId tenantId, EdgeId edgeId, EntityId entityId, String body, EdgeEventType type, EdgeEventActionType action) { - tbClusterService.sendNotificationMsgToEdgeService(tenantId, edgeId, entityId, body, type, action); - } - - protected void sendAlarmDeleteNotificationMsg(TenantId tenantId, EntityId entityId, List edgeIds, Alarm alarm) { - try { - sendDeleteNotificationMsg(tenantId, entityId, edgeIds, json.writeValueAsString(alarm)); - } catch (Exception e) { - log.warn("Failed to push delete alarm msg to core: {}", alarm, e); - } - } - @SuppressWarnings("unchecked") protected I emptyId(EntityType entityType) { return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID); @@ -231,13 +188,4 @@ public abstract class AbstractTbEntityService { } return result; } - - protected String entityToStr(E entity) { - try { - return json.writeValueAsString(json.valueToTree(entity)); - } catch (JsonProcessingException e) { - log.warn("[{}] Failed to convert entity to string!", entity, e); - } - return null; - } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbDeviceService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbDeviceService.java deleted file mode 100644 index 3e64462d2f..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbDeviceService.java +++ /dev/null @@ -1,299 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.service.entitiy; - -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.thingsboard.rule.engine.api.msg.DeviceCredentialsUpdateNotificationMsg; -import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.DataConstants; -import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.audit.ActionType; -import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; -import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.security.DeviceCredentials; -import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.TbMsgDataType; -import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.gateway_device.GatewayNotificationsService; -import org.thingsboard.server.service.security.model.SecurityUser; - -import java.util.List; - -@AllArgsConstructor -@TbCoreComponent -@Service -@Slf4j -public class DefaultTbDeviceService extends AbstractTbEntityService implements TbDeviceService { - - private final GatewayNotificationsService gatewayNotificationsService; - - @Override - public Device save(SecurityUser user, TenantId tenantId, Device device, Device oldDevice, String accessToken) throws ThingsboardException { - boolean created = device.getId() == null; - try { - Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken)); - tbClusterService.onDeviceUpdated(savedDevice, oldDevice); - - logEntityAction(user, tenantId, savedDevice.getId(), savedDevice, - savedDevice.getCustomerId(), - created ? ActionType.ADDED : ActionType.UPDATED, null); - - return savedDevice; - } catch (Exception e) { - logEntityAction(user, tenantId, emptyId(EntityType.DEVICE), device, - null, created ? ActionType.ADDED : ActionType.UPDATED, e); - throw handleException(e); - } - } - - @Override - public Device saveDeviceWithCredentials(SecurityUser user, TenantId tenantId, Device device, DeviceCredentials credentials) throws ThingsboardException { - boolean created = device.getId() == null; - try { - Device savedDevice = checkNotNull(deviceService.saveDeviceWithCredentials(device, credentials)); - tbClusterService.onDeviceUpdated(savedDevice, device); - logEntityAction(user, tenantId, savedDevice.getId(), savedDevice, - savedDevice.getCustomerId(), - created ? ActionType.ADDED : ActionType.UPDATED, null); - return savedDevice; - } catch (Exception e) { - logEntityAction(user, tenantId, emptyId(EntityType.DEVICE), device, - null, created ? ActionType.ADDED : ActionType.UPDATED, e); - throw handleException(e); - } - } - - @Override - public void deleteDevice(SecurityUser user, TenantId tenantId, DeviceId deviceId) throws ThingsboardException { - try { - Device device = deviceService.findDeviceById(tenantId, deviceId); - List relatedEdgeIds = findRelatedEdgeIds(tenantId, deviceId); - - deviceService.deleteDevice(tenantId, deviceId); - - gatewayNotificationsService.onDeviceDeleted(device); - tbClusterService.onDeviceDeleted(device, null); - - logEntityAction(user, tenantId, deviceId, device, - device.getCustomerId(), - ActionType.DELETED, null, deviceId.toString()); - - sendDeleteNotificationMsg(tenantId, deviceId, relatedEdgeIds); - } catch (Exception e) { - logEntityAction(user, tenantId, emptyId(EntityType.DEVICE), - null, - null, - ActionType.DELETED, e, deviceId.toString()); - throw handleException(e); - } - } - - @Override - public Device assignDeviceToCustomer(SecurityUser user, TenantId tenantId, DeviceId deviceId, CustomerId customerId) throws ThingsboardException { - try { - Device savedDevice = checkNotNull(deviceService.assignDeviceToCustomer(user.getTenantId(), deviceId, customerId)); - - Customer customer = customerService.findCustomerById(user.getTenantId(), customerId); - - logEntityAction(user, tenantId, deviceId, savedDevice, - savedDevice.getCustomerId(), - ActionType.ASSIGNED_TO_CUSTOMER, null, deviceId.toString(), customerId.toString(), customer.getName()); - - sendEntityAssignToCustomerNotificationMsg(savedDevice.getTenantId(), savedDevice.getId(), - customerId, EdgeEventActionType.ASSIGNED_TO_CUSTOMER); - - return savedDevice; - } catch (Exception e) { - logEntityAction(user, tenantId, emptyId(EntityType.DEVICE), null, - null, - ActionType.ASSIGNED_TO_CUSTOMER, e, deviceId.toString(), customerId.toString()); - throw handleException(e); - } - } - - @Override - public Device unassignDeviceFromCustomer(SecurityUser user, TenantId tenantId, DeviceId deviceId) throws ThingsboardException { - try { - Device device = deviceService.findDeviceById(tenantId, deviceId); - Customer customer = customerService.findCustomerById(tenantId, device.getCustomerId()); - Device savedDevice = checkNotNull(deviceService.unassignDeviceFromCustomer(tenantId, deviceId)); - - logEntityAction(user, tenantId, deviceId, device, - device.getCustomerId(), - ActionType.UNASSIGNED_FROM_CUSTOMER, null, deviceId.toString(), customer.getId().toString(), customer.getName()); - - sendEntityAssignToCustomerNotificationMsg(savedDevice.getTenantId(), savedDevice.getId(), - customer.getId(), EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER); - - return savedDevice; - } catch (Exception e) { - logEntityAction(user, tenantId, emptyId(EntityType.DEVICE), null, - null, - ActionType.UNASSIGNED_FROM_CUSTOMER, e, deviceId.toString()); - throw handleException(e); - } - } - - @Override - public Device assignDeviceToPublicCustomer(SecurityUser user, TenantId tenantId, DeviceId deviceId) throws ThingsboardException { - try { - Customer publicCustomer = customerService.findOrCreatePublicCustomer(tenantId); - Device savedDevice = checkNotNull(deviceService.assignDeviceToCustomer(tenantId, deviceId, publicCustomer.getId())); - - logEntityAction(user, tenantId, deviceId, savedDevice, - savedDevice.getCustomerId(), - ActionType.ASSIGNED_TO_CUSTOMER, null, deviceId.toString(), publicCustomer.getId().toString(), publicCustomer.getName()); - - return savedDevice; - } catch (Exception e) { - logEntityAction(user, tenantId, emptyId(EntityType.DEVICE), null, - null, - ActionType.ASSIGNED_TO_CUSTOMER, e, deviceId.toString()); - throw handleException(e); - } - } - - @Override - public DeviceCredentials getDeviceCredentialsByDeviceId(SecurityUser user, TenantId tenantId, DeviceId deviceId) throws ThingsboardException { - try { - Device device = deviceService.findDeviceById(tenantId, deviceId); - DeviceCredentials deviceCredentials = checkNotNull(deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, deviceId)); - logEntityAction(user, tenantId, deviceId, device, - device.getCustomerId(), - ActionType.CREDENTIALS_READ, null, deviceId.toString()); - return deviceCredentials; - } catch (Exception e) { - logEntityAction(user, tenantId, emptyId(EntityType.DEVICE), null, - null, - ActionType.CREDENTIALS_READ, e, deviceId.toString()); - throw handleException(e); - } - } - - @Override - public DeviceCredentials updateDeviceCredentials(SecurityUser user, TenantId tenantId, DeviceCredentials deviceCredentials) throws ThingsboardException { - try { - DeviceId deviceId = deviceCredentials.getDeviceId(); - Device device = deviceService.findDeviceById(tenantId, deviceId); - DeviceCredentials result = checkNotNull(deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials)); - tbClusterService.pushMsgToCore(new DeviceCredentialsUpdateNotificationMsg(tenantId, deviceCredentials.getDeviceId(), result), null); - - sendEntityNotificationMsg(tenantId, device.getId(), EdgeEventActionType.CREDENTIALS_UPDATED); - - logEntityAction(user, tenantId, deviceId, device, - device.getCustomerId(), - ActionType.CREDENTIALS_UPDATED, null, deviceCredentials); - return result; - } catch (Exception e) { - logEntityAction(user, tenantId, emptyId(EntityType.DEVICE), null, - null, - ActionType.CREDENTIALS_UPDATED, e, deviceCredentials); - throw handleException(e); - } - } - - @Override - public Device assignDeviceToTenant(SecurityUser user, TenantId tenantId, TenantId newTenantId, DeviceId deviceId) throws ThingsboardException { - try { - Tenant newTenant = tenantService.findTenantById(newTenantId); - Device device = deviceService.findDeviceById(tenantId, deviceId); - - Device assignedDevice = deviceService.assignDeviceToTenant(newTenantId, device); - - logEntityAction(user, tenantId, deviceId, assignedDevice, - assignedDevice.getCustomerId(), - ActionType.ASSIGNED_TO_TENANT, null, newTenantId.toString(), newTenant.getName()); - - Tenant currentTenant = tenantService.findTenantById(tenantId); - pushAssignedFromNotification(currentTenant, newTenantId, assignedDevice); - - return assignedDevice; - } catch (Exception e) { - logEntityAction(user, tenantId, emptyId(EntityType.DEVICE), null, - null, - ActionType.ASSIGNED_TO_TENANT, e, newTenantId.toString()); - throw handleException(e); - } - } - - @Override - public Device assignDeviceToEdge(SecurityUser user, TenantId tenantId, DeviceId deviceId, EdgeId edgeId) throws ThingsboardException { - try { - Device savedDevice = checkNotNull(deviceService.assignDeviceToEdge(tenantId, deviceId, edgeId)); - Edge edge = edgeService.findEdgeById(tenantId, edgeId); - - logEntityAction(user, tenantId, deviceId, savedDevice, - savedDevice.getCustomerId(), - ActionType.ASSIGNED_TO_EDGE, null, deviceId.toString(), edgeId.toString(), edge.getName()); - - sendEntityAssignToEdgeNotificationMsg(tenantId, edgeId, savedDevice.getId(), EdgeEventActionType.ASSIGNED_TO_EDGE); - - return savedDevice; - } catch (Exception e) { - logEntityAction(user, tenantId, emptyId(EntityType.DEVICE), null, - null, - ActionType.ASSIGNED_TO_EDGE, e, deviceId.toString(), edgeId.toString()); - throw handleException(e); - } - } - - @Override - public Device unassignDeviceFromEdge(SecurityUser user, TenantId tenantId, DeviceId deviceId, EdgeId edgeId) throws ThingsboardException { - try { - Device device = deviceService.findDeviceById(tenantId, deviceId); - Device savedDevice = checkNotNull(deviceService.unassignDeviceFromEdge(tenantId, deviceId, edgeId)); - Edge edge = edgeService.findEdgeById(tenantId, edgeId); - - logEntityAction(user, tenantId, deviceId, device, - device.getCustomerId(), - ActionType.UNASSIGNED_FROM_EDGE, null, deviceId.toString(), edgeId.toString(), edge.getName()); - - sendEntityAssignToEdgeNotificationMsg(tenantId, edgeId, savedDevice.getId(), EdgeEventActionType.UNASSIGNED_FROM_EDGE); - - return savedDevice; - } catch (Exception e) { - logEntityAction(user, tenantId, emptyId(EntityType.DEVICE), null, - null, - ActionType.UNASSIGNED_FROM_EDGE, e, deviceId.toString(), edgeId.toString()); - throw handleException(e); - } - } - - private void pushAssignedFromNotification(Tenant currentTenant, TenantId newTenantId, Device assignedDevice) { - String data = entityToStr(assignedDevice); - if (data != null) { - TbMsg tbMsg = TbMsg.newMsg(DataConstants.ENTITY_ASSIGNED_FROM_TENANT, assignedDevice.getId(), assignedDevice.getCustomerId(), getMetaDataForAssignedFrom(currentTenant), TbMsgDataType.JSON, data); - tbClusterService.pushMsgToRuleEngine(newTenantId, assignedDevice.getId(), tbMsg, null); - } - } - - private TbMsgMetaData getMetaDataForAssignedFrom(Tenant tenant) { - TbMsgMetaData metaData = new TbMsgMetaData(); - metaData.putValue("assignedFromTenantId", tenant.getId().getId().toString()); - metaData.putValue("assignedFromTenantName", tenant.getName()); - return metaData; - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java new file mode 100644 index 0000000000..ae508f5e3a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java @@ -0,0 +1,220 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.entitiy; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.thingsboard.rule.engine.api.msg.DeviceCredentialsUpdateNotificationMsg; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageDataIterableByTenantIdEntityId; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgDataType; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.edge.EdgeService; +import org.thingsboard.server.service.action.EntityActionService; +import org.thingsboard.server.service.gateway_device.GatewayNotificationsService; +import org.thingsboard.server.service.security.model.SecurityUser; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class DefaultTbNotificationEntityService implements TbNotificationEntityService { + + protected static final int DEFAULT_PAGE_SIZE = 1000; + + private static final ObjectMapper json = new ObjectMapper(); + + @Value("${edges.enabled}") + @Getter + protected boolean edgesEnabled; + + private final EntityActionService entityActionService; + private final TbClusterService tbClusterService; + private final GatewayNotificationsService gatewayNotificationsService; + private final EdgeService edgeService; + + @Override + public void sendNotification(TenantId tenantId, I entityId, E entity, CustomerId customerId, + ActionType actionType, SecurityUser user, Exception e, + Object... additionalInfo) { + logEntityAction(tenantId, entityId, entity, customerId, actionType, user, e, additionalInfo); + } + + //Device + @Override + public void notifyCreateOrUpdateDevice(TenantId tenantId, DeviceId deviceId, CustomerId customerId, + Device device, Device oldDevice, ActionType actionType, + SecurityUser user, Object... additionalInfo) { + tbClusterService.onDeviceUpdated(device, oldDevice); + logEntityAction(tenantId, deviceId, device, customerId, actionType, user, additionalInfo); + } + + @Override + public void notifyDeleteDevice(TenantId tenantId, DeviceId deviceId, CustomerId customerId, Device device, + SecurityUser user, Object... additionalInfo) { + gatewayNotificationsService.onDeviceDeleted(device); + tbClusterService.onDeviceDeleted(device, null); + + logEntityAction(tenantId, deviceId, device, customerId, ActionType.DELETED, user, additionalInfo); + + List relatedEdgeIds = findRelatedEdgeIds(tenantId, deviceId); + sendDeleteNotificationMsg(tenantId, deviceId, relatedEdgeIds); + } + + @Override + public void notifyAssignOrUnassignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, CustomerId customerId, + Device device, ActionType actionType, EdgeEventActionType edgeActionType, + SecurityUser user, boolean sendToEdge, Object... additionalInfo) { + logEntityAction(tenantId, deviceId, device, customerId, actionType, user, additionalInfo); + + if (sendToEdge) { + sendEntityAssignToCustomerNotificationMsg(tenantId, deviceId, customerId, edgeActionType); + } + } + + @Override + public void notifyUpdateDeviceCredentials(TenantId tenantId, DeviceId deviceId, CustomerId customerId, Device device, + DeviceCredentials deviceCredentials, SecurityUser user) { + tbClusterService.pushMsgToCore(new DeviceCredentialsUpdateNotificationMsg(tenantId, deviceCredentials.getDeviceId(), deviceCredentials), null); + sendEntityNotificationMsg(tenantId, deviceId, EdgeEventActionType.CREDENTIALS_UPDATED); + logEntityAction(tenantId, deviceId, device, customerId, ActionType.CREDENTIALS_UPDATED, user, deviceCredentials); + } + + @Override + public void notifyAssignDeviceToTenant(TenantId tenantId, TenantId newTenantId, DeviceId deviceId, CustomerId customerId, + Device device, Tenant tenant, SecurityUser user, Object... additionalInfo) { + logEntityAction(tenantId, deviceId, device, customerId, ActionType.ASSIGNED_TO_TENANT, user, additionalInfo); + pushAssignedFromNotification(tenant, newTenantId, device); + } + + @Override + public void notifyAssignOrUnassignDeviceToEdge(TenantId tenantId, DeviceId deviceId, CustomerId customerId, EdgeId edgeId, + Device device, ActionType actionType, EdgeEventActionType edgeActionType, + SecurityUser user, Object... additionalInfo) { + logEntityAction(tenantId, deviceId, device, customerId, actionType, user, additionalInfo); + sendEntityAssignToEdgeNotificationMsg(tenantId, edgeId, deviceId, edgeActionType); + } + + private void logEntityAction(TenantId tenantId, I entityId, E entity, CustomerId customerId, + ActionType actionType, SecurityUser user, Object... additionalInfo) { + logEntityAction(tenantId, entityId, entity, customerId, actionType, user, null, additionalInfo); + } + + private void logEntityAction(TenantId tenantId, I entityId, E entity, CustomerId customerId, + ActionType actionType, SecurityUser user, Exception e, Object... additionalInfo) { + if (user != null) { + entityActionService.logEntityAction(user, entityId, entity, customerId, actionType, e, additionalInfo); + } else if (e == null) { + entityActionService.pushEntityActionToRuleEngine(entityId, entity, tenantId, customerId, actionType, null, additionalInfo); + } + } + + protected void sendEntityNotificationMsg(TenantId tenantId, EntityId entityId, EdgeEventActionType action) { + sendNotificationMsgToEdgeService(tenantId, null, entityId, null, null, action); + } + + protected void sendEntityAssignToCustomerNotificationMsg(TenantId tenantId, EntityId entityId, CustomerId customerId, EdgeEventActionType action) { + try { + sendNotificationMsgToEdgeService(tenantId, null, entityId, json.writeValueAsString(customerId), null, action); + } catch (Exception e) { + log.warn("Failed to push assign/unassign to/from customer to core: {}", customerId, e); + } + } + + protected void sendDeleteNotificationMsg(TenantId tenantId, EntityId entityId, List edgeIds) { + sendDeleteNotificationMsg(tenantId, entityId, edgeIds, null); + } + + protected void sendDeleteNotificationMsg(TenantId tenantId, EntityId entityId, List edgeIds, String body) { + if (edgeIds != null && !edgeIds.isEmpty()) { + for (EdgeId edgeId : edgeIds) { + sendNotificationMsgToEdgeService(tenantId, edgeId, entityId, body, null, EdgeEventActionType.DELETED); + } + } + } + + protected void sendEntityAssignToEdgeNotificationMsg(TenantId tenantId, EdgeId edgeId, EntityId entityId, EdgeEventActionType action) { + sendNotificationMsgToEdgeService(tenantId, edgeId, entityId, null, null, action); + } + + private void sendNotificationMsgToEdgeService(TenantId tenantId, EdgeId edgeId, EntityId entityId, String body, EdgeEventType type, EdgeEventActionType action) { + tbClusterService.sendNotificationMsgToEdgeService(tenantId, edgeId, entityId, body, type, action); + } + + private List findRelatedEdgeIds(TenantId tenantId, EntityId entityId) { + if (!edgesEnabled) { + return null; + } + if (EntityType.EDGE.equals(entityId.getEntityType())) { + return Collections.singletonList(new EdgeId(entityId.getId())); + } + PageDataIterableByTenantIdEntityId relatedEdgeIdsIterator = + new PageDataIterableByTenantIdEntityId<>(edgeService::findRelatedEdgeIdsByEntityId, tenantId, entityId, DEFAULT_PAGE_SIZE); + List result = new ArrayList<>(); + for (EdgeId edgeId : relatedEdgeIdsIterator) { + result.add(edgeId); + } + return result; + } + + private void pushAssignedFromNotification(Tenant currentTenant, TenantId newTenantId, Device assignedDevice) { + String data = entityToStr(assignedDevice); + if (data != null) { + TbMsg tbMsg = TbMsg.newMsg(DataConstants.ENTITY_ASSIGNED_FROM_TENANT, assignedDevice.getId(), assignedDevice.getCustomerId(), getMetaDataForAssignedFrom(currentTenant), TbMsgDataType.JSON, data); + tbClusterService.pushMsgToRuleEngine(newTenantId, assignedDevice.getId(), tbMsg, null); + } + } + + private TbMsgMetaData getMetaDataForAssignedFrom(Tenant tenant) { + TbMsgMetaData metaData = new TbMsgMetaData(); + metaData.putValue("assignedFromTenantId", tenant.getId().getId().toString()); + metaData.putValue("assignedFromTenantName", tenant.getName()); + return metaData; + } + + private String entityToStr(E entity) { + try { + return json.writeValueAsString(json.valueToTree(entity)); + } catch (JsonProcessingException e) { + log.warn("[{}] Failed to convert entity to string!", entity, e); + } + return null; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java new file mode 100644 index 0000000000..4cd56f47fb --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.entitiy; + +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.service.security.model.SecurityUser; + +public interface TbNotificationEntityService { + + void sendNotification(TenantId tenantId, I entityId, E entity, CustomerId customerId, + ActionType actionType, SecurityUser user, Exception e, + Object... additionalInfo); + + void notifyCreateOrUpdateDevice(TenantId tenantId, DeviceId deviceId, CustomerId customerId, Device device, + Device oldDevice, ActionType actionType, SecurityUser user, Object... additionalInfo); + + void notifyDeleteDevice(TenantId tenantId, DeviceId deviceId, CustomerId customerId, Device device, + SecurityUser user, Object... additionalInfo); + + void notifyAssignOrUnassignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, CustomerId customerId, + Device device, ActionType actionType, EdgeEventActionType edgeActionType, + SecurityUser user, boolean sendToEdge, Object... additionalInfo); + + void notifyUpdateDeviceCredentials(TenantId tenantId, DeviceId deviceId, CustomerId customerId, Device device, + DeviceCredentials deviceCredentials, SecurityUser user); + + void notifyAssignDeviceToTenant(TenantId tenantId, TenantId newTenantId, DeviceId deviceId, CustomerId customerId, + Device device, Tenant tenant, SecurityUser user, Object... additionalInfo); + + void notifyAssignOrUnassignDeviceToEdge(TenantId tenantId, DeviceId deviceId, CustomerId customerId, EdgeId edgeId, + Device device, ActionType actionType, EdgeEventActionType edgeActionType, + SecurityUser user, Object... additionalInfo); +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java new file mode 100644 index 0000000000..6e13b89358 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java @@ -0,0 +1,273 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.entitiy.device; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.dao.device.claim.ClaimResponse; +import org.thingsboard.server.dao.device.claim.ClaimResult; +import org.thingsboard.server.dao.device.claim.ReclaimResult; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.AbstractTbEntityService; +import org.thingsboard.server.service.security.model.SecurityUser; + +@AllArgsConstructor +@TbCoreComponent +@Service +@Slf4j +public class DefaultTbDeviceService extends AbstractTbEntityService implements TbDeviceService { + + @Override + public Device save(SecurityUser user, TenantId tenantId, Device device, Device oldDevice, String accessToken) throws ThingsboardException { + ActionType actionType = device.getId() == null ? ActionType.ADDED : ActionType.UPDATED; + try { + Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken)); + notificationEntityService.notifyCreateOrUpdateDevice(tenantId, savedDevice.getId(), savedDevice.getCustomerId(), + savedDevice, oldDevice, actionType, user); + + return savedDevice; + } catch (Exception e) { + notificationEntityService.sendNotification(tenantId, emptyId(EntityType.DEVICE), device, null, actionType, user, e); + throw handleException(e); + } + } + + @Override + public Device saveDeviceWithCredentials(SecurityUser user, TenantId tenantId, Device device, DeviceCredentials credentials) throws ThingsboardException { + ActionType actionType = device.getId() == null ? ActionType.ADDED : ActionType.UPDATED; + try { + Device savedDevice = checkNotNull(deviceService.saveDeviceWithCredentials(device, credentials)); + notificationEntityService.notifyCreateOrUpdateDevice(tenantId, savedDevice.getId(), savedDevice.getCustomerId(), + savedDevice, device, actionType, user); + + return savedDevice; + } catch (Exception e) { + notificationEntityService.sendNotification(tenantId, emptyId(EntityType.DEVICE), device, null, actionType, user, e); + throw handleException(e); + } + } + + @Override + public ListenableFuture deleteDevice(SecurityUser user, TenantId tenantId, DeviceId deviceId) throws ThingsboardException { + try { + Device device = deviceService.findDeviceById(tenantId, deviceId); + deviceService.deleteDevice(tenantId, deviceId); + notificationEntityService.notifyDeleteDevice(tenantId, deviceId, device.getCustomerId(), device, user, deviceId.toString()); + return removeAlarmsByEntityId(tenantId, deviceId); + } catch (Exception e) { + notificationEntityService.sendNotification(tenantId, emptyId(EntityType.DEVICE), null, null, + ActionType.DELETED, user, e, deviceId.toString()); + throw handleException(e); + } + } + + @Override + public Device assignDeviceToCustomer(SecurityUser user, TenantId tenantId, DeviceId deviceId, CustomerId customerId) throws ThingsboardException { + ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER; + try { + Device savedDevice = checkNotNull(deviceService.assignDeviceToCustomer(user.getTenantId(), deviceId, customerId)); + + Customer customer = customerService.findCustomerById(user.getTenantId(), customerId); + + notificationEntityService.notifyAssignOrUnassignDeviceToCustomer(tenantId, deviceId, customerId, savedDevice, + actionType, EdgeEventActionType.ASSIGNED_TO_CUSTOMER, user, true, customerId.toString(), customer.getName()); + + return savedDevice; + } catch (Exception e) { + notificationEntityService.sendNotification(tenantId, emptyId(EntityType.DEVICE), null, null, + actionType, user, e, deviceId.toString(), customerId.toString()); + throw handleException(e); + } + } + + @Override + public Device unassignDeviceFromCustomer(SecurityUser user, TenantId tenantId, DeviceId deviceId) throws ThingsboardException { + ActionType actionType = ActionType.UNASSIGNED_FROM_CUSTOMER; + try { + Device device = deviceService.findDeviceById(tenantId, deviceId); + Customer customer = customerService.findCustomerById(tenantId, device.getCustomerId()); + Device savedDevice = checkNotNull(deviceService.unassignDeviceFromCustomer(tenantId, deviceId)); + CustomerId customerId = customer.getId(); + + notificationEntityService.notifyAssignOrUnassignDeviceToCustomer(tenantId, deviceId, customerId, savedDevice, + actionType, EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER, user, + true, customerId.toString(), customer.getName()); + + return savedDevice; + } catch (Exception e) { + notificationEntityService.sendNotification(tenantId, emptyId(EntityType.DEVICE), null, null, + actionType, user, e, false, deviceId.toString()); + throw handleException(e); + } + } + + @Override + public Device assignDeviceToPublicCustomer(SecurityUser user, TenantId tenantId, DeviceId deviceId) throws ThingsboardException { + ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER; + try { + Customer publicCustomer = customerService.findOrCreatePublicCustomer(tenantId); + Device savedDevice = checkNotNull(deviceService.assignDeviceToCustomer(tenantId, deviceId, publicCustomer.getId())); + + notificationEntityService.notifyAssignOrUnassignDeviceToCustomer(tenantId, deviceId, savedDevice.getCustomerId(), savedDevice, + actionType, null, user, false, deviceId.toString(), + publicCustomer.getId().toString(), publicCustomer.getName()); + + return savedDevice; + } catch (Exception e) { + notificationEntityService.sendNotification(tenantId, emptyId(EntityType.DEVICE), null, null, + actionType, user, e, false, deviceId.toString()); + throw handleException(e); + } + } + + @Override + public DeviceCredentials getDeviceCredentialsByDeviceId(SecurityUser user, TenantId tenantId, DeviceId deviceId) throws ThingsboardException { + ActionType actionType = ActionType.CREDENTIALS_READ; + try { + Device device = deviceService.findDeviceById(tenantId, deviceId); + DeviceCredentials deviceCredentials = checkNotNull(deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, deviceId)); + notificationEntityService.sendNotification(tenantId, deviceId, device, device.getCustomerId(), + actionType, user, null, deviceId.toString()); + return deviceCredentials; + } catch (Exception e) { + notificationEntityService.sendNotification(tenantId, emptyId(EntityType.DEVICE), null, null, + actionType, user, e, deviceId.toString()); + throw handleException(e); + } + } + + @Override + public DeviceCredentials updateDeviceCredentials(SecurityUser user, TenantId tenantId, DeviceCredentials deviceCredentials) throws ThingsboardException { + try { + DeviceId deviceId = deviceCredentials.getDeviceId(); + Device device = deviceService.findDeviceById(tenantId, deviceId); + DeviceCredentials result = checkNotNull(deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials)); + notificationEntityService.notifyUpdateDeviceCredentials(tenantId, deviceId, device.getCustomerId(), device, result, user); + return result; + } catch (Exception e) { + notificationEntityService.sendNotification(tenantId, emptyId(EntityType.DEVICE), null, null, + ActionType.CREDENTIALS_UPDATED, user, e, deviceCredentials); + throw handleException(e); + } + } + + @Override + public ListenableFuture claimDevice(TenantId tenantId, Device device, CustomerId customerId, String secretKey, SecurityUser user) throws ThingsboardException { + try { + ListenableFuture future = claimDevicesService.claimDevice(device, customerId, secretKey); + + return Futures.transform(future, result -> { + if (result != null && result.getResponse().equals(ClaimResponse.SUCCESS)) { + notificationEntityService.sendNotification(tenantId, device.getId(), result.getDevice(), customerId, + ActionType.ASSIGNED_TO_CUSTOMER, user, null, device.getId().toString(), customerId.toString(), + customerService.findCustomerById(tenantId, customerId).getName()); + } + return result; + }, MoreExecutors.directExecutor()); + } catch (Exception e) { + throw handleException(e); + } + } + + @Override + public ListenableFuture reclaimDevice(TenantId tenantId, Device device, SecurityUser user) throws ThingsboardException { + try { + ListenableFuture future = claimDevicesService.reClaimDevice(tenantId, device); + + return Futures.transform(future, result -> { + Customer unassignedCustomer = result.getUnassignedCustomer(); + if (unassignedCustomer != null) { + notificationEntityService.sendNotification(tenantId, device.getId(), device, device.getCustomerId(), ActionType.UNASSIGNED_FROM_CUSTOMER, user, null, + device.getId().toString(), unassignedCustomer.getId().toString(), unassignedCustomer.getName()); + } + return result; + }, MoreExecutors.directExecutor()); + } catch (Exception e) { + throw handleException(e); + } + } + + @Override + public Device assignDeviceToTenant(SecurityUser user, TenantId tenantId, TenantId newTenantId, DeviceId deviceId) throws ThingsboardException { + try { + Tenant tenant = tenantService.findTenantById(tenantId); + Tenant newTenant = tenantService.findTenantById(newTenantId); + Device device = deviceService.findDeviceById(tenantId, deviceId); + Device assignedDevice = deviceService.assignDeviceToTenant(newTenantId, device); + + notificationEntityService.notifyAssignDeviceToTenant(tenantId, newTenantId, deviceId, + assignedDevice.getCustomerId(), assignedDevice, tenant, user, newTenantId.toString(), newTenant.getName()); + + return assignedDevice; + } catch (Exception e) { + notificationEntityService.sendNotification(tenantId, emptyId(EntityType.DEVICE), null, null, + ActionType.ASSIGNED_TO_TENANT, user, e, newTenantId.toString()); + throw handleException(e); + } + } + + @Override + public Device assignDeviceToEdge(SecurityUser user, TenantId tenantId, DeviceId deviceId, EdgeId edgeId) throws ThingsboardException { + ActionType actionType = ActionType.ASSIGNED_TO_EDGE; + try { + Device savedDevice = checkNotNull(deviceService.assignDeviceToEdge(tenantId, deviceId, edgeId)); + Edge edge = edgeService.findEdgeById(tenantId, edgeId); + notificationEntityService.notifyAssignOrUnassignDeviceToEdge(tenantId, deviceId, savedDevice.getCustomerId(), + edgeId, savedDevice, actionType, EdgeEventActionType.ASSIGNED_TO_EDGE, user, deviceId.toString(), edgeId.toString(), edge.getName()); + return savedDevice; + } catch (Exception e) { + notificationEntityService.sendNotification(tenantId, emptyId(EntityType.DEVICE), null, null, + actionType, user, e, deviceId.toString(), edgeId.toString()); + throw handleException(e); + } + } + + @Override + public Device unassignDeviceFromEdge(SecurityUser user, TenantId tenantId, DeviceId deviceId, EdgeId edgeId) throws ThingsboardException { + ActionType actionType = ActionType.UNASSIGNED_FROM_EDGE; + try { + Device device = deviceService.findDeviceById(tenantId, deviceId); + Device savedDevice = checkNotNull(deviceService.unassignDeviceFromEdge(tenantId, deviceId, edgeId)); + Edge edge = edgeService.findEdgeById(tenantId, edgeId); + + notificationEntityService.notifyAssignOrUnassignDeviceToEdge(tenantId, deviceId, device.getCustomerId(), + edgeId, device, actionType, EdgeEventActionType.UNASSIGNED_FROM_EDGE, user, deviceId.toString(), edgeId.toString(), edge.getName()); + return savedDevice; + } catch (Exception e) { + notificationEntityService.sendNotification(tenantId, emptyId(EntityType.DEVICE), null, null, + actionType, user, e, deviceId.toString(), edgeId.toString()); + throw handleException(e); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/TbDeviceService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java similarity index 79% rename from application/src/main/java/org/thingsboard/server/service/entitiy/TbDeviceService.java rename to application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java index d4466d28a0..52fbf1da8a 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/TbDeviceService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.entitiy; +package org.thingsboard.server.service.entitiy.device; +import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; @@ -22,6 +23,8 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.dao.device.claim.ClaimResult; +import org.thingsboard.server.dao.device.claim.ReclaimResult; import org.thingsboard.server.service.security.model.SecurityUser; public interface TbDeviceService { @@ -30,7 +33,7 @@ public interface TbDeviceService { Device saveDeviceWithCredentials(SecurityUser user, TenantId tenantId, Device device, DeviceCredentials deviceCredentials) throws ThingsboardException; - void deleteDevice(SecurityUser user, TenantId tenantId, DeviceId deviceId) throws ThingsboardException; + ListenableFuture deleteDevice(SecurityUser user, TenantId tenantId, DeviceId deviceId) throws ThingsboardException; Device assignDeviceToCustomer(SecurityUser user, TenantId tenantId, DeviceId deviceId, CustomerId customerId) throws ThingsboardException; @@ -42,6 +45,10 @@ public interface TbDeviceService { DeviceCredentials updateDeviceCredentials(SecurityUser user, TenantId tenantId, DeviceCredentials deviceCredentials) throws ThingsboardException; + ListenableFuture claimDevice(TenantId tenantId, Device device, CustomerId customerId, String secretKey, SecurityUser user) throws ThingsboardException; + + ListenableFuture reclaimDevice(TenantId tenantId, Device device, SecurityUser user) throws ThingsboardException; + Device assignDeviceToTenant(SecurityUser user, TenantId tenantId, TenantId newTenantId, DeviceId deviceId) throws ThingsboardException; Device assignDeviceToEdge(SecurityUser user, TenantId tenantId, DeviceId deviceId, EdgeId edgeId) throws ThingsboardException; From 5db8992e5246198d1631205fbb683c5c435a61b9 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 27 Apr 2022 16:25:27 +0300 Subject: [PATCH 060/276] UI: Add edge quick overview widget settings form --- .../system/widget_bundles/edge_widgets.json | 5 +- ...ck-overview-widget-settings.component.html | 22 ++++++++ ...uick-overview-widget-settings.component.ts | 52 +++++++++++++++++++ .../lib/settings/widget-settings.module.ts | 12 +++-- .../assets/locale/locale.constant-en_US.json | 3 ++ 5 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/edge-quick-overview-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/edge-quick-overview-widget-settings.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/edge_widgets.json b/application/src/main/data/json/system/widget_bundles/edge_widgets.json index 4a5edfbde9..5d9c9526fa 100644 --- a/application/src/main/data/json/system/widget_bundles/edge_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/edge_widgets.json @@ -19,10 +19,11 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n dataKeysOptional: true\n };\n}\n\nself.onDestroy = function() {\n};\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EdgeOverviewSettings\",\n \"properties\": {\n \"enableDefaultTitle\": {\n \"title\": \"Display default title\",\n \"type\": \"boolean\",\n \"default\": true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"enableDefaultTitle\"\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-edge-quick-overview-widget-settings", "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"showTitleIcon\":true,\"titleIcon\":\"router\",\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{},\"title\":\"Edge Quick Overview\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"widgetStyle\":{},\"actions\":{}}" } } ] -} +} \ No newline at end of file diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/edge-quick-overview-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/edge-quick-overview-widget-settings.component.html new file mode 100644 index 0000000000..e2f403eb46 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/edge-quick-overview-widget-settings.component.html @@ -0,0 +1,22 @@ + +
+ + {{ 'widgets.edge.display-default-title' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/edge-quick-overview-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/edge-quick-overview-widget-settings.component.ts new file mode 100644 index 0000000000..2d26f5e76d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/edge-quick-overview-widget-settings.component.ts @@ -0,0 +1,52 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-edge-quick-overview-widget-settings', + templateUrl: './edge-quick-overview-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class EdgeQuickOverviewWidgetSettingsComponent extends WidgetSettingsComponent { + + edgeQuickOverviewWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.edgeQuickOverviewWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + enableDefaultTitle: true + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.edgeQuickOverviewWidgetSettingsForm = this.fb.group({ + enableDefaultTitle: [settings.enableDefaultTitle, []] + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index 6dd8000b80..f48749446b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -144,6 +144,9 @@ import { import { DateRangeNavigatorWidgetSettingsComponent } from '@home/components/widget/lib/settings/date/date-range-navigator-widget-settings.component'; +import { + EdgeQuickOverviewWidgetSettingsComponent +} from '@home/components/widget/lib/settings/cards/edge-quick-overview-widget-settings.component'; @NgModule({ declarations: [ @@ -197,7 +200,8 @@ import { KnobControlWidgetSettingsComponent, RpcTerminalWidgetSettingsComponent, RpcShellWidgetSettingsComponent, - DateRangeNavigatorWidgetSettingsComponent + DateRangeNavigatorWidgetSettingsComponent, + EdgeQuickOverviewWidgetSettingsComponent ], imports: [ CommonModule, @@ -255,7 +259,8 @@ import { KnobControlWidgetSettingsComponent, RpcTerminalWidgetSettingsComponent, RpcShellWidgetSettingsComponent, - DateRangeNavigatorWidgetSettingsComponent + DateRangeNavigatorWidgetSettingsComponent, + EdgeQuickOverviewWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -299,5 +304,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type Date: Wed, 27 Apr 2022 16:30:02 +0300 Subject: [PATCH 061/276] UI: Update entity admin widgets settings --- .../system/widget_bundles/entity_admin_widgets.json | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json b/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json index 4e5115bb98..3257ab1892 100644 --- a/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json @@ -19,8 +19,10 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyHeader\": {\n \"title\": \"Always display header\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyAction\": {\n \"title\": \"Always display actions column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"reserveSpaceForHiddenAction\": {\n \"title\": \"Hidden cell button actions display mode\",\n \"type\": \"string\",\n \"default\": \"true\"\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n },\n \"useRowStyleFunction\": {\n \"title\": \"Use row style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"rowStyleFunction\": {\n \"title\": \"Row style function: f(entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"enableStickyHeader\",\n \"enableStickyAction\",\n {\n \"key\": \"reserveSpaceForHiddenAction\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"true\",\n \"label\": \"Show empty space instead of hidden cell button action\"\n },\n {\n \"value\": \"false\",\n \"label\": \"Don't reserve space for hidden action buttons\"\n }\n ]\n },\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\",\n \"useRowStyleFunction\",\n {\n \"key\": \"rowStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entity/row_style_fn\",\n \"condition\": \"model.useRowStyleFunction === true\"\n }\n ]\n}", - "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value, entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"defaultColumnVisibility\": {\n \"title\": \"Default column visibility\",\n \"type\": \"string\",\n \"default\": \"visible\"\n },\n \"columnSelectionToDisplay\": {\n \"title\": \"Column selection in 'Columns to Display'\",\n \"type\": \"string\",\n \"default\": \"enabled\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entity/cell_style_fn\",\n \"condition\": \"model.useCellStyleFunction === true\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entity/cell_content_fn\",\n \"condition\": \"model.useCellContentFunction === true\"\n },\n {\n \"key\": \"defaultColumnVisibility\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"visible\",\n \"label\": \"Visible\"\n },\n {\n \"value\": \"hidden\",\n \"label\": \"Hidden\"\n } \n ]\n },\n {\n \"key\": \"columnSelectionToDisplay\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"enabled\",\n \"label\": \"Enabled\"\n },\n {\n \"value\": \"disabled\",\n \"label\": \"Disabled\"\n } \n ]\n }\n ]\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-entities-table-widget-settings", + "dataKeySettingsDirective": "tb-entities-table-key-settings", "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Device admin table\",\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"reserveSpaceForHiddenAction\":\"true\",\"displayEntityLabel\":false,\"useRowStyleFunction\":false},\"title\":\"Device admin table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"name\":\"Add device\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"\\n \\n

Add device

\\n \\n \\n
\\n \\n \\n
\\n
\\n
\\n \\n Device name\\n \\n \\n Device name is required.\\n \\n \\n
\\n \\n \\n Label\\n \\n \\n
\\n
\\n \\n Latitude\\n \\n \\n \\n Longitude\\n \\n \\n
\\n
\\n
\\n
\\n \\n \\n \\n
\\n\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenAddDeviceDialog();\\n\\nfunction openAddDeviceDialog() {\\n customDialog.customDialog(htmlTemplate, AddDeviceDialogController).subscribe();\\n}\\n\\nfunction AddDeviceDialogController(instance) {\\n let vm = instance;\\n \\n vm.addDeviceFormGroup = vm.fb.group({\\n deviceName: ['', [vm.validators.required]],\\n deviceType: ['', [vm.validators.required]],\\n deviceLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.addDeviceFormGroup.markAsPristine();\\n let device = {\\n name: vm.addDeviceFormGroup.get('deviceName').value,\\n type: vm.addDeviceFormGroup.get('deviceType').value,\\n label: vm.addDeviceFormGroup.get('deviceLabel').value\\n };\\n deviceService.saveDevice(device).subscribe(\\n function (device) {\\n saveAttributes(device.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributes = vm.addDeviceFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\"}],\"actionCellButton\":[{\"name\":\"Edit device\",\"icon\":\"edit\",\"useShowWidgetActionFunction\":null,\"showWidgetActionFunction\":\"return true;\",\"type\":\"customPretty\",\"customHtml\":\"
\\n \\n

Edit device

\\n \\n \\n
\\n \\n \\n
\\n
\\n
\\n \\n Device name\\n \\n \\n Device name is required.\\n \\n \\n
\\n \\n \\n Label\\n \\n \\n
\\n
\\n \\n Latitude\\n \\n \\n \\n Longitude\\n \\n \\n
\\n
\\n
\\n
\\n \\n \\n \\n
\\n
\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenEditDeviceDialog();\\n\\nfunction openEditDeviceDialog() {\\n customDialog.customDialog(htmlTemplate, EditDeviceDialogController).subscribe();\\n}\\n\\nfunction EditDeviceDialogController(instance) {\\n let vm = instance;\\n \\n vm.device = null;\\n vm.attributes = {};\\n \\n vm.editDeviceFormGroup = vm.fb.group({\\n deviceName: ['', [vm.validators.required]],\\n deviceType: ['', [vm.validators.required]],\\n deviceLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.editDeviceFormGroup.markAsPristine();\\n if (vm.editDeviceFormGroup.get('deviceType').value !== vm.device.type) {\\n delete vm.device.deviceProfileId;\\n }\\n vm.device.name = vm.editDeviceFormGroup.get('deviceName').value,\\n vm.device.type = vm.editDeviceFormGroup.get('deviceType').value,\\n vm.device.label = vm.editDeviceFormGroup.get('deviceLabel').value\\n deviceService.saveDevice(vm.device).subscribe(\\n function () {\\n saveAttributes().subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n deviceService.getDevice(entityId.id).subscribe(\\n function (device) {\\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE',\\n ['latitude', 'longitude']).subscribe(\\n function (attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n vm.device = device;\\n vm.editDeviceFormGroup.patchValue(\\n {\\n deviceName: vm.device.name,\\n deviceType: vm.device.type,\\n deviceLabel: vm.device.label,\\n attributes: {\\n latitude: vm.attributes.latitude,\\n longitude: vm.attributes.longitude\\n }\\n }, {emitEvent: false}\\n );\\n } \\n );\\n }\\n ); \\n }\\n \\n function saveAttributes() {\\n let attributes = vm.editDeviceFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, 'SERVER_SCOPE', attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"openInSeparateDialog\":false,\"openInPopover\":false,\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\"},{\"name\":\"Delete device\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));\\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\\n\\nopenDeleteDeviceDialog();\\n\\nfunction openDeleteDeviceDialog() {\\n let title = \\\"Are you sure you want to delete the device \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the device and all related data will become unrecoverable!\\\";\\n dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(\\n function (result) {\\n if (result) {\\n deleteDevice();\\n }\\n }\\n );\\n}\\n\\nfunction deleteDevice() {\\n deviceService.deleteDevice(entityId.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n }\\n );\\n}\\n\",\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\"}]}}" } }, @@ -37,8 +39,10 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyHeader\": {\n \"title\": \"Always display header\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyAction\": {\n \"title\": \"Always display actions column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"reserveSpaceForHiddenAction\": {\n \"title\": \"Hidden cell button actions display mode\",\n \"type\": \"string\",\n \"default\": \"true\"\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n },\n \"useRowStyleFunction\": {\n \"title\": \"Use row style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"rowStyleFunction\": {\n \"title\": \"Row style function: f(entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"enableStickyHeader\",\n \"enableStickyAction\",\n {\n \"key\": \"reserveSpaceForHiddenAction\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"true\",\n \"label\": \"Show empty space instead of hidden cell button action\"\n },\n {\n \"value\": \"false\",\n \"label\": \"Don't reserve space for hidden action buttons\"\n }\n ]\n },\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\",\n \"useRowStyleFunction\",\n {\n \"key\": \"rowStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entity/row_style_fn\",\n \"condition\": \"model.useRowStyleFunction === true\"\n }\n ]\n}", - "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value, entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"defaultColumnVisibility\": {\n \"title\": \"Default column visibility\",\n \"type\": \"string\",\n \"default\": \"visible\"\n },\n \"columnSelectionToDisplay\": {\n \"title\": \"Column selection in 'Columns to Display'\",\n \"type\": \"string\",\n \"default\": \"enabled\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entity/cell_style_fn\",\n \"condition\": \"model.useCellStyleFunction === true\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entity/cell_content_fn\",\n \"condition\": \"model.useCellContentFunction === true\"\n },\n {\n \"key\": \"defaultColumnVisibility\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"visible\",\n \"label\": \"Visible\"\n },\n {\n \"value\": \"hidden\",\n \"label\": \"Hidden\"\n } \n ]\n },\n {\n \"key\": \"columnSelectionToDisplay\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"enabled\",\n \"label\": \"Enabled\"\n },\n {\n \"value\": \"disabled\",\n \"label\": \"Disabled\"\n } \n ]\n }\n ]\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-entities-table-widget-settings", + "dataKeySettingsDirective": "tb-entities-table-key-settings", "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Asset admin table\",\"enableSelectColumnDisplay\":true},\"title\":\"Asset admin table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"name\":\"Add asset\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"
\\n \\n

Add asset

\\n \\n \\n
\\n \\n \\n
\\n
\\n
\\n \\n Asset name\\n \\n \\n Asset name is required.\\n \\n \\n
\\n \\n \\n Label\\n \\n \\n
\\n
\\n \\n Latitude\\n \\n \\n \\n Longitude\\n \\n \\n
\\n
\\n
\\n
\\n \\n \\n \\n
\\n
\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenAddAssetDialog();\\n\\nfunction openAddAssetDialog() {\\n customDialog.customDialog(htmlTemplate, AddAssetDialogController).subscribe();\\n}\\n\\nfunction AddAssetDialogController(instance) {\\n let vm = instance;\\n \\n vm.addAssetFormGroup = vm.fb.group({\\n assetName: ['', [vm.validators.required]],\\n assetType: ['', [vm.validators.required]],\\n assetLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.addAssetFormGroup.markAsPristine();\\n let asset = {\\n name: vm.addAssetFormGroup.get('assetName').value,\\n type: vm.addAssetFormGroup.get('assetType').value,\\n label: vm.addAssetFormGroup.get('assetLabel').value\\n };\\n assetService.saveAsset(asset).subscribe(\\n function (asset) {\\n saveAttributes(asset.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributes = vm.addAssetFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\"}],\"actionCellButton\":[{\"name\":\"Edit asset\",\"icon\":\"edit\",\"type\":\"customPretty\",\"customHtml\":\"
\\n \\n

Edit asset

\\n \\n \\n
\\n \\n \\n
\\n
\\n
\\n \\n Asset name\\n \\n \\n Asset name is required.\\n \\n \\n
\\n \\n \\n Label\\n \\n \\n
\\n
\\n \\n Latitude\\n \\n \\n \\n Longitude\\n \\n \\n
\\n
\\n
\\n
\\n \\n \\n \\n
\\n
\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenEditAssetDialog();\\n\\nfunction openEditAssetDialog() {\\n customDialog.customDialog(htmlTemplate, EditAssetDialogController).subscribe();\\n}\\n\\nfunction EditAssetDialogController(instance) {\\n let vm = instance;\\n \\n vm.asset = null;\\n vm.attributes = {};\\n \\n vm.editAssetFormGroup = vm.fb.group({\\n assetName: ['', [vm.validators.required]],\\n assetType: ['', [vm.validators.required]],\\n assetLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.editAssetFormGroup.markAsPristine();\\n vm.asset.name = vm.editAssetFormGroup.get('assetName').value,\\n vm.asset.type = vm.editAssetFormGroup.get('assetType').value,\\n vm.asset.label = vm.editAssetFormGroup.get('assetLabel').value\\n assetService.saveAsset(vm.asset).subscribe(\\n function () {\\n saveAttributes().subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n assetService.getAsset(entityId.id).subscribe(\\n function (asset) {\\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE',\\n ['latitude', 'longitude']).subscribe(\\n function (attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n vm.asset = asset;\\n vm.editAssetFormGroup.patchValue(\\n {\\n assetName: vm.asset.name,\\n assetType: vm.asset.type,\\n assetLabel: vm.asset.label,\\n attributes: {\\n latitude: vm.attributes.latitude,\\n longitude: vm.attributes.longitude\\n }\\n }, {emitEvent: false}\\n );\\n } \\n );\\n }\\n ); \\n }\\n \\n function saveAttributes() {\\n let attributes = vm.editAssetFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, 'SERVER_SCOPE', attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\"},{\"name\":\"Delete asset\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\n\\nopenDeleteAssetDialog();\\n\\nfunction openDeleteAssetDialog() {\\n let title = \\\"Are you sure you want to delete the asset \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the asset and all related data will become unrecoverable!\\\";\\n dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(\\n function (result) {\\n if (result) {\\n deleteAsset();\\n }\\n }\\n );\\n}\\n\\nfunction deleteAsset() {\\n assetService.deleteAsset(entityId.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n }\\n );\\n}\\n\",\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\"}]}}" } } From c47e17a21f8e3993d1d9592b1ce66dafbbdffea0 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 27 Apr 2022 19:54:16 +0300 Subject: [PATCH 062/276] UI: Implement gateway widgets settings forms --- .../widget_bundles/gateway_widgets.json | 9 +- ...ngle-device-widget-settings.component.html | 26 ++++++ ...single-device-widget-settings.component.ts | 54 ++++++++++++ ...eway-config-widget-settings.component.html | 45 ++++++++++ ...ateway-config-widget-settings.component.ts | 60 ++++++++++++++ ...eway-events-widget-settings.component.html | 41 ++++++++++ ...ateway-events-widget-settings.component.ts | 82 +++++++++++++++++++ .../lib/settings/widget-settings.module.ts | 24 +++++- .../assets/locale/locale.constant-en_US.json | 14 ++++ 9 files changed, 349 insertions(+), 6 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-single-device-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-single-device-widget-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-widget-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-events-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-events-widget-settings.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/gateway_widgets.json b/application/src/main/data/json/system/widget_bundles/gateway_widgets.json index bec97fe3cf..dc86c0f003 100644 --- a/application/src/main/data/json/system/widget_bundles/gateway_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/gateway_widgets.json @@ -19,8 +19,9 @@ "templateHtml": "\n\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"Gateway Configuration\"\n },\n \"archiveFileName\": {\n \"title\": \"Default archive file name\",\n \"type\": \"string\",\n \"default\": \"gatewayConfiguration\"\n },\n \"gatewayType\": {\n \"title\": \"Device type for new gateway\",\n \"type\": \"string\",\n \"default\": \"Gateway\"\n },\n \"successfulSave\": {\n \"title\": \"Text message about successfully saved gateway configuration\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"gatewayNameExists\": {\n \"title\": \"Text message when device with entered name is already exists\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n [\n \"widgetTitle\",\n \"archiveFileName\",\n \"gatewayType\"\n ],\n [\n \"successfulSave\",\n \"gatewayNameExists\"\n ]\n ],\n \"groupInfoes\": [{\n \"formIndex\": 0,\n \"GroupTitle\": \"General settings\"\n }, {\n \"formIndex\": 1,\n \"GroupTitle\": \"Messages settings\"\n }]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-gateway-config-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"widgetTitle\":\"Gateway Configuration\",\"archiveFileName\":\"configurationGateway\"},\"title\":\"Gateway Configuration\",\"dropShadow\":true,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -37,8 +38,9 @@ "templateHtml": "", "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}", "controllerScript": "let types;\nlet eventsReg = \"eventsReg\";\n\nself.onInit = function() {\n \n self.ctx.datasourceTitleCells = [];\n self.ctx.valueCells = [];\n self.ctx.labelCells = [];\n\n if (self.ctx.datasources.length && self.ctx.datasources[0].type === 'entity') {\n getDatasourceKeys(self.ctx.datasources[0]);\n } else {\n processDatasources(self.ctx.datasources);\n }\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.valueCells.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData && cellData.data && cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length -\n 1];\n var value = tvPair[1];\n var textValue;\n //toDo -> + IsNumber\n \n if (isNumber(value)) {\n var decimals = self.ctx.decimals;\n var units = self.ctx.units;\n if (cellData.dataKey.decimals || cellData.dataKey.decimals === 0) {\n decimals = cellData.dataKey.decimals;\n }\n if (cellData.dataKey.units) {\n units = cellData.dataKey.units;\n }\n txtValue = self.ctx.utils.formatValue(value, decimals, units, false);\n }\n else {\n txtValue = value;\n }\n self.ctx.valueCells[i].html(txtValue);\n }\n }\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n}\n\nself.onResize = function() {\n var datasourceTitleFontSize = self.ctx.height/8;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n datasourceTitleFontSize = self.ctx.width/12;\n }\n datasourceTitleFontSize = Math.min(datasourceTitleFontSize, 20);\n for (var i = 0; i < self.ctx.datasourceTitleCells.length; i++) {\n self.ctx.datasourceTitleCells[i].css('font-size', datasourceTitleFontSize+'px');\n }\n var valueFontSize = self.ctx.height/9;\n var labelFontSize = self.ctx.height/9;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n valueFontSize = self.ctx.width/15;\n labelFontSize = self.ctx.width/15;\n }\n valueFontSize = Math.min(valueFontSize, 18);\n labelFontSize = Math.min(labelFontSize, 18);\n\n for (i = 0; i < self.ctx.valueCells; i++) {\n self.ctx.valueCells[i].css('font-size', valueFontSize+'px');\n self.ctx.valueCells[i].css('height', valueFontSize*2.5+'px');\n self.ctx.valueCells[i].css('padding', '0px ' + valueFontSize + 'px');\n self.ctx.labelCells[i].css('font-size', labelFontSize+'px');\n self.ctx.labelCells[i].css('height', labelFontSize*2.5+'px');\n self.ctx.labelCells[i].css('padding', '0px ' + labelFontSize + 'px');\n } \n}\n\nfunction processDatasources(datasources) {\n var i = 0;\n var tbDatasource = datasources[i];\n var datasourceId = 'tbDatasource' + i;\n self.ctx.$container.append(\n \"
\"\n );\n\n var datasourceContainer = $('#' + datasourceId,\n self.ctx.$container);\n\n datasourceContainer.append(\n \"
\" +\n tbDatasource.name + \"
\"\n );\n \n var datasourceTitleCell = $('.tbDatasource-title', datasourceContainer);\n self.ctx.datasourceTitleCells.push(datasourceTitleCell);\n \n var tableId = 'table' + i;\n datasourceContainer.append(\n \"
\"\n );\n var table = $('#' + tableId, self.ctx.$container);\n\n for (var a = 0; a < tbDatasource.dataKeys.length; a++) {\n var dataKey = tbDatasource.dataKeys[a];\n var labelCellId = 'labelCell' + a;\n var cellId = 'cell' + a;\n table.append(\"\" + dataKey.label +\n \"\");\n var labelCell = $('#' + labelCellId, table);\n self.ctx.labelCells.push(labelCell);\n var valueCell = $('#' + cellId, table);\n self.ctx.valueCells.push(valueCell);\n }\n self.onResize();\n}\n\nfunction getDatasourceKeys (datasource) {\n let entityService = self.ctx.$scope.$injector.get(self.ctx.servicesMap.get('entityService'));\n if (datasource.entityId && datasource.entityType) {\n entityService.getEntityKeys({entityType: datasource.entityType, id: datasource.entityId}, '', 'timeseries').subscribe(\n function(data){\n if (data.length) {\n subscribeForKeys (datasource, data);\n }\n });\n }\n}\n\nfunction subscribeForKeys (datasource, data) {\n let eventsRegVals = self.ctx.settings[eventsReg];\n if (eventsRegVals && eventsRegVals.length > 0) {\n var dataKeys = [];\n data.sort();\n data.forEach(dataValue => {eventsRegVals.forEach(event => {\n if (dataValue.toLowerCase().includes(event.toLowerCase())) {\n var dataKey = {\n type: 'timeseries',\n name: dataValue,\n label: dataValue,\n settings: {},\n _hash: Math.random()\n };\n dataKeys.push(dataKey);\n }\n })});\n\n if (dataKeys.length) {\n updateSubscription (datasource, dataKeys);\n }\n }\n}\n\nfunction updateSubscription (datasource, dataKeys) {\n var datasources = [\n {\n type: 'entity',\n name: datasource.aliasName,\n aliasName: datasource.aliasName,\n entityAliasId: datasource.entityAliasId,\n dataKeys: dataKeys\n }\n ];\n \n var subscriptionOptions = {\n datasources: datasources,\n useDashboardTimewindow: false,\n type: 'latest',\n callbacks: {\n onDataUpdated: (subscription) => {\n self.ctx.data = subscription.data;\n self.onDataUpdated();\n }\n }\n };\n \n processDatasources(datasources);\n self.ctx.subscriptionApi.createSubscription(subscriptionOptions, true).subscribe(\n (subscription) => {\n self.ctx.defaultSubscription = subscription;\n }\n );\n}\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\t\n dataKeysOptional: true,\n singleEntity: true\n };\n}\n\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"GatewayEventsForm\",\n \"properties\": {\n \"eventsTitle\": {\n \"title\": \"Gateway events form title\",\n \"type\": \"string\",\n \"default\": \"Gateway Events Form\"\n },\n \"eventsReg\": {\n \"title\": \"Events filten.\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Event key contains\",\n \"type\": \"string\"\n }\n }\n }\n },\n \"form\": [\n \"eventsTitle\",\n \"eventsReg\"\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-gateway-events-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Function Math.round\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.826503672916844,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"eventsTitle\":\"Gateway Events Form\",\"eventsReg\":[]},\"title\":\"Gateway events\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -55,8 +57,9 @@ "templateHtml": "\n", "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}", "controllerScript": "self.onInit = function() {\n}\n\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\t\t\t\n dataKeysOptional: true,\n singleEntity: true\n };\n}\n\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"GatewayConfigForm\",\n \"properties\": {\n \"gatewayTitle\": {\n \"title\": \"Gateway form\",\n \"type\": \"string\",\n \"default\": \"Gateway configuration (Single device)\"\n },\n \"readOnly\": {\n \"title\": \"Read Only\",\n \"type\": \"boolean\",\n \"default\": false\n }\n }\n },\n \"form\": [\n \"gatewayTitle\",\n \"readOnly\"\n ]\n}\n", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-gateway-config-single-device-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"gatewayTitle\":\"Gateway configuration (Single device)\"},\"title\":\"Gateway configuration (Single device)\"}" } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-single-device-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-single-device-widget-settings.component.html new file mode 100644 index 0000000000..dc1064fbf5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-single-device-widget-settings.component.html @@ -0,0 +1,26 @@ + +
+ + widgets.gateway.gateway-title + + + + {{ 'widgets.gateway.read-only' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-single-device-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-single-device-widget-settings.component.ts new file mode 100644 index 0000000000..e6c413fc2c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-single-device-widget-settings.component.ts @@ -0,0 +1,54 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-gateway-config-single-device-widget-settings', + templateUrl: './gateway-config-single-device-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class GatewayConfigSingleDeviceWidgetSettingsComponent extends WidgetSettingsComponent { + + gatewayConfigSingleDeviceWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.gatewayConfigSingleDeviceWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + gatewayTitle: 'Gateway configuration (Single device)', + readOnly: false + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.gatewayConfigSingleDeviceWidgetSettingsForm = this.fb.group({ + gatewayTitle: [settings.gatewayTitle, []], + readOnly: [settings.readOnly, []] + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-widget-settings.component.html new file mode 100644 index 0000000000..cc46041c01 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-widget-settings.component.html @@ -0,0 +1,45 @@ + +
+
+ widgets.gateway.general-settings + + widgets.gateway.widget-title + + + + widgets.gateway.default-archive-file-name + + + + widgets.gateway.device-type-for-new-gateway + + +
+
+ widgets.gateway.messages-settings + + widgets.gateway.save-config-success-message + + + + widgets.gateway.device-name-exists-message + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-widget-settings.component.ts new file mode 100644 index 0000000000..911b6b2c72 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-config-widget-settings.component.ts @@ -0,0 +1,60 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-gateway-config-widget-settings', + templateUrl: './gateway-config-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class GatewayConfigWidgetSettingsComponent extends WidgetSettingsComponent { + + gatewayConfigWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.gatewayConfigWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + widgetTitle: 'Gateway Configuration', + archiveFileName: 'gatewayConfiguration', + gatewayType: 'Gateway', + successfulSave: '', + gatewayNameExists: '' + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.gatewayConfigWidgetSettingsForm = this.fb.group({ + widgetTitle: [settings.widgetTitle, []], + archiveFileName: [settings.archiveFileName, []], + gatewayType: [settings.gatewayType, []], + successfulSave: [settings.successfulSave, []], + gatewayNameExists: [settings.gatewayNameExists, []] + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-events-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-events-widget-settings.component.html new file mode 100644 index 0000000000..3d3e6e81a4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-events-widget-settings.component.html @@ -0,0 +1,41 @@ + +
+ + widgets.gateway.events-title + + + + widgets.gateway.events-filter + + + {{eventFilter}} + cancel + + + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-events-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-events-widget-settings.component.ts new file mode 100644 index 0000000000..4241c1d7cd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-events-widget-settings.component.ts @@ -0,0 +1,82 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { MatChipInputEvent } from '@angular/material/chips'; +import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; + +@Component({ + selector: 'tb-gateway-events-widget-settings', + templateUrl: './gateway-events-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class GatewayEventsWidgetSettingsComponent extends WidgetSettingsComponent { + + separatorKeysCodes = [ENTER, COMMA, SEMICOLON]; + + gatewayEventsWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.gatewayEventsWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + eventsTitle: 'Gateway events form title', + eventsReg: [] + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.gatewayEventsWidgetSettingsForm = this.fb.group({ + eventsTitle: [settings.eventsTitle, []], + eventsReg: [settings.eventsReg, []] + }); + } + + removeEventFilter(eventFilter: string) { + const eventsFilter: string[] = this.gatewayEventsWidgetSettingsForm.get('eventsReg').value; + const index = eventsFilter.indexOf(eventFilter); + if (index > -1) { + eventsFilter.splice(index, 1); + this.gatewayEventsWidgetSettingsForm.get('eventsReg').setValue(eventsFilter); + this.gatewayEventsWidgetSettingsForm.get('eventsReg').markAsDirty(); + } + } + + addEventFilterFromInput(event: MatChipInputEvent) { + const value = event.value; + if ((value || '').trim()) { + const eventsFilter: string[] = this.gatewayEventsWidgetSettingsForm.get('eventsReg').value; + const index = eventsFilter.indexOf(value); + if (index === -1) { + eventsFilter.push(value); + this.gatewayEventsWidgetSettingsForm.get('eventsReg').setValue(eventsFilter); + this.gatewayEventsWidgetSettingsForm.get('eventsReg').markAsDirty(); + } + event.chipInput.clear(); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index f48749446b..cd27ca9135 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -147,6 +147,15 @@ import { import { EdgeQuickOverviewWidgetSettingsComponent } from '@home/components/widget/lib/settings/cards/edge-quick-overview-widget-settings.component'; +import { + GatewayConfigWidgetSettingsComponent +} from '@home/components/widget/lib/settings/gateway/gateway-config-widget-settings.component'; +import { + GatewayConfigSingleDeviceWidgetSettingsComponent +} from '@home/components/widget/lib/settings/gateway/gateway-config-single-device-widget-settings.component'; +import { + GatewayEventsWidgetSettingsComponent +} from '@home/components/widget/lib/settings/gateway/gateway-events-widget-settings.component'; @NgModule({ declarations: [ @@ -201,7 +210,10 @@ import { RpcTerminalWidgetSettingsComponent, RpcShellWidgetSettingsComponent, DateRangeNavigatorWidgetSettingsComponent, - EdgeQuickOverviewWidgetSettingsComponent + EdgeQuickOverviewWidgetSettingsComponent, + GatewayConfigWidgetSettingsComponent, + GatewayConfigSingleDeviceWidgetSettingsComponent, + GatewayEventsWidgetSettingsComponent ], imports: [ CommonModule, @@ -260,7 +272,10 @@ import { RpcTerminalWidgetSettingsComponent, RpcShellWidgetSettingsComponent, DateRangeNavigatorWidgetSettingsComponent, - EdgeQuickOverviewWidgetSettingsComponent + EdgeQuickOverviewWidgetSettingsComponent, + GatewayConfigWidgetSettingsComponent, + GatewayConfigSingleDeviceWidgetSettingsComponent, + GatewayEventsWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -305,5 +320,8 @@ export const widgetSettingsComponentsMap: {[key: string]: Type Date: Thu, 28 Apr 2022 16:38:58 +0300 Subject: [PATCH 063/276] UI: Add GPIO widgets settings forms. --- .../system/widget_bundles/gpio_widgets.json | 12 +- .../data-key-config-dialog.component.html | 1 + .../data-key-config-dialog.component.ts | 2 + .../widget/data-key-config.component.html | 1 + .../widget/data-key-config.component.ts | 4 + .../components/widget/data-keys.component.ts | 1 + .../label-widget-settings.component.html | 2 +- .../cards/label-widget-settings.component.ts | 25 +-- ...logue-gauge-widget-settings.component.html | 2 +- ...nalogue-gauge-widget-settings.component.ts | 29 ++-- ...digital-gauge-widget-settings.component.ts | 65 +++++--- ...pio-control-widget-settings.component.html | 96 +++++++++++ .../gpio-control-widget-settings.component.ts | 145 +++++++++++++++++ .../settings/gpio/gpio-item.component.html | 73 +++++++++ .../settings/gpio/gpio-item.component.scss | 40 +++++ .../lib/settings/gpio/gpio-item.component.ts | 150 ++++++++++++++++++ .../gpio-panel-widget-settings.component.html | 55 +++++++ .../gpio-panel-widget-settings.component.ts | 114 +++++++++++++ .../lib/settings/widget-settings.module.ts | 21 ++- .../components/json-content.component.html | 2 +- .../components/json-content.component.ts | 9 ++ ui-ngx/src/app/shared/models/widget.models.ts | 7 +- .../assets/locale/locale.constant-en_US.json | 20 +++ 23 files changed, 820 insertions(+), 56 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-control-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-control-widget-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-item.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-item.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-item.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-panel-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-panel-widget-settings.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/gpio_widgets.json b/application/src/main/data/json/system/widget_bundles/gpio_widgets.json index 41583a4a8f..e539824aca 100644 --- a/application/src/main/data/json/system/widget_bundles/gpio_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/gpio_widgets.json @@ -19,8 +19,9 @@ "templateHtml": "
\n
\n
\n
\n {{ cell.label }}\n
\n {{cell.pin}}\n \n \n \n \n {{cell.pin}}\n
\n {{ cell.label }}\n
\n
\n \n \n \n
\n
\n
\n {{rpcErrorText}}\n \n
", "templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.gpio-panel section[fxflex] {\n min-width: 0px;\n}\n\n\n.switch-panel {\n margin: 0;\n height: 32px;\n width: 66px;\n min-width: 66px;\n}\n\n.switch-panel mat-slide-toggle {\n margin: 0;\n width: 36px;\n min-width: 36px;\n}\n\n.switch-panel.col-0 mat-slide-toggle {\n margin-left: 8px;\n margin-right: 4px;\n}\n\n.switch-panel.col-1 mat-slide-toggle {\n margin-left: 4px;\n margin-right: 8px;\n}\n\n.gpio-row {\n height: 32px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.switch-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.switch-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}", "controllerScript": "var namespace;\nvar cssParser = new cssjs();\n\nself.onInit = function() {\n var utils = self.ctx.$injector.get(self.ctx.servicesMap.get('utils'));\n namespace = 'gpio-control-' + utils.guid();\n cssParser.testMode = false;\n cssParser.cssPreviewNamespace = namespace;\n self.ctx.$container.addClass(namespace);\n self.ctx.ngZone.run(function() {\n init(); \n });\n}\n\nfunction init() {\n \n var i, gpio;\n var scope = self.ctx.$scope;\n var settings = self.ctx.settings;\n scope.gpioList = [];\n for (var g = 0; g < settings.gpioList.length; g++) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false\n }\n );\n }\n\n scope.requestTimeout = settings.requestTimeout || 1000;\n\n scope.switchPanelBackgroundColor = settings.switchPanelBackgroundColor || tinycolor('green').lighten(2).toRgbString();\n\n scope.gpioStatusRequest = {\n method: \"getGpioStatus\",\n paramsBody: \"{}\"\n };\n \n if (settings.gpioStatusRequest) {\n scope.gpioStatusRequest.method = settings.gpioStatusRequest.method || scope.gpioStatusRequest.method;\n scope.gpioStatusRequest.paramsBody = settings.gpioStatusRequest.paramsBody || scope.gpioStatusRequest.paramsBody;\n }\n \n scope.gpioStatusChangeRequest = {\n method: \"setGpioStatus\",\n paramsBody: \"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"\n };\n \n if (settings.gpioStatusChangeRequest) {\n scope.gpioStatusChangeRequest.method = settings.gpioStatusChangeRequest.method || scope.gpioStatusChangeRequest.method;\n scope.gpioStatusChangeRequest.paramsBody = settings.gpioStatusChangeRequest.paramsBody || scope.gpioStatusChangeRequest.paramsBody;\n }\n \n scope.parseGpioStatusFunction = \"return body[pin] === true;\";\n \n if (settings.parseGpioStatusFunction && settings.parseGpioStatusFunction.length > 0) {\n scope.parseGpioStatusFunction = settings.parseGpioStatusFunction;\n }\n \n scope.parseGpioStatusFunction = new Function(\"body, pin\", scope.parseGpioStatusFunction);\n \n function requestGpioStatus() {\n self.ctx.controlApi.sendTwoWayCommand(scope.gpioStatusRequest.method, \n scope.gpioStatusRequest.paramsBody, \n scope.requestTimeout)\n .subscribe(\n function success(responseBody) {\n for (var g = 0; g < scope.gpioList.length; g++) {\n var gpio = scope.gpioList[g];\n var enabled = scope.parseGpioStatusFunction.apply(this, [responseBody, gpio.pin]);\n gpio.enabled = enabled; \n self.ctx.detectChanges();\n }\n }\n );\n }\n \n function changeGpioStatus(gpio) {\n var pin = gpio.pin + '';\n var enabled = !gpio.enabled;\n enabled = enabled === true ? 'true' : 'false';\n var paramsBody = scope.gpioStatusChangeRequest.paramsBody;\n var requestBody = JSON.parse(paramsBody.replace(\"\\\"{$pin}\\\"\", pin).replace(\"\\\"{$enabled}\\\"\", enabled));\n self.ctx.controlApi.sendTwoWayCommand(scope.gpioStatusChangeRequest.method, \n requestBody, scope.requestTimeout)\n .subscribe(\n function success(responseBody) {\n var enabled = scope.parseGpioStatusFunction.apply(this, [responseBody, gpio.pin]);\n gpio.enabled = enabled;\n self.ctx.detectChanges();\n }\n );\n }\n \n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+'_'+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+'_'+c]) {\n row[c] = scope.gpioCells[i+'_'+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n }\n\n scope.gpioClick = function($event, gpio) {\n if (scope.rpcEnabled && !scope.executingRpcRequest) {\n changeGpioStatus(gpio);\n }\n };\n \n scope.gpioToggleChange = function($event, gpio) {\n gpio.enabled = !$event.checked;\n $event.source.toggle();\n self.ctx.detectChanges();\n }\n \n if (scope.rpcEnabled) {\n requestGpioStatus(); \n }\n \n self.onResize();\n}\n\nself.onResize = function() {\n var scope = self.ctx.$scope;\n var rowCount = scope.rows.length;\n var prefferedRowHeight = (self.ctx.height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n scope.prefferedRowHeight = prefferedRowHeight;\n var ratio = prefferedRowHeight/32;\n \n var css = '.mat-slide-toggle .mat-slide-toggle-bar {\\n' +\n ' height: ' + 14*ratio+'px;\\n'+\n ' width: ' + 36*ratio+'px;\\n'+\n '}\\n';\n css += '.mat-slide-toggle .mat-slide-toggle-thumb-container {\\n' +\n ' height: ' + 20*ratio+'px;\\n'+\n ' width: ' + 20*ratio+'px;\\n'+\n '}\\n';\n css += '.mat-slide-toggle .mat-slide-toggle-thumb {\\n' +\n ' height: ' + 20*ratio+'px;\\n'+\n ' width: ' + 20*ratio+'px;\\n'+\n '}\\n';\n css += '.mat-slide-toggle .mat-slide-toggle-ripple {\\n' +\n ' height: ' + 40*ratio+'px;\\n'+\n ' width: ' + 40*ratio+'px;\\n'+\n ' top: calc(50% - '+20*ratio+'px);\\n'+\n ' left: calc(50% - '+20*ratio+'px);\\n'+\n '}\\n';\n css += '.gpio-left-label, .gpio-right-label {\\n' +\n ' font-size: ' + 16*ratio+'px;\\n'+\n '}\\n';\n var pinsFontSize = Math.max(9, 12*ratio);\n css += '.pin {\\n' +\n ' font-size: ' + pinsFontSize+'px;\\n'+\n '}\\n';\n\n cssParser.createStyleElement(namespace, css);\n \n self.ctx.detectChanges();\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"gpioList\": {\n \"title\": \"Gpio switches\",\n \"type\": \"array\",\n \"minItems\" : 1,\n \"items\": {\n \"title\": \"Gpio switch\",\n \"type\": \"object\",\n \"properties\": {\n \"pin\": {\n \"title\": \"Pin\",\n \"type\": \"number\"\n },\n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"row\": {\n \"title\": \"Row\",\n \"type\": \"number\"\n },\n \"col\": {\n \"title\": \"Column\",\n \"type\": \"number\"\n }\n },\n \"required\": [\"pin\", \"label\", \"row\", \"col\"]\n }\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"switchPanelBackgroundColor\": {\n \"title\": \"Switches panel background color\",\n \"type\": \"string\",\n \"default\": \"#008a00\"\n },\n \"gpioStatusRequest\": {\n \"title\": \"GPIO status request\",\n \"type\": \"object\",\n \"properties\": {\n \"method\": {\n \"title\": \"Method name\",\n \"type\": \"string\",\n \"default\": \"getGpioStatus\"\n },\n \"paramsBody\": {\n \"title\": \"Method body\",\n \"type\": \"string\",\n \"default\": \"{}\"\n }\n },\n \"required\": [\"method\", \"paramsBody\"]\n },\n \"gpioStatusChangeRequest\": {\n \"title\": \"GPIO status change request\",\n \"type\": \"object\",\n \"properties\": {\n \"method\": {\n \"title\": \"Method name\",\n \"type\": \"string\",\n \"default\": \"setGpioStatus\"\n },\n \"paramsBody\": {\n \"title\": \"Method body\",\n \"type\": \"string\",\n \"default\": \"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"\n }\n },\n \"required\": [\"method\", \"paramsBody\"]\n },\n \"parseGpioStatusFunction\": {\n \"title\": \"Parse gpio status function\",\n \"type\": \"string\",\n \"default\": \"return body[pin] === true;\"\n } \n },\n \"required\": [\"gpioList\", \n \"requestTimeout\",\n \"switchPanelBackgroundColor\",\n \"gpioStatusRequest\",\n \"gpioStatusChangeRequest\",\n \"parseGpioStatusFunction\"]\n },\n \"form\": [\n \"gpioList\",\n \"requestTimeout\",\n {\n \"key\": \"switchPanelBackgroundColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"gpioStatusRequest\",\n \"items\": [\n \"gpioStatusRequest.method\",\n {\n \"key\": \"gpioStatusRequest.paramsBody\",\n \"type\": \"json\"\n }\n ]\n },\n {\n \"key\": \"gpioStatusChangeRequest\",\n \"items\": [\n \"gpioStatusChangeRequest.method\",\n {\n \"key\": \"gpioStatusChangeRequest.paramsBody\",\n \"type\": \"json\"\n }\n ]\n },\n {\n \"key\": \"parseGpioStatusFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/rpc/parse_gpio_status_fn\"\n }\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-gpio-control-widget-settings", "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"parseGpioStatusFunction\":\"return body[pin] === true;\",\"gpioStatusChangeRequest\":{\"method\":\"setGpioStatus\",\"paramsBody\":\"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"},\"requestTimeout\":500,\"switchPanelBackgroundColor\":\"#b71c1c\",\"gpioStatusRequest\":{\"method\":\"getGpioStatus\",\"paramsBody\":\"{}\"},\"gpioList\":[{\"pin\":1,\"label\":\"GPIO 1\",\"row\":0,\"col\":0,\"_uniqueKey\":0},{\"pin\":2,\"label\":\"GPIO 2\",\"row\":0,\"col\":1,\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 3\",\"row\":1,\"col\":0,\"_uniqueKey\":2}]},\"title\":\"Basic GPIO Control\"}" } }, @@ -37,8 +38,9 @@ "templateHtml": "
\n
\n
\n
\n {{ cell.label }}\n
\n {{cell.pin}}\n \n \n \n \n {{cell.pin}}\n
\n {{ cell.label }}\n
\n
\n \n \n \n
\n
\n
\n
", "templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.gpio-panel section[fxflex] {\n min-width: 0px;\n}\n\n\n.gpio-panel tb-led-light > div {\n margin: auto;\n}\n\n.led-panel {\n margin: 0;\n width: 66px;\n min-width: 66px;\n}\n\n.led-container {\n width: 48px;\n min-width: 48px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.led-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.led-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}", "controllerScript": "var namespace;\nvar cssParser = new cssjs();\n\nself.onInit = function() {\n var utils = self.ctx.$injector.get(self.ctx.servicesMap.get('utils'));\n namespace = 'gpio-panel-' + utils.guid();\n cssParser.testMode = false;\n cssParser.cssPreviewNamespace = namespace;\n self.ctx.$container.addClass(namespace);\n self.ctx.ngZone.run(function() {\n init(); \n });\n}\n\nfunction init() {\n var i, gpio;\n \n var scope = self.ctx.$scope;\n var settings = self.ctx.settings;\n \n scope.gpioList = [];\n scope.gpioByPin = {};\n for (var g = 0; g < settings.gpioList.length; g++) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false,\n colorOn: tinycolor(gpio.color).lighten(20).toHexString(),\n colorOff: tinycolor(gpio.color).darken().toHexString()\n }\n );\n scope.gpioByPin[gpio.pin] = scope.gpioList[scope.gpioList.length-1];\n }\n\n scope.ledPanelBackgroundColor = settings.ledPanelBackgroundColor || tinycolor('green').lighten(2).toRgbString();\n\n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+'_'+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+'_'+c]) {\n row[c] = scope.gpioCells[i+'_'+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n } \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n var changed = false;\n for (var d = 0; d < self.ctx.data.length; d++) {\n var cellData = self.ctx.data[d];\n var dataKey = cellData.dataKey;\n var gpio = self.ctx.$scope.gpioByPin[dataKey.label];\n if (gpio) {\n var enabled = false;\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n enabled = (tvPair[1] === true || tvPair[1] === 'true');\n }\n if (gpio.enabled != enabled) {\n changed = true;\n gpio.enabled = enabled;\n }\n }\n }\n if (changed) {\n self.ctx.detectChanges();\n } \n}\n\nself.onResize = function() {\n var rowCount = self.ctx.$scope.rows.length;\n var prefferedRowHeight = (self.ctx.height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n self.ctx.$scope.prefferedRowHeight = prefferedRowHeight;\n \n var ratio = prefferedRowHeight/32;\n \n var css = '.gpio-left-label, .gpio-right-label {\\n' +\n ' font-size: ' + 16*ratio+'px;\\n'+\n '}\\n';\n var pinsFontSize = Math.max(9, 12*ratio);\n css += '.pin {\\n' +\n ' font-size: ' + pinsFontSize+'px;\\n'+\n '}\\n';\n \n cssParser.createStyleElement(namespace, css); \n \n self.ctx.detectChanges();\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"gpioList\": {\n \"title\": \"Gpio leds\",\n \"type\": \"array\",\n \"minItems\" : 1,\n \"items\": {\n \"title\": \"Gpio led\",\n \"type\": \"object\",\n \"properties\": {\n \"pin\": {\n \"title\": \"Pin\",\n \"type\": \"number\"\n },\n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"row\": {\n \"title\": \"Row\",\n \"type\": \"number\"\n },\n \"col\": {\n \"title\": \"Column\",\n \"type\": \"number\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\",\n \"default\": \"red\"\n }\n },\n \"required\": [\"pin\", \"label\", \"row\", \"col\", \"color\"]\n }\n },\n \"ledPanelBackgroundColor\": {\n \"title\": \"LED panel background color\",\n \"type\": \"string\",\n \"default\": \"#008a00\"\n } \n },\n \"required\": [\"gpioList\", \n \"ledPanelBackgroundColor\"]\n },\n \"form\": [\n {\n \"key\": \"gpioList\",\n \"items\": [\n \"gpioList[].pin\",\n \"gpioList[].label\",\n \"gpioList[].row\",\n \"gpioList[].col\",\n {\n \"key\": \"gpioList[].color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"ledPanelBackgroundColor\",\n \"type\": \"color\"\n }\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-gpio-panel-widget-settings", "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"gpioList\":[{\"pin\":1,\"label\":\"GPIO 1\",\"row\":0,\"col\":0,\"color\":\"#008000\",\"_uniqueKey\":0},{\"pin\":2,\"label\":\"GPIO 2\",\"row\":0,\"col\":1,\"color\":\"#ffff00\",\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 3\",\"row\":1,\"col\":0,\"color\":\"#cf006f\",\"_uniqueKey\":2}],\"ledPanelBackgroundColor\":\"#b71c1c\"},\"title\":\"Basic GPIO Panel\",\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"1\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.22518255793320163,\"funcBody\":\"var period = time % 1500;\\nreturn period < 500;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"2\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.7008206860666621,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 500 && period < 1000;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"3\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.42600325102193426,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 1000;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}}}" } }, @@ -55,8 +57,9 @@ "templateHtml": "
\n
\n
\n
\n {{ cell.label }}\n
\n {{cell.pin}}\n \n \n \n \n {{cell.pin}}\n
\n {{ cell.label }}\n
\n
\n \n \n \n
\n
\n
\n
", "templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.gpio-panel section[fxflex] {\n min-width: 0px;\n}\n\n.gpio-panel tb-led-light > div {\n margin: auto;\n}\n\n.led-panel {\n margin: 0;\n width: 66px;\n min-width: 66px;\n}\n\n.led-container {\n width: 48px;\n min-width: 48px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.led-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.led-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}", "controllerScript": "var namespace;\nvar cssParser = new cssjs();\n\nself.onInit = function() {\n var utils = self.ctx.$injector.get(self.ctx.servicesMap.get('utils'));\n namespace = 'gpio-panel-' + utils.guid();\n cssParser.testMode = false;\n cssParser.cssPreviewNamespace = namespace;\n self.ctx.$container.addClass(namespace);\n self.ctx.ngZone.run(function() {\n init(); \n });\n}\n\nfunction init() {\n var i, gpio;\n \n var scope = self.ctx.$scope;\n var settings = self.ctx.settings;\n \n scope.gpioList = [];\n scope.gpioByPin = {};\n for (var g = 0; g < settings.gpioList.length; g++) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false,\n colorOn: tinycolor(gpio.color).lighten(20).toHexString(),\n colorOff: tinycolor(gpio.color).darken().toHexString()\n }\n );\n scope.gpioByPin[gpio.pin] = scope.gpioList[scope.gpioList.length-1];\n }\n\n scope.ledPanelBackgroundColor = settings.ledPanelBackgroundColor || tinycolor('green').lighten(2).toRgbString();\n\n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+'_'+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+'_'+c]) {\n row[c] = scope.gpioCells[i+'_'+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n } \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n var changed = false;\n for (var d = 0; d < self.ctx.data.length; d++) {\n var cellData = self.ctx.data[d];\n var dataKey = cellData.dataKey;\n var gpio = self.ctx.$scope.gpioByPin[dataKey.label];\n if (gpio) {\n var enabled = false;\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n enabled = (tvPair[1] === true || tvPair[1] === 'true');\n }\n if (gpio.enabled != enabled) {\n changed = true;\n gpio.enabled = enabled;\n }\n }\n }\n if (changed) {\n self.ctx.detectChanges();\n } \n}\n\nself.onResize = function() {\n var rowCount = self.ctx.$scope.rows.length;\n var prefferedRowHeight = (self.ctx.height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n self.ctx.$scope.prefferedRowHeight = prefferedRowHeight;\n \n var ratio = prefferedRowHeight/32;\n \n var css = '.gpio-left-label, .gpio-right-label {\\n' +\n ' font-size: ' + 16*ratio+'px;\\n'+\n '}\\n';\n var pinsFontSize = Math.max(9, 12*ratio);\n css += '.pin {\\n' +\n ' font-size: ' + pinsFontSize+'px;\\n'+\n '}\\n';\n \n cssParser.createStyleElement(namespace, css); \n \n self.ctx.detectChanges();\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"gpioList\": {\n \"title\": \"Gpio leds\",\n \"type\": \"array\",\n \"minItems\" : 1,\n \"items\": {\n \"title\": \"Gpio led\",\n \"type\": \"object\",\n \"properties\": {\n \"pin\": {\n \"title\": \"Pin\",\n \"type\": \"number\"\n },\n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"row\": {\n \"title\": \"Row\",\n \"type\": \"number\"\n },\n \"col\": {\n \"title\": \"Column\",\n \"type\": \"number\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\",\n \"default\": \"red\"\n }\n },\n \"required\": [\"pin\", \"label\", \"row\", \"col\", \"color\"]\n }\n },\n \"ledPanelBackgroundColor\": {\n \"title\": \"LED panel background color\",\n \"type\": \"string\",\n \"default\": \"#008a00\"\n } \n },\n \"required\": [\"gpioList\", \n \"ledPanelBackgroundColor\"]\n },\n \"form\": [\n {\n \"key\": \"gpioList\",\n \"items\": [\n \"gpioList[].pin\",\n \"gpioList[].label\",\n \"gpioList[].row\",\n \"gpioList[].col\",\n {\n \"key\": \"gpioList[].color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"ledPanelBackgroundColor\",\n \"type\": \"color\"\n }\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-gpio-panel-widget-settings", "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"gpioList\":[{\"pin\":1,\"label\":\"3.3V\",\"row\":0,\"col\":0,\"color\":\"#fc9700\",\"_uniqueKey\":0},{\"pin\":2,\"label\":\"5V\",\"row\":0,\"col\":1,\"color\":\"#fb0000\",\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 2 (I2C1_SDA)\",\"row\":1,\"col\":0,\"color\":\"#02fefb\",\"_uniqueKey\":2},{\"color\":\"#fb0000\",\"pin\":4,\"label\":\"5V\",\"row\":1,\"col\":1},{\"color\":\"#02fefb\",\"pin\":5,\"label\":\"GPIO 3 (I2C1_SCL)\",\"row\":2,\"col\":0},{\"color\":\"#000000\",\"pin\":6,\"label\":\"GND\",\"row\":2,\"col\":1},{\"color\":\"#00fd00\",\"pin\":7,\"label\":\"GPIO 4 (GPCLK0)\",\"row\":3,\"col\":0},{\"color\":\"#fdfb00\",\"pin\":8,\"label\":\"GPIO 14 (UART_TXD)\",\"row\":3,\"col\":1},{\"color\":\"#000000\",\"pin\":9,\"label\":\"GND\",\"row\":4,\"col\":0},{\"color\":\"#fdfb00\",\"pin\":10,\"label\":\"GPIO 15 (UART_RXD)\",\"row\":4,\"col\":1},{\"color\":\"#00fd00\",\"pin\":11,\"label\":\"GPIO 17\",\"row\":5,\"col\":0},{\"color\":\"#00fd00\",\"pin\":12,\"label\":\"GPIO 18\",\"row\":5,\"col\":1},{\"color\":\"#00fd00\",\"pin\":13,\"label\":\"GPIO 27\",\"row\":6,\"col\":0},{\"color\":\"#000000\",\"pin\":14,\"label\":\"GND\",\"row\":6,\"col\":1},{\"color\":\"#00fd00\",\"pin\":15,\"label\":\"GPIO 22\",\"row\":7,\"col\":0},{\"color\":\"#00fd00\",\"pin\":16,\"label\":\"GPIO 23\",\"row\":7,\"col\":1},{\"color\":\"#fc9700\",\"pin\":17,\"label\":\"3.3V\",\"row\":8,\"col\":0},{\"color\":\"#00fd00\",\"pin\":18,\"label\":\"GPIO 24\",\"row\":8,\"col\":1},{\"color\":\"#fd01fd\",\"pin\":19,\"label\":\"GPIO 10 (SPI_MOSI)\",\"row\":9,\"col\":0},{\"color\":\"#000000\",\"pin\":20,\"label\":\"GND\",\"row\":9,\"col\":1},{\"color\":\"#fd01fd\",\"pin\":21,\"label\":\"GPIO 9 (SPI_MISO)\",\"row\":10,\"col\":0},{\"color\":\"#00fd00\",\"pin\":22,\"label\":\"GPIO 25\",\"row\":10,\"col\":1},{\"color\":\"#fd01fd\",\"pin\":23,\"label\":\"GPIO 11 (SPI_SCLK)\",\"row\":11,\"col\":0},{\"color\":\"#fd01fd\",\"pin\":24,\"label\":\"GPIO 8 (SPI_CE0)\",\"row\":11,\"col\":1},{\"color\":\"#000000\",\"pin\":25,\"label\":\"GND\",\"row\":12,\"col\":0},{\"color\":\"#fd01fd\",\"pin\":26,\"label\":\"GPIO 7 (SPI_CE1)\",\"row\":12,\"col\":1},{\"color\":\"#ffffff\",\"pin\":27,\"label\":\"ID_SD\",\"row\":13,\"col\":0},{\"color\":\"#ffffff\",\"pin\":28,\"label\":\"ID_SC\",\"row\":13,\"col\":1},{\"color\":\"#00fd00\",\"pin\":29,\"label\":\"GPIO 5\",\"row\":14,\"col\":0},{\"color\":\"#000000\",\"pin\":30,\"label\":\"GND\",\"row\":14,\"col\":1},{\"color\":\"#00fd00\",\"pin\":31,\"label\":\"GPIO 6\",\"row\":15,\"col\":0},{\"color\":\"#00fd00\",\"pin\":32,\"label\":\"GPIO 12\",\"row\":15,\"col\":1},{\"color\":\"#00fd00\",\"pin\":33,\"label\":\"GPIO 13\",\"row\":16,\"col\":0},{\"color\":\"#000000\",\"pin\":34,\"label\":\"GND\",\"row\":16,\"col\":1},{\"color\":\"#00fd00\",\"pin\":35,\"label\":\"GPIO 19\",\"row\":17,\"col\":0},{\"color\":\"#00fd00\",\"pin\":36,\"label\":\"GPIO 16\",\"row\":17,\"col\":1},{\"color\":\"#00fd00\",\"pin\":37,\"label\":\"GPIO 26\",\"row\":18,\"col\":0},{\"color\":\"#00fd00\",\"pin\":38,\"label\":\"GPIO 20\",\"row\":18,\"col\":1},{\"color\":\"#000000\",\"pin\":39,\"label\":\"GND\",\"row\":19,\"col\":0},{\"color\":\"#00fd00\",\"pin\":40,\"label\":\"GPIO 21\",\"row\":19,\"col\":1}],\"ledPanelBackgroundColor\":\"#008a00\"},\"title\":\"Raspberry Pi GPIO Panel\",\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"7\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.22518255793320163,\"funcBody\":\"var period = time % 1500;\\nreturn period < 500;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"11\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.7008206860666621,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 500 && period < 1000;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"12\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.42600325102193426,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 1000;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"13\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.48362241571415243,\"funcBody\":\"var period = time % 1500;\\nreturn period < 500;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"29\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.7217670147518815,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 500 && period < 1000;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}}}" } }, @@ -73,8 +76,9 @@ "templateHtml": "
\n
\n
\n
\n {{ cell.label }}\n
\n {{cell.pin}}\n \n \n \n \n {{cell.pin}}\n
\n {{ cell.label }}\n
\n
\n \n \n \n
\n
\n
\n {{rpcErrorText}}\n \n
", "templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.gpio-panel section[fxflex] {\n min-width: 0px;\n}\n\n.switch-panel {\n margin: 0;\n height: 32px;\n width: 66px;\n min-width: 66px;\n}\n\n.switch-panel mat-slide-toggle {\n margin: 0;\n width: 36px;\n min-width: 36px;\n}\n\n.switch-panel.col-0 mat-slide-toggle {\n margin-left: 8px;\n margin-right: 4px;\n}\n\n.switch-panel.col-1 mat-slide-toggle {\n margin-left: 4px;\n margin-right: 8px;\n}\n\n.gpio-row {\n height: 32px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.switch-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.switch-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}", "controllerScript": "var namespace;\nvar cssParser = new cssjs();\n\nself.onInit = function() {\n var utils = self.ctx.$injector.get(self.ctx.servicesMap.get('utils'));\n namespace = 'gpio-control-' + utils.guid();\n cssParser.testMode = false;\n cssParser.cssPreviewNamespace = namespace;\n self.ctx.$container.addClass(namespace);\n self.ctx.ngZone.run(function() {\n init(); \n });\n}\n\nfunction init() {\n \n var i, gpio;\n var scope = self.ctx.$scope;\n var settings = self.ctx.settings;\n scope.gpioList = [];\n for (var g = 0; g < settings.gpioList.length; g++) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false\n }\n );\n }\n\n scope.requestTimeout = settings.requestTimeout || 1000;\n\n scope.switchPanelBackgroundColor = settings.switchPanelBackgroundColor || tinycolor('green').lighten(2).toRgbString();\n\n scope.gpioStatusRequest = {\n method: \"getGpioStatus\",\n paramsBody: \"{}\"\n };\n \n if (settings.gpioStatusRequest) {\n scope.gpioStatusRequest.method = settings.gpioStatusRequest.method || scope.gpioStatusRequest.method;\n scope.gpioStatusRequest.paramsBody = settings.gpioStatusRequest.paramsBody || scope.gpioStatusRequest.paramsBody;\n }\n \n scope.gpioStatusChangeRequest = {\n method: \"setGpioStatus\",\n paramsBody: \"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"\n };\n \n if (settings.gpioStatusChangeRequest) {\n scope.gpioStatusChangeRequest.method = settings.gpioStatusChangeRequest.method || scope.gpioStatusChangeRequest.method;\n scope.gpioStatusChangeRequest.paramsBody = settings.gpioStatusChangeRequest.paramsBody || scope.gpioStatusChangeRequest.paramsBody;\n }\n \n scope.parseGpioStatusFunction = \"return body[pin] === true;\";\n \n if (settings.parseGpioStatusFunction && settings.parseGpioStatusFunction.length > 0) {\n scope.parseGpioStatusFunction = settings.parseGpioStatusFunction;\n }\n \n scope.parseGpioStatusFunction = new Function(\"body, pin\", scope.parseGpioStatusFunction);\n \n function requestGpioStatus() {\n self.ctx.controlApi.sendTwoWayCommand(scope.gpioStatusRequest.method, \n scope.gpioStatusRequest.paramsBody, \n scope.requestTimeout)\n .subscribe(\n function success(responseBody) {\n for (var g = 0; g < scope.gpioList.length; g++) {\n var gpio = scope.gpioList[g];\n var enabled = scope.parseGpioStatusFunction.apply(this, [responseBody, gpio.pin]);\n gpio.enabled = enabled; \n self.ctx.detectChanges();\n }\n }\n );\n }\n \n function changeGpioStatus(gpio) {\n var pin = gpio.pin + '';\n var enabled = !gpio.enabled;\n enabled = enabled === true ? 'true' : 'false';\n var paramsBody = scope.gpioStatusChangeRequest.paramsBody;\n var requestBody = JSON.parse(paramsBody.replace(\"\\\"{$pin}\\\"\", pin).replace(\"\\\"{$enabled}\\\"\", enabled));\n self.ctx.controlApi.sendTwoWayCommand(scope.gpioStatusChangeRequest.method, \n requestBody, scope.requestTimeout)\n .subscribe(\n function success(responseBody) {\n var enabled = scope.parseGpioStatusFunction.apply(this, [responseBody, gpio.pin]);\n gpio.enabled = enabled;\n self.ctx.detectChanges();\n }\n );\n }\n \n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+'_'+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+'_'+c]) {\n row[c] = scope.gpioCells[i+'_'+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n }\n\n scope.gpioClick = function($event, gpio) {\n if (scope.rpcEnabled && !scope.executingRpcRequest) {\n changeGpioStatus(gpio);\n }\n };\n \n scope.gpioToggleChange = function($event, gpio) {\n gpio.enabled = !$event.checked;\n $event.source.toggle();\n self.ctx.detectChanges();\n }\n \n if (scope.rpcEnabled) {\n requestGpioStatus(); \n }\n \n self.onResize();\n}\n\nself.onResize = function() {\n var scope = self.ctx.$scope;\n var rowCount = scope.rows.length;\n var prefferedRowHeight = (self.ctx.height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n scope.prefferedRowHeight = prefferedRowHeight;\n var ratio = prefferedRowHeight/32;\n \n var css = '.mat-slide-toggle .mat-slide-toggle-bar {\\n' +\n ' height: ' + 14*ratio+'px;\\n'+\n ' width: ' + 36*ratio+'px;\\n'+\n '}\\n';\n css += '.mat-slide-toggle .mat-slide-toggle-thumb-container {\\n' +\n ' height: ' + 20*ratio+'px;\\n'+\n ' width: ' + 20*ratio+'px;\\n'+\n '}\\n';\n css += '.mat-slide-toggle .mat-slide-toggle-thumb {\\n' +\n ' height: ' + 20*ratio+'px;\\n'+\n ' width: ' + 20*ratio+'px;\\n'+\n '}\\n';\n css += '.mat-slide-toggle .mat-slide-toggle-ripple {\\n' +\n ' height: ' + 40*ratio+'px;\\n'+\n ' width: ' + 40*ratio+'px;\\n'+\n ' top: calc(50% - '+20*ratio+'px);\\n'+\n ' left: calc(50% - '+20*ratio+'px);\\n'+\n '}\\n';\n css += '.gpio-left-label, .gpio-right-label {\\n' +\n ' font-size: ' + 16*ratio+'px;\\n'+\n '}\\n';\n var pinsFontSize = Math.max(9, 12*ratio);\n css += '.pin {\\n' +\n ' font-size: ' + pinsFontSize+'px;\\n'+\n '}\\n';\n\n cssParser.createStyleElement(namespace, css);\n \n self.ctx.detectChanges();\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"gpioList\": {\n \"title\": \"Gpio switches\",\n \"type\": \"array\",\n \"minItems\" : 1,\n \"items\": {\n \"title\": \"Gpio switch\",\n \"type\": \"object\",\n \"properties\": {\n \"pin\": {\n \"title\": \"Pin\",\n \"type\": \"number\"\n },\n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"row\": {\n \"title\": \"Row\",\n \"type\": \"number\"\n },\n \"col\": {\n \"title\": \"Column\",\n \"type\": \"number\"\n }\n },\n \"required\": [\"pin\", \"label\", \"row\", \"col\"]\n }\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"switchPanelBackgroundColor\": {\n \"title\": \"Switches panel background color\",\n \"type\": \"string\",\n \"default\": \"#008a00\"\n },\n \"gpioStatusRequest\": {\n \"title\": \"GPIO status request\",\n \"type\": \"object\",\n \"properties\": {\n \"method\": {\n \"title\": \"Method name\",\n \"type\": \"string\",\n \"default\": \"getGpioStatus\"\n },\n \"paramsBody\": {\n \"title\": \"Method body\",\n \"type\": \"string\",\n \"default\": \"{}\"\n }\n },\n \"required\": [\"method\", \"paramsBody\"]\n },\n \"gpioStatusChangeRequest\": {\n \"title\": \"GPIO status change request\",\n \"type\": \"object\",\n \"properties\": {\n \"method\": {\n \"title\": \"Method name\",\n \"type\": \"string\",\n \"default\": \"setGpioStatus\"\n },\n \"paramsBody\": {\n \"title\": \"Method body\",\n \"type\": \"string\",\n \"default\": \"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"\n }\n },\n \"required\": [\"method\", \"paramsBody\"]\n },\n \"parseGpioStatusFunction\": {\n \"title\": \"Parse gpio status function\",\n \"type\": \"string\",\n \"default\": \"return body[pin] === true;\"\n } \n },\n \"required\": [\"gpioList\", \n \"requestTimeout\",\n \"switchPanelBackgroundColor\",\n \"gpioStatusRequest\",\n \"gpioStatusChangeRequest\",\n \"parseGpioStatusFunction\"]\n },\n \"form\": [\n \"gpioList\",\n \"requestTimeout\",\n {\n \"key\": \"switchPanelBackgroundColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"gpioStatusRequest\",\n \"items\": [\n \"gpioStatusRequest.method\",\n {\n \"key\": \"gpioStatusRequest.paramsBody\",\n \"type\": \"json\"\n }\n ]\n },\n {\n \"key\": \"gpioStatusChangeRequest\",\n \"items\": [\n \"gpioStatusChangeRequest.method\",\n {\n \"key\": \"gpioStatusChangeRequest.paramsBody\",\n \"type\": \"json\"\n }\n ]\n },\n {\n \"key\": \"parseGpioStatusFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/rpc/parse_gpio_status_fn\"\n }\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-gpio-control-widget-settings", "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"parseGpioStatusFunction\":\"return body[pin] === true;\",\"gpioStatusChangeRequest\":{\"method\":\"setGpioStatus\",\"paramsBody\":\"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"},\"requestTimeout\":500,\"switchPanelBackgroundColor\":\"#008a00\",\"gpioStatusRequest\":{\"method\":\"getGpioStatus\",\"paramsBody\":\"{}\"},\"gpioList\":[{\"pin\":7,\"label\":\"GPIO 4 (GPCLK0)\",\"row\":3,\"col\":0,\"_uniqueKey\":0},{\"pin\":11,\"label\":\"GPIO 17\",\"row\":5,\"col\":0,\"_uniqueKey\":1},{\"pin\":12,\"label\":\"GPIO 18\",\"row\":5,\"col\":1,\"_uniqueKey\":2},{\"_uniqueKey\":3,\"pin\":13,\"label\":\"GPIO 27\",\"row\":6,\"col\":0},{\"_uniqueKey\":4,\"pin\":15,\"label\":\"GPIO 22\",\"row\":7,\"col\":0},{\"_uniqueKey\":5,\"pin\":16,\"label\":\"GPIO 23\",\"row\":7,\"col\":1},{\"_uniqueKey\":6,\"pin\":18,\"label\":\"GPIO 24\",\"row\":8,\"col\":1},{\"_uniqueKey\":7,\"pin\":22,\"label\":\"GPIO 25\",\"row\":10,\"col\":1},{\"_uniqueKey\":8,\"pin\":29,\"label\":\"GPIO 5\",\"row\":14,\"col\":0},{\"_uniqueKey\":9,\"pin\":31,\"label\":\"GPIO 6\",\"row\":15,\"col\":0},{\"_uniqueKey\":10,\"pin\":32,\"label\":\"GPIO 12\",\"row\":15,\"col\":1},{\"_uniqueKey\":11,\"pin\":33,\"label\":\"GPIO 13\",\"row\":16,\"col\":0},{\"_uniqueKey\":12,\"pin\":35,\"label\":\"GPIO 19\",\"row\":17,\"col\":0},{\"_uniqueKey\":13,\"pin\":36,\"label\":\"GPIO 16\",\"row\":17,\"col\":1},{\"_uniqueKey\":14,\"pin\":37,\"label\":\"GPIO 26\",\"row\":18,\"col\":0},{\"_uniqueKey\":15,\"pin\":38,\"label\":\"GPIO 20\",\"row\":18,\"col\":1},{\"_uniqueKey\":16,\"pin\":40,\"label\":\"GPIO 21\",\"row\":19,\"col\":1}]},\"title\":\"Raspberry Pi GPIO Control\"}" } } diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html index 9cfe9611fc..4f85de08b0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html @@ -34,6 +34,7 @@ [dataKeySettingsDirective]="data.dataKeySettingsDirective" [entityAliasId]="data.entityAliasId" [dashboard]="data.dashboard" + [aliasController]="data.aliasController" [widget]="data.widget" [showPostProcessing]="data.showPostProcessing" [callbacks]="data.callbacks" diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts index 4e88f389dc..5c67242958 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts @@ -26,12 +26,14 @@ import { DataKey, Widget } from '@shared/models/widget.models'; import { DataKeysCallbacks } from './data-keys.component.models'; import { DataKeyConfigComponent } from '@home/components/widget/data-key-config.component'; import { Dashboard } from '@shared/models/dashboard.models'; +import { IAliasController } from '@core/api/widget-api.models'; export interface DataKeyConfigDialogData { dataKey: DataKey; dataKeySettingsSchema: any; dataKeySettingsDirective: string; dashboard: Dashboard; + aliasController: IAliasController; widget: Widget; entityAliasId?: string; showPostProcessing?: boolean; diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html index f6e9acda1b..1fbd396cce 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html @@ -101,6 +101,7 @@ diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts index b0d8847c1b..6ce4a98a09 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts @@ -42,6 +42,7 @@ import { JsFuncComponent } from '@shared/components/js-func.component'; import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; import { WidgetService } from '@core/http/widget.service'; import { Dashboard } from '@shared/models/dashboard.models'; +import { IAliasController } from '@core/api/widget-api.models'; @Component({ selector: 'tb-data-key-config', @@ -73,6 +74,9 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con @Input() dashboard: Dashboard; + @Input() + aliasController: IAliasController; + @Input() widget: Widget; diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts index fc36cb5ae6..d2361bbf9d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts @@ -422,6 +422,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie dataKeySettingsSchema: this.datakeySettingsSchema, dataKeySettingsDirective: this.dataKeySettingsDirective, dashboard: this.dashboard, + aliasController: this.aliasController, widget: this.widget, entityAliasId: this.entityAliasId, showPostProcessing: this.widgetType !== widgetType.alarm, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.html index 472978c097..5c7598b7c6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-settings.component.html @@ -25,7 +25,7 @@
-
= []; - if (settings.labels) { - settings.labels.forEach((label) => { + if (labels) { + labels.forEach((label) => { labelsControls.push(this.fb.control(label, [Validators.required])); }); } - this.labelWidgetSettingsForm = this.fb.group({ - backgroundImageUrl: [settings.backgroundImageUrl, [Validators.required]], - labels: this.fb.array(labelsControls) - }); + return this.fb.array(labelsControls); } labelsFormArray(): FormArray { return this.labelWidgetSettingsForm.get('labels') as FormArray; } - public trackByLabel(index: number, labelControl: AbstractControl): any { - const label: LabelWidgetLabel = labelControl.value; - return label; + public trackByLabelControl(index: number, labelControl: AbstractControl): any { + return labelControl; } public removeLabel(index: number) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-gauge-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-gauge-widget-settings.component.html index 3da794f7ab..fcce29b1b4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-gauge-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-gauge-widget-settings.component.html @@ -204,7 +204,7 @@
-
= []; - if (settings.highlights) { - settings.highlights.forEach((highlight) => { - highlightsControls.push(this.fb.control(highlight, [Validators.required])); - }); - } this.analogueGaugeWidgetSettingsForm = this.fb.group({ // Radial gauge settings @@ -155,7 +149,7 @@ export class AnalogueGaugeWidgetSettingsComponent extends WidgetSettingsComponen // Highlights settings highlightsWidth: [settings.highlightsWidth, [Validators.min(0)]], - highlights: this.fb.array(highlightsControls), + highlights: this.prepareHighlightsFormArray(settings.highlights), // Animation settings animation: [settings.animation, []], @@ -214,13 +208,26 @@ export class AnalogueGaugeWidgetSettingsComponent extends WidgetSettingsComponen this.analogueGaugeWidgetSettingsForm.get('animationRule').updateValueAndValidity({emitEvent}); } + protected doUpdateSettings(settingsForm: FormGroup, settings: WidgetSettings) { + settingsForm.setControl('highlights', this.prepareHighlightsFormArray(settings.highlights), {emitEvent: false}); + } + + private prepareHighlightsFormArray(highlights: GaugeHighlight[] | undefined): FormArray { + const highlightsControls: Array = []; + if (highlights) { + highlights.forEach((highlight) => { + highlightsControls.push(this.fb.control(highlight, [Validators.required])); + }); + } + return this.fb.array(highlightsControls); + } + highlightsFormArray(): FormArray { return this.analogueGaugeWidgetSettingsForm.get('highlights') as FormArray; } - public trackByHighlight(index: number, highlightControl: AbstractControl): any { - const highlight: GaugeHighlight = highlightControl.value; - return highlight; + public trackByHighlightControl(index: number, highlightControl: AbstractControl): any { + return highlightControl; } public removeHighlight(index: number) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.ts index 40958f8965..6b4cf54b15 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.ts @@ -21,7 +21,6 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { GaugeType } from '@home/components/widget/lib/canvas-digital-gauge'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; -import { GaugeHighlight } from '@home/components/widget/lib/settings/gauge/gauge-highlight.component'; import { FixedColorLevel, fixedColorLevelValidator @@ -108,26 +107,6 @@ export class DigitalGaugeWidgetSettingsComponent extends WidgetSettingsComponent } protected onSettingsSet(settings: WidgetSettings) { - - const levelColorsControls: Array = []; - const fixedLevelColorsControls: Array = []; - const ticksValueControls: Array = []; - if (settings.levelColors) { - settings.levelColors.forEach((levelColor) => { - levelColorsControls.push(this.fb.control(levelColor, [Validators.required])); - }); - } - if (settings.fixedLevelColors) { - settings.fixedLevelColors.forEach((fixedLevelColor) => { - fixedLevelColorsControls.push(this.fb.control(fixedLevelColor, [fixedColorLevelValidator])); - }); - } - if (settings.ticksValue) { - settings.ticksValue.forEach((tickValue) => { - ticksValueControls.push(this.fb.control(tickValue, [Validators.required])); - }); - } - this.digitalGaugeWidgetSettingsForm = this.fb.group({ // Common gauge settings @@ -146,8 +125,8 @@ export class DigitalGaugeWidgetSettingsComponent extends WidgetSettingsComponent // Gauge bar colors settings gaugeColor: [settings.gaugeColor, []], useFixedLevelColor: [settings.useFixedLevelColor, []], - levelColors: this.fb.array(levelColorsControls), - fixedLevelColors: this.fb.array(fixedLevelColorsControls), + levelColors: this.prepareLevelColorFormArray(settings.levelColors), + fixedLevelColors: this.prepareFixedLevelColorFormArray(settings.fixedLevelColors), // Title settings showTitle: [settings.showTitle, []], @@ -173,7 +152,7 @@ export class DigitalGaugeWidgetSettingsComponent extends WidgetSettingsComponent showTicks: [settings.showTicks, []], tickWidth: [settings.tickWidth, [Validators.min(0)]], colorTicks: [settings.colorTicks, []], - ticksValue: this.fb.array(ticksValueControls), + ticksValue: this.prepareTicksValueFormArray(settings.ticksValue), // Animation settings animation: [settings.animation, []], @@ -275,12 +254,48 @@ export class DigitalGaugeWidgetSettingsComponent extends WidgetSettingsComponent this.digitalGaugeWidgetSettingsForm.get('animationRule').updateValueAndValidity({emitEvent}); } + protected doUpdateSettings(settingsForm: FormGroup, settings: WidgetSettings) { + settingsForm.setControl('levelColors', this.prepareLevelColorFormArray(settings.levelColors), {emitEvent: false}); + settingsForm.setControl('fixedLevelColors', this.prepareFixedLevelColorFormArray(settings.fixedLevelColors), {emitEvent: false}); + settingsForm.setControl('ticksValue', this.prepareTicksValueFormArray(settings.ticksValue), {emitEvent: false}); + } + + private prepareLevelColorFormArray(levelColors: string[] | undefined): FormArray { + const levelColorsControls: Array = []; + if (levelColors) { + levelColors.forEach((levelColor) => { + levelColorsControls.push(this.fb.control(levelColor, [Validators.required])); + }); + } + return this.fb.array(levelColorsControls); + } + + private prepareFixedLevelColorFormArray(fixedLevelColors: FixedColorLevel[] | undefined): FormArray { + const fixedLevelColorsControls: Array = []; + if (fixedLevelColors) { + fixedLevelColors.forEach((fixedLevelColor) => { + fixedLevelColorsControls.push(this.fb.control(fixedLevelColor, [fixedColorLevelValidator])); + }); + } + return this.fb.array(fixedLevelColorsControls); + } + + private prepareTicksValueFormArray(ticksValue: ValueSourceProperty[] | undefined): FormArray { + const ticksValueControls: Array = []; + if (ticksValue) { + ticksValue.forEach((tickValue) => { + ticksValueControls.push(this.fb.control(tickValue, [Validators.required])); + }); + } + return this.fb.array(ticksValueControls); + } + levelColorsFormArray(): FormArray { return this.digitalGaugeWidgetSettingsForm.get('levelColors') as FormArray; } public trackByLevelColor(index: number, levelColorControl: AbstractControl): any { - return index; + return levelColorControl; } public removeLevelColor(index: number) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-control-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-control-widget-settings.component.html new file mode 100644 index 0000000000..cef8746c93 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-control-widget-settings.component.html @@ -0,0 +1,96 @@ + +
+
+ widgets.gpio.panel-settings + + +
+ widgets.gpio.gpio-switches +
+
+
+ + +
+
+
+ widgets.gpio.no-gpio-switches +
+
+ +
+
+
+
+
+ widgets.rpc.rpc-settings + + widgets.rpc.request-timeout + + +
+ widgets.gpio.gpio-status-request + + widgets.gpio.method-name + + + + +
+
+ widgets.gpio.gpio-status-change-request + + widgets.gpio.method-name + + + + +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-control-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-control-widget-settings.component.ts new file mode 100644 index 0000000000..b398394d1a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-control-widget-settings.component.ts @@ -0,0 +1,145 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { AbstractControl, FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { GpioItem, gpioItemValidator } from '@home/components/widget/lib/settings/gpio/gpio-item.component'; +import { ContentType } from '@shared/models/constants'; + +@Component({ + selector: 'tb-gpio-control-widget-settings', + templateUrl: './gpio-control-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class GpioControlWidgetSettingsComponent extends WidgetSettingsComponent { + + gpioControlWidgetSettingsForm: FormGroup; + + contentTypes = ContentType; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.gpioControlWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + switchPanelBackgroundColor: '#008a00', + gpioList: [], + requestTimeout: 500, + gpioStatusRequest: { + method: 'getGpioStatus', + paramsBody: '{}' + }, + gpioStatusChangeRequest: { + method: 'setGpioStatus', + paramsBody: '{\n "pin": "{$pin}",\n "enabled": "{$enabled}"\n}' + }, + parseGpioStatusFunction: 'return body[pin] === true;' + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.gpioControlWidgetSettingsForm = this.fb.group({ + + // Panel settings + + switchPanelBackgroundColor: [settings.switchPanelBackgroundColor, [Validators.required]], + + // --> GPIO switches + + gpioList: this.prepareGpioListFormArray(settings.gpioList), + + // RPC settings + + requestTimeout: [settings.requestTimeout, [Validators.min(0), Validators.required]], + + // --> GPIO status request + + gpioStatusRequest: this.fb.group({ + method: [settings.gpioStatusRequest?.method, [Validators.required]], + paramsBody: [settings.gpioStatusRequest?.paramsBody, [Validators.required]] + }, {validators: [Validators.required]}), + + // --> GPIO status change request + + gpioStatusChangeRequest: this.fb.group({ + method: [settings.gpioStatusChangeRequest?.method, [Validators.required]], + paramsBody: [settings.gpioStatusChangeRequest?.paramsBody, [Validators.required]] + }, {validators: [Validators.required]}), + + parseGpioStatusFunction: [settings.parseGpioStatusFunction, [Validators.required]] + }); + } + + protected doUpdateSettings(settingsForm: FormGroup, settings: WidgetSettings) { + settingsForm.setControl('gpioList', this.prepareGpioListFormArray(settings.gpioList), {emitEvent: false}); + } + + private prepareGpioListFormArray(gpioList: GpioItem[] | undefined): FormArray { + const gpioListControls: Array = []; + if (gpioList) { + gpioList.forEach((gpioItem) => { + gpioListControls.push(this.fb.control(gpioItem, [gpioItemValidator(false)])); + }); + } + return this.fb.array(gpioListControls, [(control: AbstractControl) => { + const gpioItems = control.value; + if (!gpioItems || !gpioItems.length) { + return { + gpioItems: true + }; + } + return null; + }]); + } + + gpioListFormArray(): FormArray { + return this.gpioControlWidgetSettingsForm.get('gpioList') as FormArray; + } + + public trackByGpioItem(index: number, gpioItemControl: AbstractControl): any { + return gpioItemControl; + } + + public removeGpioItem(index: number) { + (this.gpioControlWidgetSettingsForm.get('gpioList') as FormArray).removeAt(index); + } + + public addGpioItem() { + const gpioItem: GpioItem = { + pin: null, + label: null, + row: null, + col: null + }; + const gpioListArray = this.gpioControlWidgetSettingsForm.get('gpioList') as FormArray; + const gpioItemControl = this.fb.control(gpioItem, [gpioItemValidator(false)]); + (gpioItemControl as any).new = true; + gpioListArray.push(gpioItemControl); + this.gpioControlWidgetSettingsForm.updateValueAndValidity(); + if (!this.gpioControlWidgetSettingsForm.valid) { + this.onSettingsChanged(this.gpioControlWidgetSettingsForm.value); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-item.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-item.component.html new file mode 100644 index 0000000000..9ebbdeefd9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-item.component.html @@ -0,0 +1,73 @@ + + + +
+ +
+
+
+
+
+
+
+ + +
+
+ +
+ +
+
+ + widgets.gpio.pin + + + + widgets.gpio.label + + +
+
+ + widgets.gpio.row + + + + widgets.gpio.column + + +
+ + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-item.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-item.component.scss new file mode 100644 index 0000000000..5190223be1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-item.component.scss @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2022 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. + */ +:host { + display: block; + .mat-expansion-panel { + box-shadow: none; + &.gpio-item { + border: 1px groove rgba(0, 0, 0, .25); + .mat-expansion-panel-header { + padding: 0 24px 0 8px; + &.mat-expanded { + height: 48px; + } + } + } + } +} + +:host ::ng-deep { + .mat-expansion-panel { + &.gpio-item { + .mat-expansion-panel-body { + padding: 0 8px 8px; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-item.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-item.component.ts new file mode 100644 index 0000000000..9d3c9ef8de --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-item.component.ts @@ -0,0 +1,150 @@ +/// +/// Copyright © 2016-2022 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 { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormBuilder, + FormGroup, + NG_VALUE_ACCESSOR, ValidationErrors, ValidatorFn, + Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { isNumber } from '@core/utils'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; + +export interface GpioItem { + pin: number; + label: string; + row: number; + col: number; + color?: string; +} + +export function gpioItemValidator(hasColor: boolean): ValidatorFn { + return (control: AbstractControl) => { + const gpioItem: GpioItem = control.value; + if (!gpioItem + || !isNumber(gpioItem.pin) || gpioItem.pin < 1 + || !isNumber(gpioItem.row) || gpioItem.row < 0 + || !isNumber(gpioItem.col) || gpioItem.col < 0 || gpioItem.col > 1 + || !gpioItem.label + || (hasColor && !gpioItem.color) + ) { + return { + gpioItem: true + }; + } + return null; + }; +} + +@Component({ + selector: 'tb-gpio-item', + templateUrl: './gpio-item.component.html', + styleUrls: ['./gpio-item.component.scss', './../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => GpioItemComponent), + multi: true + } + ] +}) +export class GpioItemComponent extends PageComponent implements OnInit, ControlValueAccessor { + + @Input() + disabled: boolean; + + @Input() + expanded = false; + + @Input() + hasColor = false; + + @Output() + removeGpioItem = new EventEmitter(); + + private modelValue: GpioItem; + + private propagateChange = null; + + public gpioItemFormGroup: FormGroup; + + constructor(protected store: Store, + private translate: TranslateService, + private domSanitizer: DomSanitizer, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.gpioItemFormGroup = this.fb.group({ + pin: [null, [Validators.required, Validators.min(1)]], + label: [null, [Validators.required]], + row: [null, [Validators.required, Validators.min(0)]], + col: [null, [Validators.required, Validators.min(0), Validators.max(1)]] + }); + if (this.hasColor) { + this.gpioItemFormGroup.addControl('color', this.fb.control(null, [Validators.required])); + } + this.gpioItemFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.gpioItemFormGroup.disable({emitEvent: false}); + } else { + this.gpioItemFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: GpioItem): void { + this.modelValue = value; + this.gpioItemFormGroup.patchValue( + value, {emitEvent: false} + ); + } + + gpioItemHtml(): SafeHtml { + const value: GpioItem = this.gpioItemFormGroup.value; + const pin = isNumber(value.pin) && value.pin > 0 ? value.pin : 'Undefined'; + const row = isNumber(value.row) && value.row > -1 ? value.row : 'Undefined'; + const col = isNumber(value.col) && value.col > -1 ? value.col : 'Undefined'; + const label = value.label || 'Undefined'; + return this.domSanitizer.bypassSecurityTrustHtml(`${label} (pin:${pin}) - [row:${row}:col:${col}]`); + } + + private updateModel() { + const value: GpioItem = this.gpioItemFormGroup.value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-panel-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-panel-widget-settings.component.html new file mode 100644 index 0000000000..1562e42496 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-panel-widget-settings.component.html @@ -0,0 +1,55 @@ + +
+
+ widgets.gpio.panel-settings + + +
+ widgets.gpio.gpio-leds +
+
+
+ + +
+
+
+ widgets.gpio.no-gpio-leds +
+
+ +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-panel-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-panel-widget-settings.component.ts new file mode 100644 index 0000000000..d39ceade3e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gpio/gpio-panel-widget-settings.component.ts @@ -0,0 +1,114 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { AbstractControl, FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { GpioItem, gpioItemValidator } from '@home/components/widget/lib/settings/gpio/gpio-item.component'; + +@Component({ + selector: 'tb-gpio-panel-widget-settings', + templateUrl: './gpio-panel-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class GpioPanelWidgetSettingsComponent extends WidgetSettingsComponent { + + gpioPanelWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.gpioPanelWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + ledPanelBackgroundColor: '#008a00', + gpioList: [] + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.gpioPanelWidgetSettingsForm = this.fb.group({ + + // Panel settings + + ledPanelBackgroundColor: [settings.ledPanelBackgroundColor, [Validators.required]], + + // --> GPIO leds + + gpioList: this.prepareGpioListFormArray(settings.gpioList), + + }); + } + + protected doUpdateSettings(settingsForm: FormGroup, settings: WidgetSettings) { + settingsForm.setControl('gpioList', this.prepareGpioListFormArray(settings.gpioList), {emitEvent: false}); + } + + private prepareGpioListFormArray(gpioList: GpioItem[] | undefined): FormArray { + const gpioListControls: Array = []; + if (gpioList) { + gpioList.forEach((gpioItem) => { + gpioListControls.push(this.fb.control(gpioItem, [gpioItemValidator(true)])); + }); + } + return this.fb.array(gpioListControls, [(control: AbstractControl) => { + const gpioItems = control.value; + if (!gpioItems || !gpioItems.length) { + return { + gpioItems: true + }; + } + return null; + }]); + } + + gpioListFormArray(): FormArray { + return this.gpioPanelWidgetSettingsForm.get('gpioList') as FormArray; + } + + public trackByGpioItem(index: number, gpioItemControl: AbstractControl): any { + return gpioItemControl; + } + + public removeGpioItem(index: number) { + (this.gpioPanelWidgetSettingsForm.get('gpioList') as FormArray).removeAt(index); + } + + public addGpioItem() { + const gpioItem: GpioItem = { + pin: null, + label: null, + row: null, + col: null, + color: null + }; + const gpioListArray = this.gpioPanelWidgetSettingsForm.get('gpioList') as FormArray; + const gpioItemControl = this.fb.control(gpioItem, [gpioItemValidator(true)]); + (gpioItemControl as any).new = true; + gpioListArray.push(gpioItemControl); + this.gpioPanelWidgetSettingsForm.updateValueAndValidity(); + if (!this.gpioPanelWidgetSettingsForm.valid) { + this.onSettingsChanged(this.gpioPanelWidgetSettingsForm.value); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index cd27ca9135..6c9993b0b4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -156,6 +156,13 @@ import { import { GatewayEventsWidgetSettingsComponent } from '@home/components/widget/lib/settings/gateway/gateway-events-widget-settings.component'; +import { GpioItemComponent } from '@home/components/widget/lib/settings/gpio/gpio-item.component'; +import { + GpioControlWidgetSettingsComponent +} from '@home/components/widget/lib/settings/gpio/gpio-control-widget-settings.component'; +import { + GpioPanelWidgetSettingsComponent +} from '@home/components/widget/lib/settings/gpio/gpio-panel-widget-settings.component'; @NgModule({ declarations: [ @@ -213,7 +220,10 @@ import { EdgeQuickOverviewWidgetSettingsComponent, GatewayConfigWidgetSettingsComponent, GatewayConfigSingleDeviceWidgetSettingsComponent, - GatewayEventsWidgetSettingsComponent + GatewayEventsWidgetSettingsComponent, + GpioItemComponent, + GpioControlWidgetSettingsComponent, + GpioPanelWidgetSettingsComponent ], imports: [ CommonModule, @@ -275,7 +285,10 @@ import { EdgeQuickOverviewWidgetSettingsComponent, GatewayConfigWidgetSettingsComponent, GatewayConfigSingleDeviceWidgetSettingsComponent, - GatewayEventsWidgetSettingsComponent + GatewayEventsWidgetSettingsComponent, + GpioItemComponent, + GpioControlWidgetSettingsComponent, + GpioPanelWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -323,5 +336,7 @@ export const widgetSettingsComponentsMap: {[key: string]: Type
- +
diff --git a/ui-ngx/src/app/shared/components/material-icon-select.component.scss b/ui-ngx/src/app/shared/components/material-icon-select.component.scss index cd77a15453..2227bdab95 100644 --- a/ui-ngx/src/app/shared/components/material-icon-select.component.scss +++ b/ui-ngx/src/app/shared/components/material-icon-select.component.scss @@ -14,7 +14,7 @@ * limitations under the License. */ :host { - .mat-icon { + .mat-icon.icon-value { padding: 4px; margin: 8px 4px 4px; cursor: pointer; diff --git a/ui-ngx/src/app/shared/components/material-icon-select.component.ts b/ui-ngx/src/app/shared/components/material-icon-select.component.ts index 14c7d3e9b9..868ed79c58 100644 --- a/ui-ngx/src/app/shared/components/material-icon-select.component.ts +++ b/ui-ngx/src/app/shared/components/material-icon-select.component.ts @@ -20,6 +20,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; import { DialogService } from '@core/services/dialog.service'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; @Component({ selector: 'tb-material-icon-select', @@ -38,6 +39,27 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit @Input() disabled: boolean; + private iconClearButtonValue: boolean; + get iconClearButton(): boolean { + return this.iconClearButtonValue; + } + @Input() + set iconClearButton(value: boolean) { + const newVal = coerceBooleanProperty(value); + if (this.iconClearButtonValue !== newVal) { + this.iconClearButtonValue = newVal; + } + } + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + private modelValue: string; private propagateChange = null; @@ -104,4 +126,8 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit ); } } + + clear() { + this.materialIconFormGroup.get('icon').patchValue(null, {emitEvent: true}); + } } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 343d706ea5..c2b149ae57 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3615,6 +3615,16 @@ "no-labels": "No labels configured", "add-label": "Add label" }, + "navigation": { + "title": "Title", + "navigation-path": "Navigation path", + "filter-type": "Filter type", + "filter-type-all": "All items", + "filter-type-include": "Include items", + "filter-type-exclude": "Exclude items", + "items": "Items", + "enter-urls-to-filter": "Enter urls to filter..." + }, "persistent-table": { "rpc-id": "RPC ID", "message-type": "Message type", From 8adc0c739fd998935e550b3cef440e453100b84a Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 28 Apr 2022 18:45:58 +0300 Subject: [PATCH 065/276] removed sql test suites in favour independent test run, fixed NoSqlDaoServiceTestSuite --- application/pom.xml | 3 - .../controller/ControllerSqlTestSuite.java | 33 ------ .../server/edge/EdgeSqlTestSuite.java | 29 ----- .../server/rules/RuleEngineSqlTestSuite.java | 30 ----- .../server/service/ServiceSqlTestSuite.java | 30 ----- .../server/system/SystemSqlTestSuite.java | 32 ----- .../transport/TransportSqlTestSuite.java | 37 ------ dao/pom.xml | 4 - .../thingsboard/server/dao/CustomSqlUnit.java | 111 ------------------ .../server/dao/JpaDaoTestSuite.java | 28 ----- .../server/dao/JpaDbunitTestConfig.java | 54 --------- .../server/dao/NoSqlDaoServiceTestSuite.java | 2 +- .../server/dao/SqlDaoServiceTestSuite.java | 31 ----- pom.xml | 4 +- 14 files changed, 3 insertions(+), 425 deletions(-) delete mode 100644 application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java delete mode 100644 application/src/test/java/org/thingsboard/server/edge/EdgeSqlTestSuite.java delete mode 100644 application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.java delete mode 100644 application/src/test/java/org/thingsboard/server/service/ServiceSqlTestSuite.java delete mode 100644 application/src/test/java/org/thingsboard/server/system/SystemSqlTestSuite.java delete mode 100644 application/src/test/java/org/thingsboard/server/transport/TransportSqlTestSuite.java delete mode 100644 dao/src/test/java/org/thingsboard/server/dao/CustomSqlUnit.java delete mode 100644 dao/src/test/java/org/thingsboard/server/dao/JpaDaoTestSuite.java delete mode 100644 dao/src/test/java/org/thingsboard/server/dao/JpaDbunitTestConfig.java delete mode 100644 dao/src/test/java/org/thingsboard/server/dao/SqlDaoServiceTestSuite.java diff --git a/application/pom.xml b/application/pom.xml index 98a65bc5bb..4330300adf 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -369,14 +369,11 @@ org.apache.maven.plugins maven-surefire-plugin - ${surfire.version} thingsboard - **/sql/*Test.java - **/psql/*Test.java **/nosql/*Test.java diff --git a/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java deleted file mode 100644 index 9dcba19044..0000000000 --- a/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.controller; - -import org.junit.BeforeClass; -import org.junit.extensions.cpsuite.ClasspathSuite; -import org.junit.runner.RunWith; -import org.thingsboard.server.queue.memory.InMemoryStorage; - -@RunWith(ClasspathSuite.class) -@ClasspathSuite.ClassnameFilters({ -// "org.thingsboard.server.controller.sql.WebsocketApiSqlTest", -// "org.thingsboard.server.controller.sql.EntityQueryControllerSqlTest", -// "org.thingsboard.server.controller.sql.TbResourceControllerSqlTest", -// "org.thingsboard.server.controller.sql.DeviceProfileControllerSqlTest", - "org.thingsboard.server.controller.sql.*Test", -}) -public class ControllerSqlTestSuite { - -} diff --git a/application/src/test/java/org/thingsboard/server/edge/EdgeSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/edge/EdgeSqlTestSuite.java deleted file mode 100644 index e7fff461f3..0000000000 --- a/application/src/test/java/org/thingsboard/server/edge/EdgeSqlTestSuite.java +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.edge; - -import org.junit.BeforeClass; -import org.junit.extensions.cpsuite.ClasspathSuite; -import org.junit.runner.RunWith; -import org.thingsboard.server.queue.memory.InMemoryStorage; - -@RunWith(ClasspathSuite.class) -@ClasspathSuite.ClassnameFilters({ - "org.thingsboard.server.edge.sql.*Test", -}) -public class EdgeSqlTestSuite { - -} diff --git a/application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.java deleted file mode 100644 index cb99c21165..0000000000 --- a/application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.java +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.rules; - -import org.junit.BeforeClass; -import org.junit.extensions.cpsuite.ClasspathSuite; -import org.junit.runner.RunWith; -import org.thingsboard.server.queue.memory.InMemoryStorage; - -@RunWith(ClasspathSuite.class) -@ClasspathSuite.ClassnameFilters({ - "org.thingsboard.server.rules.flow.sql.*Test", - "org.thingsboard.server.rules.lifecycle.sql.*Test", -}) -public class RuleEngineSqlTestSuite { - -} diff --git a/application/src/test/java/org/thingsboard/server/service/ServiceSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/service/ServiceSqlTestSuite.java deleted file mode 100644 index 11f06875fe..0000000000 --- a/application/src/test/java/org/thingsboard/server/service/ServiceSqlTestSuite.java +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.service; - -import org.junit.BeforeClass; -import org.junit.extensions.cpsuite.ClasspathSuite; -import org.junit.runner.RunWith; -import org.thingsboard.server.queue.memory.InMemoryStorage; - -@RunWith(ClasspathSuite.class) -@ClasspathSuite.ClassnameFilters({ - "org.thingsboard.server.service.resource.sql.*Test", - "org.thingsboard.server.service.sql.*Test" -}) -public class ServiceSqlTestSuite { - -} diff --git a/application/src/test/java/org/thingsboard/server/system/SystemSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/system/SystemSqlTestSuite.java deleted file mode 100644 index 714ad67519..0000000000 --- a/application/src/test/java/org/thingsboard/server/system/SystemSqlTestSuite.java +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.system; - -import org.junit.BeforeClass; -import org.junit.extensions.cpsuite.ClasspathSuite; -import org.junit.runner.RunWith; -import org.thingsboard.server.queue.memory.InMemoryStorage; - -/** - * Created by Valerii Sosliuk on 6/27/2017. - */ -@RunWith(ClasspathSuite.class) -@ClasspathSuite.ClassnameFilters({ - "org.thingsboard.server.system.sql.*SqlTest", -}) -public class SystemSqlTestSuite { - -} diff --git a/application/src/test/java/org/thingsboard/server/transport/TransportSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/transport/TransportSqlTestSuite.java deleted file mode 100644 index be416b3a69..0000000000 --- a/application/src/test/java/org/thingsboard/server/transport/TransportSqlTestSuite.java +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.transport; - -import org.junit.BeforeClass; -import org.junit.extensions.cpsuite.ClasspathSuite; -import org.junit.runner.RunWith; -import org.thingsboard.server.queue.memory.InMemoryStorage; - -@RunWith(ClasspathSuite.class) -@ClasspathSuite.ClassnameFilters({ - "org.thingsboard.server.transport.*.rpc.sql.*Test", - "org.thingsboard.server.transport.*.telemetry.timeseries.sql.*Test", - "org.thingsboard.server.transport.*.telemetry.attributes.sql.*Test", - "org.thingsboard.server.transport.*.attributes.updates.sql.*Test", - "org.thingsboard.server.transport.*.attributes.request.sql.*Test", - "org.thingsboard.server.transport.*.claim.sql.*Test", - "org.thingsboard.server.transport.*.provision.sql.*Test", - "org.thingsboard.server.transport.*.credentials.sql.*Test", - "org.thingsboard.server.transport.lwm2m.*.sql.*Test" -}) -public class TransportSqlTestSuite { - -} diff --git a/dao/pom.xml b/dao/pom.xml index 17d982d02e..1528a74869 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -234,12 +234,8 @@ org.apache.maven.plugins maven-surefire-plugin - ${surfire.version} - **/sql/*Test.java - **/sql/*/*DaoTest.java - **/psql/*Test.java **/nosql/*Test.java diff --git a/dao/src/test/java/org/thingsboard/server/dao/CustomSqlUnit.java b/dao/src/test/java/org/thingsboard/server/dao/CustomSqlUnit.java deleted file mode 100644 index 73e12f6e78..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/CustomSqlUnit.java +++ /dev/null @@ -1,111 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.dao; - -import com.google.common.base.Charsets; -import com.google.common.io.Resources; -import lombok.extern.slf4j.Slf4j; -import org.junit.rules.ExternalResource; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; -import java.util.List; -import java.util.Properties; - - -/** - * Created by Valerii Sosliuk on 6/24/2017. - * - * Deprecated. Use PostgreSqlInitializer class instead - */ -@Slf4j -@Deprecated -public class CustomSqlUnit extends ExternalResource { - - private final List sqlFiles; - private final String dropAllTablesSqlFile; - private final String dbUrl; - private final String dbUserName; - private final String dbPassword; - - public CustomSqlUnit(List sqlFiles, String dropAllTablesSqlFile, String configurationFileName) { - this.sqlFiles = sqlFiles; - this.dropAllTablesSqlFile = dropAllTablesSqlFile; - final Properties properties = new Properties(); - try (final InputStream stream = this.getClass().getClassLoader().getResourceAsStream(configurationFileName)) { - properties.load(stream); - this.dbUrl = properties.getProperty("spring.datasource.url"); - this.dbUserName = properties.getProperty("spring.datasource.username"); - this.dbPassword = properties.getProperty("spring.datasource.password"); - } catch (IOException e) { - throw new RuntimeException(e.getMessage(), e); - } - } - - @Override - public void before() { - cleanUpDb(); - - Connection conn = null; - try { - conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword); - for (String sqlFile : sqlFiles) { - URL sqlFileUrl = Resources.getResource(sqlFile); - String sql = Resources.toString(sqlFileUrl, Charsets.UTF_8); - conn.createStatement().execute(sql); - } - } catch (IOException | SQLException e) { - throw new RuntimeException("Unable to start embedded hsqldb. Reason: " + e.getMessage(), e); - } finally { - if (conn != null) { - try { - conn.close(); - } catch (SQLException e) { - log.error(e.getMessage(), e); - } - } - } - } - - @Override - public void after() { - cleanUpDb(); - } - - private void cleanUpDb() { - Connection conn = null; - try { - conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword); - URL dropAllTableSqlFileUrl = Resources.getResource(dropAllTablesSqlFile); - String dropAllTablesSql = Resources.toString(dropAllTableSqlFileUrl, Charsets.UTF_8); - conn.createStatement().execute(dropAllTablesSql); - } catch (IOException | SQLException e) { - throw new RuntimeException("Unable to clean up embedded hsqldb. Reason: " + e.getMessage(), e); - } finally { - if (conn != null) { - try { - conn.close(); - } catch (SQLException e) { - log.error(e.getMessage(), e); - } - } - } - } -} diff --git a/dao/src/test/java/org/thingsboard/server/dao/JpaDaoTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/JpaDaoTestSuite.java deleted file mode 100644 index fccbc705b2..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/JpaDaoTestSuite.java +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.dao; - -import org.junit.extensions.cpsuite.ClasspathSuite; -import org.junit.extensions.cpsuite.ClasspathSuite.ClassnameFilters; -import org.junit.runner.RunWith; - -@RunWith(ClasspathSuite.class) -@ClassnameFilters({ - "org.thingsboard.server.dao.sql.*Test" -}) -public class JpaDaoTestSuite { - -} diff --git a/dao/src/test/java/org/thingsboard/server/dao/JpaDbunitTestConfig.java b/dao/src/test/java/org/thingsboard/server/dao/JpaDbunitTestConfig.java deleted file mode 100644 index 744d529897..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/JpaDbunitTestConfig.java +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.dao; - -import com.github.springtestdbunit.bean.DatabaseConfigBean; -import com.github.springtestdbunit.bean.DatabaseDataSourceConnectionFactoryBean; -import org.dbunit.DatabaseUnitException; -import org.dbunit.ext.hsqldb.HsqldbDataTypeFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import javax.sql.DataSource; -import java.io.IOException; -import java.sql.SQLException; - -/** - * Created by Valerii Sosliuk on 5/6/2017. - */ -@Configuration -@Deprecated -public class JpaDbunitTestConfig { - - @Autowired - private DataSource dataSource; - - @Bean - public DatabaseConfigBean databaseConfigBean() { - DatabaseConfigBean databaseConfigBean = new DatabaseConfigBean(); - databaseConfigBean.setDatatypeFactory(new HsqldbDataTypeFactory()); - return databaseConfigBean; - } - - @Bean(name = "dbUnitDatabaseConnection") - public DatabaseDataSourceConnectionFactoryBean dbUnitDatabaseConnection() throws SQLException, DatabaseUnitException, IOException { - DatabaseDataSourceConnectionFactoryBean databaseDataSourceConnectionFactoryBean = new DatabaseDataSourceConnectionFactoryBean(); - databaseDataSourceConnectionFactoryBean.setDatabaseConfig(databaseConfigBean()); - databaseDataSourceConnectionFactoryBean.setDataSource(dataSource); - return databaseDataSourceConnectionFactoryBean; - } -} diff --git a/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java index 569535a535..838be51349 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java +++ b/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java @@ -25,7 +25,7 @@ import java.util.Arrays; @RunWith(ClasspathSuite.class) @ClassnameFilters({ - "org.thingsboard.server.dao.service.nosql.*ServiceNoSqlTest", + "org.thingsboard.server.dao.service.*.nosql.*ServiceNoSqlTest", }) public class NoSqlDaoServiceTestSuite { diff --git a/dao/src/test/java/org/thingsboard/server/dao/SqlDaoServiceTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/SqlDaoServiceTestSuite.java deleted file mode 100644 index 12f77ece14..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/SqlDaoServiceTestSuite.java +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.dao; - -import org.junit.extensions.cpsuite.ClasspathSuite; -import org.junit.extensions.cpsuite.ClasspathSuite.ClassnameFilters; -import org.junit.runner.RunWith; - -@RunWith(ClasspathSuite.class) -@ClassnameFilters({ - "org.thingsboard.server.dao.service.attributes.sql.*SqlTest", - "org.thingsboard.server.dao.service.event.sql.*SqlTest", - "org.thingsboard.server.dao.service.sql.*SqlTest", - "org.thingsboard.server.dao.service.timeseries.sql.*SqlTest", - "org.thingsboard.server.dao.service.install.sql.*SqlTest" -}) -public class SqlDaoServiceTestSuite { -} diff --git a/pom.xml b/pom.xml index 02297203e6..b733975383 100755 --- a/pom.xml +++ b/pom.xml @@ -83,7 +83,7 @@ 2.0.51.Final 1.7.0 4.8.0 - 2.19.1 + 3.0.0-M6 3.0.2 3.0.4 1.6.3 @@ -642,7 +642,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.0.0-M5 + ${surefire.version} --illegal-access=permit From 31f8d5f061eea7894c068838f7ff363483e40d43 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 28 Apr 2022 18:50:37 +0300 Subject: [PATCH 066/276] refactoring fluky testRuleChainWithOneRule --- ...actRuleEngineLifecycleIntegrationTest.java | 117 +++++++----------- 1 file changed, 46 insertions(+), 71 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java index 9f82b6ca2b..c3a9d9817a 100644 --- a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java @@ -15,30 +15,24 @@ */ package org.thingsboard.server.rules.lifecycle; -import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; +import org.awaitility.Awaitility; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; -import org.mockito.stubbing.Answer; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Event; -import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; -import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; -import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; @@ -46,16 +40,13 @@ import org.thingsboard.server.common.msg.queue.TbMsgCallback; import org.thingsboard.server.controller.AbstractRuleEngineControllerTest; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.event.EventService; -import org.thingsboard.server.queue.memory.InMemoryStorage; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static org.awaitility.Awaitility.await; -import static org.mockito.Mockito.spy; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static java.util.concurrent.TimeUnit.MILLISECONDS; /** * @author Valerii Sosliuk @@ -63,9 +54,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @Slf4j public abstract class AbstractRuleEngineLifecycleIntegrationTest extends AbstractRuleEngineControllerTest { - protected Tenant savedTenant; - protected User tenantAdmin; - @Autowired protected ActorSystemContext actorSystem; @@ -75,50 +63,14 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac @Autowired protected EventService eventService; - @Autowired - protected InMemoryStorage storage; - @Before public void beforeTest() throws Exception { - - EventService spyEventService = spy(eventService); - - Mockito.doAnswer((Answer>) invocation -> { - Object[] args = invocation.getArguments(); - Event event = (Event) args[0]; - ListenableFuture future = eventService.saveAsync(event); - try { - future.get(); - } catch (Exception e) {} - return future; - }).when(spyEventService).saveAsync(Mockito.any(Event.class)); - - ReflectionTestUtils.setField(actorSystem, "eventService", spyEventService); - - loginSysAdmin(); - - Tenant tenant = new Tenant(); - tenant.setTitle("My tenant"); - savedTenant = doPost("/api/tenant", tenant, Tenant.class); - Assert.assertNotNull(savedTenant); - ruleChainService.deleteRuleChainsByTenantId(savedTenant.getId()); - - tenantAdmin = new User(); - tenantAdmin.setAuthority(Authority.TENANT_ADMIN); - tenantAdmin.setTenantId(savedTenant.getId()); - tenantAdmin.setEmail("tenant2@thingsboard.org"); - tenantAdmin.setFirstName("Joe"); - tenantAdmin.setLastName("Downs"); - - createUserAndLogin(tenantAdmin, "testPassword1"); + loginTenantAdmin(); + ruleChainService.deleteRuleChainsByTenantId(tenantId); } @After public void afterTest() throws Exception { - loginSysAdmin(); - if (savedTenant != null) { - doDelete("/api/tenant/" + savedTenant.getId().getId().toString()).andExpect(status().isOk()); - } } @Test @@ -126,7 +78,7 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac // Creating Rule Chain RuleChain ruleChain = new RuleChain(); ruleChain.setName("Simple Rule Chain"); - ruleChain.setTenantId(savedTenant.getId()); + ruleChain.setTenantId(tenantId); ruleChain.setRoot(true); ruleChain.setDebugMode(true); ruleChain = saveRuleChain(ruleChain); @@ -149,8 +101,21 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac metaData = saveRuleChainMetaData(metaData); Assert.assertNotNull(metaData); - ruleChain = getRuleChain(ruleChain.getId()); - Assert.assertNotNull(ruleChain.getFirstRuleNodeId()); + final RuleChain ruleChainFinal = getRuleChain(ruleChain.getId()); + Assert.assertNotNull(ruleChainFinal.getFirstRuleNodeId()); + + //TODO find out why RULE_NODE update event did not appear all the time +// List rcEvents = Awaitility.await("get debug by rule chain") +// .pollInterval(10, MILLISECONDS) +// .atMost(TIMEOUT, TimeUnit.SECONDS) +// .until(() -> { +// List debugEvents = getDebugEvents(tenantId, ruleChainFinal.getFirstRuleNodeId(), 1000) +// .getData().stream().filter(x->true).collect(Collectors.toList()); +// log.warn("filtered debug events [{}]", debugEvents.size()); +// debugEvents.forEach((e) -> log.warn("event: {}", e)); +// return debugEvents; +// }, +// x -> x.size() >= 2); // Saving the device Device device = new Device(); @@ -158,35 +123,45 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac device.setType("default"); device = doPost("/api/device", device, Device.class); + log.warn("before update attr"); attributesService.save(device.getTenantId(), device.getId(), DataConstants.SERVER_SCOPE, - Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry("serverAttributeKey", "serverAttributeValue"), System.currentTimeMillis()))); - - await("total inMemory queue lag is empty").atMost(30, TimeUnit.SECONDS) - .until(() -> storage.getLagTotal() == 0); - Thread.sleep(1000); - + Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry("serverAttributeKey", "serverAttributeValue"), System.currentTimeMillis()))) + .get(TIMEOUT, TimeUnit.SECONDS); + log.warn("attr updated"); TbMsgCallback tbMsgCallback = Mockito.mock(TbMsgCallback.class); Mockito.when(tbMsgCallback.isMsgValid()).thenReturn(true); TbMsg tbMsg = TbMsg.newMsg("CUSTOM", device.getId(), new TbMsgMetaData(), "{}", tbMsgCallback); - QueueToRuleEngineMsg qMsg = new QueueToRuleEngineMsg(savedTenant.getId(), tbMsg, null, null); + QueueToRuleEngineMsg qMsg = new QueueToRuleEngineMsg(tenantId, tbMsg, null, null); // Pushing Message to the system actorSystem.tell(qMsg); - Mockito.verify(tbMsgCallback, Mockito.timeout(10000)).onSuccess(); - - - PageData eventsPage = getDebugEvents(savedTenant.getId(), ruleChain.getFirstRuleNodeId(), 1000); - List events = eventsPage.getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList()); - - Assert.assertEquals(2, events.size()); + log.warn("awaiting tbMsgCallback"); + Mockito.verify(tbMsgCallback, Mockito.timeout(TimeUnit.SECONDS.toMillis(TIMEOUT))).onSuccess(); + log.warn("awaiting events"); + List events = Awaitility.await("get debug by custom event") + .pollInterval(10, MILLISECONDS) + .atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> { + List debugEvents = getDebugEvents(tenantId, ruleChainFinal.getFirstRuleNodeId(), 1000) + .getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList()); + log.warn("filtered debug events [{}]", debugEvents.size()); + debugEvents.forEach((e) -> log.warn("event: {}", e)); + return debugEvents; + }, + x -> x.size() == 2); + log.warn("asserting.."); Event inEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.IN)).findFirst().get(); - Assert.assertEquals(ruleChain.getFirstRuleNodeId(), inEvent.getEntityId()); + Assert.assertEquals(ruleChainFinal.getFirstRuleNodeId(), inEvent.getEntityId()); Assert.assertEquals(device.getId().getId().toString(), inEvent.getBody().get("entityId").asText()); Event outEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.OUT)).findFirst().get(); - Assert.assertEquals(ruleChain.getFirstRuleNodeId(), outEvent.getEntityId()); + Assert.assertEquals(ruleChainFinal.getFirstRuleNodeId(), outEvent.getEntityId()); Assert.assertEquals(device.getId().getId().toString(), outEvent.getBody().get("entityId").asText()); + log.warn("OUT event {}", outEvent); + log.warn("OUT event metadata {}", getMetadata(outEvent)); + + Assert.assertNotNull("metadata has ss_serverAttributeKey", getMetadata(outEvent).get("ss_serverAttributeKey")); Assert.assertEquals("serverAttributeValue", getMetadata(outEvent).get("ss_serverAttributeKey").asText()); } From 2cb6b6d425110f48da8a9971fb289adb96cbee6d Mon Sep 17 00:00:00 2001 From: Vladyslav Prykhodko Date: Thu, 28 Apr 2022 23:03:37 +0300 Subject: [PATCH 067/276] UI: Add page setting fb2 --- .../http/two-factor-authentication.service.ts | 25 +++ ui-ngx/src/app/core/services/menu.service.ts | 16 +- .../home/pages/admin/admin-routing.module.ts | 15 ++ .../modules/home/pages/admin/admin.module.ts | 4 +- .../two-factor-auth-settings.component.html | 142 ++++++++++++++++ .../two-factor-auth-settings.component.scss | 0 .../two-factor-auth-settings.component.ts | 155 ++++++++++++++++++ .../shared/models/two-factor-auth.models.ts | 30 ++++ .../assets/locale/locale.constant-en_US.json | 16 ++ 9 files changed, 401 insertions(+), 2 deletions(-) create mode 100644 ui-ngx/src/app/core/http/two-factor-authentication.service.ts create mode 100644 ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts create mode 100644 ui-ngx/src/app/shared/models/two-factor-auth.models.ts diff --git a/ui-ngx/src/app/core/http/two-factor-authentication.service.ts b/ui-ngx/src/app/core/http/two-factor-authentication.service.ts new file mode 100644 index 0000000000..d608d3b362 --- /dev/null +++ b/ui-ngx/src/app/core/http/two-factor-authentication.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; +import { Observable } from 'rxjs'; +import { TwoFactorAuthSettings } from '@shared/models/two-factor-auth.models'; + +@Injectable({ + providedIn: 'root' +}) +export class TwoFactorAuthenticationService { + + constructor( + private http: HttpClient + ) { + } + + getTwoFaSettings(config?: RequestConfig): Observable { + return this.http.get(`/api/2fa/settings`, defaultHttpOptionsFromConfig(config)); + } + + saveTwoFaSettings(settings: TwoFactorAuthSettings, config?: RequestConfig): Observable { + return this.http.post(`/api/2fa/settings`, settings, defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index 4a52fc66f6..294513c782 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -108,7 +108,7 @@ export class MenuService { name: 'admin.system-settings', type: 'toggle', path: '/settings', - height: '240px', + height: '280px', icon: 'settings', pages: [ { @@ -146,6 +146,14 @@ export class MenuService { path: '/settings/oauth2', icon: 'security' }, + { + id: guid(), + name: 'admin.2fa.2fa', + type: 'link', + path: '/settings/2fa', + icon: 'mdi:two-factor-authentication', + isMdiIcon: true + }, { id: guid(), name: 'resource.resources-library', @@ -216,6 +224,12 @@ export class MenuService { icon: 'security', path: '/settings/oauth2' }, + { + name: 'admin.2fa.2fa', + icon: 'mdi:two-factor-authentication', + isMdiIcon: true, + path: '/settings/2fa' + }, { name: 'resource.resources-library', icon: 'folder', diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts index ddccbb4da4..e63eec7b52 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts @@ -32,6 +32,7 @@ import { ResourcesLibraryTableConfigResolver } from '@home/pages/admin/resource/ import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { BreadCrumbConfig } from '@shared/components/breadcrumb'; +import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-auth-settings.component'; @Injectable() export class OAuth2LoginProcessingUrlResolver implements Resolve { @@ -183,6 +184,20 @@ const routes: Routes = [ } } ] + }, + { + path: '2fa', + component: TwoFactorAuthSettingsComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], + title: 'admin.2fa.2fa', + breadcrumb: { + label: 'admin.2fa.2fa', + icon: 'mdi:two-factor-authentication', + isMdiIcon: true + } + } } ] } diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts index 326b16f950..37214d7ce5 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts @@ -28,6 +28,7 @@ import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component'; import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dialog.component'; import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component'; import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources-library.component'; +import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-auth-settings.component'; @NgModule({ declarations: @@ -39,7 +40,8 @@ import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources- SecuritySettingsComponent, OAuth2SettingsComponent, HomeSettingsComponent, - ResourcesLibraryComponent + ResourcesLibraryComponent, + TwoFactorAuthSettingsComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html new file mode 100644 index 0000000000..9a507e45f2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html @@ -0,0 +1,142 @@ +
+ + +
+ admin.2fa.2fa + + +
+
+ + +
+ +
+ + {{ 'admin.2fa.use-system-two-factor-auth-settings' | translate }} + + + + admin.2fa.total-allowed-time-for-verification + + + {{ 'admin.2fa.total-allowed-time-for-verification-required' | translate }} + + + {{ 'admin.2fa.total-allowed-time-for-verification-pattern' | translate }} + + + + admin.2fa.max-verification-failures-before-user-lockout + + + {{ 'admin.2fa.max-verification-failures-before-user-lockout-required' | translate }} + + + {{ 'admin.2fa.max-verification-failures-before-user-lockout-pattern' | translate }} + + + + admin.2fa.verification-code-send-rate-limit + + admin.2fa.verification-code-send-rate-limit-hint + + {{ 'admin.2fa.verification-code-send-rate-limit-pattern' | translate }} + + + + admin.2fa.verification-code-check-rate-limit + + admin.2fa.verification-code-check-rate-limit-hint + + {{ 'admin.2fa.verification-code-check-rate-limit-pattern' | translate }} + + +
Providers
+ +
+ + + + {{ provider.value.providerType }} + + + + + + + +
+
+
+ + admin.oauth2.login-provider + + + {{ provider }} + + + +
+ + admin.oauth2.allowed-platforms + + + {{ platformTypeTranslations.get(platform) | translate }} + + + +
+
+ + admin.oauth2.client-id + + + {{ 'admin.oauth2.client-id-required' | translate }} + + + {{ 'admin.oauth2.client-id-max-length' | translate }} + + + + + admin.oauth2.client-secret + + + {{ 'admin.oauth2.client-secret-required' | translate }} + + + {{ 'admin.oauth2.client-secret-max-length' | translate }} + + +
+ +
+
+
+
+
+ +
+
+ +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.scss b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts new file mode 100644 index 0000000000..1995605786 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts @@ -0,0 +1,155 @@ +import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ActivatedRoute } from '@angular/router'; +import { AbstractControl, FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { DialogService } from '@core/services/dialog.service'; +import { TranslateService } from '@ngx-translate/core'; +import { WINDOW } from '@core/services/window.service'; +import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; +import { AuthState } from '@core/auth/auth.models'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { Authority } from '@shared/models/authority.enum'; +import { + TwoFactorAuthProviderType, + TwoFactorAuthSettings, + TwoFactorAuthSettingsForm +} from '@shared/models/two-factor-auth.models'; + +@Component({ + selector: 'tb-2fa-settings', + templateUrl: './two-factor-auth-settings.component.html', + styleUrls: ['./two-factor-auth-settings.component.scss', './settings-card.scss'] +}) +export class TwoFactorAuthSettingsComponent extends PageComponent implements OnInit, HasConfirmForm, OnDestroy { + + private authState: AuthState = getCurrentAuthState(this.store); + private authUser = this.authState.authUser; + + twoFaFormGroup: FormGroup; + + constructor(protected store: Store, + private route: ActivatedRoute, + private twoFaService: TwoFactorAuthenticationService, + private fb: FormBuilder, + private dialogService: DialogService, + private translate: TranslateService, + @Inject(WINDOW) private window: Window) { + super(store); + } + + ngOnInit() { + this.build2faSettingsForm(); + this.twoFaService.getTwoFaSettings().subscribe((setting) => { + console.log(this.formDataPreprocessing(setting)); + }); + } + + ngOnDestroy() { + super.ngOnDestroy(); + } + + confirmForm(): FormGroup { + return this.twoFaFormGroup; + } + + isTenantAdmin(): boolean { + return this.authUser.authority === Authority.TENANT_ADMIN; + } + + save() { + + } + + private build2faSettingsForm(): void { + this.twoFaFormGroup = this.fb.group({ + useSystemTwoFactorAuthSettings: [false], + maxVerificationFailuresBeforeUserLockout: [30, [ + Validators.required, + Validators.pattern(/^\d*$/), + Validators.min(0), + Validators.max(65535) + ]], + totalAllowedTimeForVerification: [3600, [ + Validators.required, + Validators.min(1), + Validators.pattern(/^\d*$/) + ]], + verificationCodeCheckRateLimit: ['', Validators.pattern(/^[1-9]\d*:[1-9]\d*$/)], + verificationCodeSendRateLimit: ['', Validators.pattern(/^[1-9]\d*:[1-9]\d*$/)], + providers: this.fb.array([]) + }); + } + + addProviders() { + const newProviders = this.fb.group({ + providerType: [TwoFactorAuthProviderType.TOTP], + issuerName: ['', Validators.required], + smsVerificationMessageTemplate: [{ + value: 'Verification code: ${verificationCode}', + disabled: true + }, [ + Validators.required, + Validators.pattern(/\${verificationCode}/) + ]], + verificationCodeLifetime: [{ + value: 120, + disabled: true + }, [ + Validators.required, + Validators.min(1), + Validators.pattern(/^\d*$/) + ]] + }); + newProviders.get('providerType').valueChanges.subscribe(type => { + switch (type) { + case TwoFactorAuthProviderType.SMS: + newProviders.get('issuerName').disable({emitEvent: false}); + newProviders.get('smsVerificationMessageTemplate').enable({emitEvent: false}); + newProviders.get('verificationCodeLifetime').enable({emitEvent: false}); + break; + case TwoFactorAuthProviderType.TOTP: + newProviders.get('issuerName').enable({emitEvent: false}); + newProviders.get('smsVerificationMessageTemplate').disable({emitEvent: false}); + newProviders.get('verificationCodeLifetime').disable({emitEvent: false}); + break; + } + }); + if (this.providersForm.length) { + const selectProvidersType = this.providersForm.value[0].providerType; + if (selectProvidersType !== TwoFactorAuthProviderType.TOTP) { + newProviders.get('providerType').patchValue(TwoFactorAuthProviderType.SMS, {emitEvents: true}) + } + } + this.providersForm.push(newProviders); + } + + removeProviders($event: Event, index: number): void { + if ($event) { + $event.stopPropagation(); + $event.preventDefault(); + } + this.providersForm.removeAt(index); + this.providersForm.markAsTouched(); + this.providersForm.markAsDirty(); + } + + get providersForm(): FormArray { + return this.twoFaFormGroup.get('providers') as FormArray; + } + + private formDataPreprocessing(data: TwoFactorAuthSettings): TwoFactorAuthSettingsForm { + return data; + } + + private formDataPostprocessing(data: TwoFactorAuthSettingsForm): TwoFactorAuthSettings{ + return data; + } + + trackByParams(index: number): number { + return index; + } + +} diff --git a/ui-ngx/src/app/shared/models/two-factor-auth.models.ts b/ui-ngx/src/app/shared/models/two-factor-auth.models.ts new file mode 100644 index 0000000000..b2c32e6156 --- /dev/null +++ b/ui-ngx/src/app/shared/models/two-factor-auth.models.ts @@ -0,0 +1,30 @@ +export interface TwoFactorAuthSettings { + maxVerificationFailuresBeforeUserLockout: number; + providers: Array; + totalAllowedTimeForVerification: number; + useSystemTwoFactorAuthSettings: boolean; + verificationCodeCheckRateLimit: string; + verificationCodeSendRateLimit: string; +} + +export type TwoFactorAuthProviderConfig = Partial + +export interface TotpTwoFactorAuthProviderConfig { + providerType: TwoFactorAuthProviderType; + issuerName: string; +} + +export interface SmsTwoFactorAuthProviderConfig { + providerType: TwoFactorAuthProviderType; + smsVerificationMessageTemplate: string; + verificationCodeLifetime: number; +} + +export enum TwoFactorAuthProviderType{ + TOTP = 'TOTP', + SMS = 'SMS' +} + +export interface TwoFactorAuthSettingsForm extends TwoFactorAuthSettings { + +} diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index aab433c992..642592b669 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -252,6 +252,22 @@ "platform-ios": "iOS", "all-platforms": "All platforms", "allowed-platforms": "Allowed platforms" + }, + "2fa": { + "2fa": "Two-factor authentication", + "use-system-two-factor-auth-settings": "Use system two factor auth settings", + "total-allowed-time-for-verification": "Total allowed time for verification", + "total-allowed-time-for-verification-required": "Total allowed time is required.", + "total-allowed-time-for-verification-pattern": "Total allowed time must be a positive integer.", + "max-verification-failures-before-user-lockout": "Max verification failures before user lockout", + "max-verification-failures-before-user-lockout-required": "Max verification failures is required.", + "max-verification-failures-before-user-lockout-pattern": "Max verification failures must be a positive integer.", + "verification-code-check-rate-limit": "Verification code check rate limit", + "verification-code-check-rate-limit-hint": "If empty field, the limit not be apply", + "verification-code-check-rate-limit-pattern": "Verification code check limit has invalid format", + "verification-code-send-rate-limit": "Verification code send rate limit", + "verification-code-send-rate-limit-hint": "If empty field, the limit not be apply", + "verification-code-send-rate-limit-pattern": "Verification code send limit has invalid format" } }, "alarm": { From 8cde2ac848f15462c90e123c43b8b9738a34b0e7 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Fri, 29 Apr 2022 09:34:52 +0200 Subject: [PATCH 068/276] added TbAssetService --- .../server/controller/AssetController.java | 195 +++--------------- .../server/controller/DeviceController.java | 42 ++-- .../server/controller/TenantController.java | 55 ++--- .../service/asset/AssetBulkImportService.java | 6 +- .../device/DeviceBulkImportService.java | 2 +- .../entitiy/AbstractTbEntityService.java | 6 + .../DefaultTbNotificationEntityService.java | 99 +++++---- .../entitiy/TbNotificationEntityService.java | 39 +++- .../entitiy/asset/DefaultTbAssetService.java | 163 +++++++++++++++ .../service/entitiy/asset/TbAssetService.java | 42 ++++ .../device/DefaultTbDeviceService.java | 102 ++++----- .../entitiy/device/TbDeviceService.java | 26 +-- .../tenant/DefaultTbTenantService.java | 68 ++++++ .../entitiy/tenant/TbTenantService.java | 25 +++ 14 files changed, 519 insertions(+), 351 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/entitiy/tenant/TbTenantService.java diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java index 7bd9cb065d..f5654f601d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java @@ -34,13 +34,10 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntitySubtype; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetSearchQuery; -import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AssetId; @@ -54,6 +51,7 @@ import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.asset.AssetBulkImportService; +import org.thingsboard.server.service.entitiy.asset.TbAssetService; import org.thingsboard.server.service.importing.BulkImportRequest; import org.thingsboard.server.service.importing.BulkImportResult; import org.thingsboard.server.service.security.model.SecurityUser; @@ -95,6 +93,7 @@ import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE @Slf4j public class AssetController extends BaseController { private final AssetBulkImportService assetBulkImportService; + private final TbAssetService tbAssetService; public static final String ASSET_ID = "assetId"; @@ -145,39 +144,12 @@ public class AssetController extends BaseController { @RequestMapping(value = "/asset", method = RequestMethod.POST) @ResponseBody public Asset saveAsset(@ApiParam(value = "A JSON value representing the asset.") @RequestBody Asset asset) throws ThingsboardException { - try { - if (TB_SERVICE_QUEUE.equals(asset.getType())) { - throw new ThingsboardException("Unable to save asset with type " + TB_SERVICE_QUEUE, ThingsboardErrorCode.BAD_REQUEST_PARAMS); - } - - asset.setTenantId(getCurrentUser().getTenantId()); - - checkEntity(asset.getId(), asset, Resource.ASSET); - - Asset savedAsset = checkNotNull(assetService.saveAsset(asset)); - - onAssetCreatedOrUpdated(savedAsset, asset.getId() != null, getCurrentUser()); - - return savedAsset; - } catch (Exception e) { - logEntityAction(emptyId(EntityType.ASSET), asset, - null, asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e); - throw handleException(e); - } - } - - private void onAssetCreatedOrUpdated(Asset asset, boolean updated, SecurityUser user) { - try { - logEntityAction(user, asset.getId(), asset, - asset.getCustomerId(), - updated ? ActionType.UPDATED : ActionType.ADDED, null); - } catch (ThingsboardException e) { - log.error("Failed to log entity action", e); - } - - if (updated) { - sendEntityNotificationMsg(asset.getTenantId(), asset.getId(), EdgeEventActionType.UPDATED); + if (TB_SERVICE_QUEUE.equals(asset.getType())) { + throw new ThingsboardException("Unable to save asset with type " + TB_SERVICE_QUEUE, ThingsboardErrorCode.BAD_REQUEST_PARAMS); } + asset.setTenantId(getCurrentUser().getTenantId()); + checkEntity(asset.getId(), asset, Resource.ASSET); + return tbAssetService.save(asset, getCurrentUser()); } @ApiOperation(value = "Delete asset (deleteAsset)", @@ -190,21 +162,8 @@ public class AssetController extends BaseController { try { AssetId assetId = new AssetId(toUUID(strAssetId)); Asset asset = checkAssetId(assetId, Operation.DELETE); - - List relatedEdgeIds = findRelatedEdgeIds(getTenantId(), assetId); - - assetService.deleteAsset(getTenantId(), assetId); - - logEntityAction(assetId, asset, - asset.getCustomerId(), - ActionType.DELETED, null, strAssetId); - - sendDeleteNotificationMsg(getTenantId(), assetId, relatedEdgeIds); + tbAssetService.delete(asset, getCurrentUser()).get(); } catch (Exception e) { - logEntityAction(emptyId(EntityType.ASSET), - null, - null, - ActionType.DELETED, e, strAssetId); throw handleException(e); } } @@ -218,31 +177,11 @@ public class AssetController extends BaseController { @ApiParam(value = ASSET_ID_PARAM_DESCRIPTION) @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { checkParameter("customerId", strCustomerId); checkParameter(ASSET_ID, strAssetId); - try { - CustomerId customerId = new CustomerId(toUUID(strCustomerId)); - Customer customer = checkCustomerId(customerId, Operation.READ); - - AssetId assetId = new AssetId(toUUID(strAssetId)); - checkAssetId(assetId, Operation.ASSIGN_TO_CUSTOMER); - - Asset savedAsset = checkNotNull(assetService.assignAssetToCustomer(getTenantId(), assetId, customerId)); - - logEntityAction(assetId, savedAsset, - savedAsset.getCustomerId(), - ActionType.ASSIGNED_TO_CUSTOMER, null, strAssetId, strCustomerId, customer.getName()); - - sendEntityAssignToCustomerNotificationMsg(savedAsset.getTenantId(), savedAsset.getId(), - customerId, EdgeEventActionType.ASSIGNED_TO_CUSTOMER); - - return savedAsset; - } catch (Exception e) { - - logEntityAction(emptyId(EntityType.ASSET), null, - null, - ActionType.ASSIGNED_TO_CUSTOMER, e, strAssetId, strCustomerId); - - throw handleException(e); - } + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + Customer customer = checkCustomerId(customerId, Operation.READ); + AssetId assetId = new AssetId(toUUID(strAssetId)); + checkAssetId(assetId, Operation.ASSIGN_TO_CUSTOMER); + return tbAssetService.assignAssetToCustomer(getTenantId(), assetId, customer, getCurrentUser()); } @ApiOperation(value = "Unassign asset from customer (unassignAssetFromCustomer)", @@ -252,33 +191,13 @@ public class AssetController extends BaseController { @ResponseBody public Asset unassignAssetFromCustomer(@ApiParam(value = ASSET_ID_PARAM_DESCRIPTION) @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { checkParameter(ASSET_ID, strAssetId); - try { - AssetId assetId = new AssetId(toUUID(strAssetId)); - Asset asset = checkAssetId(assetId, Operation.UNASSIGN_FROM_CUSTOMER); - if (asset.getCustomerId() == null || asset.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { - throw new IncorrectParameterException("Asset isn't assigned to any customer!"); - } - - Customer customer = checkCustomerId(asset.getCustomerId(), Operation.READ); - - Asset savedAsset = checkNotNull(assetService.unassignAssetFromCustomer(getTenantId(), assetId)); - - logEntityAction(assetId, asset, - asset.getCustomerId(), - ActionType.UNASSIGNED_FROM_CUSTOMER, null, strAssetId, customer.getId().toString(), customer.getName()); - - sendEntityAssignToCustomerNotificationMsg(savedAsset.getTenantId(), savedAsset.getId(), - customer.getId(), EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER); - - return savedAsset; - } catch (Exception e) { - - logEntityAction(emptyId(EntityType.ASSET), null, - null, - ActionType.UNASSIGNED_FROM_CUSTOMER, e, strAssetId); - - throw handleException(e); + AssetId assetId = new AssetId(toUUID(strAssetId)); + Asset asset = checkAssetId(assetId, Operation.UNASSIGN_FROM_CUSTOMER); + if (asset.getCustomerId() == null || asset.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { + throw new IncorrectParameterException("Asset isn't assigned to any customer!"); } + Customer customer = checkCustomerId(asset.getCustomerId(), Operation.READ); + return tbAssetService.unassignAssetToCustomer(getTenantId(), assetId, customer, getCurrentUser()); } @ApiOperation(value = "Make asset publicly available (assignAssetToPublicCustomer)", @@ -290,25 +209,9 @@ public class AssetController extends BaseController { @ResponseBody public Asset assignAssetToPublicCustomer(@ApiParam(value = ASSET_ID_PARAM_DESCRIPTION) @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { checkParameter(ASSET_ID, strAssetId); - try { - AssetId assetId = new AssetId(toUUID(strAssetId)); - Asset asset = checkAssetId(assetId, Operation.ASSIGN_TO_CUSTOMER); - Customer publicCustomer = customerService.findOrCreatePublicCustomer(asset.getTenantId()); - Asset savedAsset = checkNotNull(assetService.assignAssetToCustomer(getTenantId(), assetId, publicCustomer.getId())); - - logEntityAction(assetId, savedAsset, - savedAsset.getCustomerId(), - ActionType.ASSIGNED_TO_CUSTOMER, null, strAssetId, publicCustomer.getId().toString(), publicCustomer.getName()); - - return savedAsset; - } catch (Exception e) { - - logEntityAction(emptyId(EntityType.ASSET), null, - null, - ActionType.ASSIGNED_TO_CUSTOMER, e, strAssetId); - - throw handleException(e); - } + AssetId assetId = new AssetId(toUUID(strAssetId)); + checkAssetId(assetId, Operation.ASSIGN_TO_CUSTOMER); + return tbAssetService.assignAssetToPublicCustomer(getTenantId(), assetId, getCurrentUser()); } @ApiOperation(value = "Get Tenant Assets (getTenantAssets)", @@ -553,30 +456,14 @@ public class AssetController extends BaseController { @ApiParam(value = ASSET_ID_PARAM_DESCRIPTION) @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); checkParameter(ASSET_ID, strAssetId); - try { - EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); - Edge edge = checkEdgeId(edgeId, Operation.READ); - AssetId assetId = new AssetId(toUUID(strAssetId)); - checkAssetId(assetId, Operation.READ); + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.READ); - Asset savedAsset = checkNotNull(assetService.assignAssetToEdge(getTenantId(), assetId, edgeId)); + AssetId assetId = new AssetId(toUUID(strAssetId)); + checkAssetId(assetId, Operation.READ); - logEntityAction(assetId, savedAsset, - savedAsset.getCustomerId(), - ActionType.ASSIGNED_TO_EDGE, null, strAssetId, strEdgeId, edge.getName()); - - sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedAsset.getId(), EdgeEventActionType.ASSIGNED_TO_EDGE); - - return savedAsset; - } catch (Exception e) { - - logEntityAction(emptyId(EntityType.ASSET), null, - null, - ActionType.ASSIGNED_TO_EDGE, e, strAssetId, strEdgeId); - - throw handleException(e); - } + return tbAssetService.assignAssetToEdge(getTenantId(), assetId, edge, getCurrentUser()); } @ApiOperation(value = "Unassign asset from edge (unassignAssetFromEdge)", @@ -593,30 +480,13 @@ public class AssetController extends BaseController { @ApiParam(value = ASSET_ID_PARAM_DESCRIPTION) @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); checkParameter(ASSET_ID, strAssetId); - try { - EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); - Edge edge = checkEdgeId(edgeId, Operation.READ); + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.READ); - AssetId assetId = new AssetId(toUUID(strAssetId)); - Asset asset = checkAssetId(assetId, Operation.READ); - - Asset savedAsset = checkNotNull(assetService.unassignAssetFromEdge(getTenantId(), assetId, edgeId)); - - logEntityAction(assetId, asset, - asset.getCustomerId(), - ActionType.UNASSIGNED_FROM_EDGE, null, strAssetId, strEdgeId, edge.getName()); + AssetId assetId = new AssetId(toUUID(strAssetId)); + Asset asset = checkAssetId(assetId, Operation.READ); - sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedAsset.getId(), EdgeEventActionType.UNASSIGNED_FROM_EDGE); - - return savedAsset; - } catch (Exception e) { - - logEntityAction(emptyId(EntityType.ASSET), null, - null, - ActionType.UNASSIGNED_FROM_EDGE, e, strAssetId, strEdgeId); - - throw handleException(e); - } + return tbAssetService.unassignAssetFromEdge(getTenantId(), asset, edge, getCurrentUser()); } @ApiOperation(value = "Get assets assigned to edge (getEdgeAssets)", @@ -681,7 +551,6 @@ public class AssetController extends BaseController { public BulkImportResult processAssetsBulkImport(@RequestBody BulkImportRequest request) throws Exception { SecurityUser user = getCurrentUser(); return assetBulkImportService.processBulkImport(request, user, importedAssetInfo -> { - onAssetCreatedOrUpdated(importedAssetInfo.getEntity(), importedAssetInfo.isUpdated(), user); }); } diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index 467a883129..09037f8445 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -38,6 +38,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; import org.thingsboard.server.common.data.ClaimRequest; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceInfo; @@ -45,6 +46,7 @@ import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.device.DeviceSearchQuery; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; @@ -171,7 +173,7 @@ public class DeviceController extends BaseController { } else { checkEntity(null, device, Resource.DEVICE); } - return tbDeviceService.save(getCurrentUser(), getTenantId(), device, oldDevice, accessToken); + return tbDeviceService.save(getTenantId(), device, oldDevice, accessToken, getCurrentUser()); } @ApiOperation(value = "Create Device (saveDevice) with credentials ", @@ -189,7 +191,7 @@ public class DeviceController extends BaseController { DeviceCredentials credentials = checkNotNull(deviceAndCredentials.getCredentials()); device.setTenantId(getCurrentUser().getTenantId()); checkEntity(device.getId(), device, Resource.DEVICE); - return tbDeviceService.saveDeviceWithCredentials(getCurrentUser(), getTenantId(), device, credentials); + return tbDeviceService.saveDeviceWithCredentials(getTenantId(), device, credentials, getCurrentUser()); } @@ -202,9 +204,9 @@ public class DeviceController extends BaseController { @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { checkParameter(DEVICE_ID, strDeviceId); DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); - checkDeviceId(deviceId, Operation.DELETE); + Device device = checkDeviceId(deviceId, Operation.DELETE); try { - tbDeviceService.deleteDevice(getCurrentUser(), getTenantId(), deviceId).get(); + tbDeviceService.deleteDevice(device, getCurrentUser()).get(); } catch (Exception e) { throw handleException(e); } @@ -222,10 +224,10 @@ public class DeviceController extends BaseController { checkParameter("customerId", strCustomerId); checkParameter(DEVICE_ID, strDeviceId); CustomerId customerId = new CustomerId(toUUID(strCustomerId)); - checkCustomerId(customerId, Operation.READ); + Customer customer = checkCustomerId(customerId, Operation.READ); DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); checkDeviceId(deviceId, Operation.ASSIGN_TO_CUSTOMER); - return tbDeviceService.assignDeviceToCustomer(getCurrentUser(), getTenantId(), deviceId, customerId); + return tbDeviceService.assignDeviceToCustomer(getTenantId(), deviceId, customer, getCurrentUser()); } @ApiOperation(value = "Unassign device from customer (unassignDeviceFromCustomer)", @@ -243,9 +245,9 @@ public class DeviceController extends BaseController { throw new IncorrectParameterException("Device isn't assigned to any customer!"); } - checkCustomerId(device.getCustomerId(), Operation.READ); + Customer customer = checkCustomerId(device.getCustomerId(), Operation.READ); - return tbDeviceService.unassignDeviceFromCustomer(getCurrentUser(), getTenantId(), deviceId); + return tbDeviceService.unassignDeviceFromCustomer(device, customer, getCurrentUser()); } catch (Exception e) { throw handleException(e); } @@ -263,7 +265,7 @@ public class DeviceController extends BaseController { checkParameter(DEVICE_ID, strDeviceId); DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); checkDeviceId(deviceId, Operation.ASSIGN_TO_CUSTOMER); - return tbDeviceService.assignDeviceToPublicCustomer(getCurrentUser(), getTenantId(), deviceId); + return tbDeviceService.assignDeviceToPublicCustomer(getTenantId(), deviceId, getCurrentUser()); } @ApiOperation(value = "Get Device Credentials (getDeviceCredentialsByDeviceId)", @@ -275,8 +277,8 @@ public class DeviceController extends BaseController { @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { checkParameter(DEVICE_ID, strDeviceId); DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); - checkDeviceId(deviceId, Operation.READ_CREDENTIALS); - return tbDeviceService.getDeviceCredentialsByDeviceId(getCurrentUser(), getTenantId(), deviceId); + Device device = checkDeviceId(deviceId, Operation.READ_CREDENTIALS); + return tbDeviceService.getDeviceCredentialsByDeviceId(device, getCurrentUser()); } @ApiOperation(value = "Update device credentials (updateDeviceCredentials)", notes = "During device creation, platform generates random 'ACCESS_TOKEN' credentials. " + @@ -290,8 +292,8 @@ public class DeviceController extends BaseController { @ApiParam(value = "A JSON value representing the device credentials.") @RequestBody DeviceCredentials deviceCredentials) throws ThingsboardException { checkNotNull(deviceCredentials); - checkDeviceId(deviceCredentials.getDeviceId(), Operation.WRITE_CREDENTIALS); - return tbDeviceService.updateDeviceCredentials(getCurrentUser(), getTenantId(), deviceCredentials); + Device device = checkDeviceId(deviceCredentials.getDeviceId(), Operation.WRITE_CREDENTIALS); + return tbDeviceService.updateDeviceCredentials(device, deviceCredentials, getCurrentUser()); } @ApiOperation(value = "Get Tenant Devices (getTenantDevices)", @@ -646,14 +648,14 @@ public class DeviceController extends BaseController { checkParameter(TENANT_ID, strTenantId); checkParameter(DEVICE_ID, strDeviceId); DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); - checkDeviceId(deviceId, Operation.ASSIGN_TO_TENANT); + Device device = checkDeviceId(deviceId, Operation.ASSIGN_TO_TENANT); TenantId newTenantId = TenantId.fromUUID(toUUID(strTenantId)); Tenant newTenant = tenantService.findTenantById(newTenantId); if (newTenant == null) { throw new ThingsboardException("Could not find the specified Tenant!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } - return tbDeviceService.assignDeviceToTenant(getCurrentUser(), getTenantId(), newTenantId, deviceId); + return tbDeviceService.assignDeviceToTenant(device, newTenant, getCurrentUser()); } @ApiOperation(value = "Assign device to edge (assignDeviceToEdge)", @@ -673,12 +675,12 @@ public class DeviceController extends BaseController { checkParameter(EDGE_ID, strEdgeId); checkParameter(DEVICE_ID, strDeviceId); EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); - checkEdgeId(edgeId, Operation.READ); + Edge edge = checkEdgeId(edgeId, Operation.READ); DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); checkDeviceId(deviceId, Operation.READ); - return tbDeviceService.assignDeviceToEdge(getCurrentUser(), getTenantId(), deviceId, edgeId); + return tbDeviceService.assignDeviceToEdge(getTenantId(), deviceId, edge, getCurrentUser()); } @ApiOperation(value = "Unassign device from edge (unassignDeviceFromEdge)", @@ -698,11 +700,11 @@ public class DeviceController extends BaseController { checkParameter(EDGE_ID, strEdgeId); checkParameter(DEVICE_ID, strDeviceId); EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); - checkEdgeId(edgeId, Operation.READ); + Edge edge = checkEdgeId(edgeId, Operation.READ); DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); - checkDeviceId(deviceId, Operation.READ); - return tbDeviceService.unassignDeviceFromEdge(getCurrentUser(), getTenantId(), deviceId, edgeId); + Device device = checkDeviceId(deviceId, Operation.READ); + return tbDeviceService.unassignDeviceFromEdge(device, edge, getCurrentUser()); } @ApiOperation(value = "Get devices assigned to edge (getEdgeDevices)", diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantController.java b/application/src/main/java/org/thingsboard/server/controller/TenantController.java index 496cd60e81..e41f0381e8 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantController.java @@ -18,8 +18,8 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; +import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; @@ -36,12 +36,10 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.install.InstallScripts; +import org.thingsboard.server.service.entitiy.tenant.TbTenantService; import org.thingsboard.server.service.security.permission.Operation; -import org.thingsboard.server.service.security.permission.Resource; import static org.thingsboard.server.controller.ControllerConstants.HOME_DASHBOARD; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; @@ -63,14 +61,13 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI @TbCoreComponent @RequestMapping("/api") @Slf4j +@AllArgsConstructor public class TenantController extends BaseController { private static final String TENANT_INFO_DESCRIPTION = "The Tenant Info object extends regular Tenant object and includes Tenant Profile name. "; - @Autowired - private InstallScripts installScripts; - @Autowired - private TenantService tenantService; + private final TenantService tenantService; + private final TbTenantService tbTenantService; @ApiOperation(value = "Get Tenant (getTenantById)", notes = "Fetch the Tenant object based on the provided Tenant Id. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @@ -121,27 +118,9 @@ public class TenantController extends BaseController { @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/tenant", method = RequestMethod.POST) @ResponseBody - public Tenant saveTenant( - @ApiParam(value = "A JSON value representing the tenant.") - @RequestBody Tenant tenant) throws ThingsboardException { - try { - boolean newTenant = tenant.getId() == null; - - checkEntity(tenant.getId(), tenant, Resource.TENANT); - - tenant = checkNotNull(tenantService.saveTenant(tenant)); - if (newTenant) { - installScripts.createDefaultRuleChains(tenant.getId()); - installScripts.createDefaultEdgeRuleChains(tenant.getId()); - } - tenantProfileCache.evict(tenant.getId()); - tbClusterService.onTenantChange(tenant, null); - tbClusterService.broadcastEntityStateChangeEvent(tenant.getId(), tenant.getId(), - newTenant ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); - return tenant; - } catch (Exception e) { - throw handleException(e); - } + public Tenant saveTenant(@ApiParam(value = "A JSON value representing the tenant.") + @RequestBody Tenant tenant) throws ThingsboardException { + return tbTenantService.save(tenant); } @ApiOperation(value = "Delete Tenant (deleteTenant)", @@ -149,20 +128,12 @@ public class TenantController extends BaseController { @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/tenant/{tenantId}", method = RequestMethod.DELETE) @ResponseStatus(value = HttpStatus.OK) - public void deleteTenant( - @ApiParam(value = TENANT_ID_PARAM_DESCRIPTION) - @PathVariable(TENANT_ID) String strTenantId) throws ThingsboardException { + public void deleteTenant(@ApiParam(value = TENANT_ID_PARAM_DESCRIPTION) + @PathVariable(TENANT_ID) String strTenantId) throws ThingsboardException { checkParameter(TENANT_ID, strTenantId); - try { - TenantId tenantId = TenantId.fromUUID(toUUID(strTenantId)); - Tenant tenant = checkTenantId(tenantId, Operation.DELETE); - tenantService.deleteTenant(tenantId); - tenantProfileCache.evict(tenantId); - tbClusterService.onTenantDelete(tenant, null); - tbClusterService.broadcastEntityStateChangeEvent(tenantId, tenantId, ComponentLifecycleEvent.DELETED); - } catch (Exception e) { - throw handleException(e); - } + TenantId tenantId = TenantId.fromUUID(toUUID(strTenantId)); + Tenant tenant = checkTenantId(tenantId, Operation.DELETE); + tbTenantService.delete(tenant); } @ApiOperation(value = "Get Tenants (getTenants)", notes = "Returns a page of tenants registered in the platform. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) diff --git a/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java index 11adad4509..e9999d2dc1 100644 --- a/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.asset; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EntityType; @@ -25,6 +26,7 @@ import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.asset.TbAssetService; import org.thingsboard.server.service.importing.AbstractBulkImportService; import org.thingsboard.server.service.importing.BulkImportColumnType; import org.thingsboard.server.service.security.model.SecurityUser; @@ -37,6 +39,7 @@ import java.util.Optional; @RequiredArgsConstructor public class AssetBulkImportService extends AbstractBulkImportService { private final AssetService assetService; + private final TbAssetService tbAssetService; @Override protected void setEntityFields(Asset entity, Map fields) { @@ -61,8 +64,9 @@ public class AssetBulkImportService extends AbstractBulkImportService { } @Override + @SneakyThrows protected Asset saveEntity(SecurityUser user, Asset entity, Map fields) { - return assetService.saveAsset(entity); + return tbAssetService.save(entity, user); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java index 7b8fdc325a..34c30d5225 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java @@ -121,7 +121,7 @@ public class DeviceBulkImportService extends AbstractBulkImportService { } entity.setDeviceProfileId(deviceProfile.getId()); - return tbDeviceService.saveDeviceWithCredentials(user, user.getTenantId(), entity, deviceCredentials); + return tbDeviceService.saveDeviceWithCredentials(user.getTenantId(), entity, deviceCredentials, user); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java index 97c6907740..9e597d729d 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java @@ -41,6 +41,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageDataIterableByTenantIdEntityId; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.alarm.AlarmService; +import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.device.ClaimDevicesService; import org.thingsboard.server.dao.device.DeviceCredentialsService; @@ -49,6 +50,7 @@ import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.service.action.EntityActionService; import org.thingsboard.server.service.executors.DbCallbackExecutorService; @@ -89,6 +91,8 @@ public abstract class AbstractTbEntityService { @Autowired protected DeviceService deviceService; @Autowired + protected AssetService assetService; + @Autowired protected DeviceCredentialsService deviceCredentialsService; @Autowired protected TenantService tenantService; @@ -96,6 +100,8 @@ public abstract class AbstractTbEntityService { protected CustomerService customerService; @Autowired protected ClaimDevicesService claimDevicesService; + @Autowired + protected TbTenantProfileCache tenantProfileCache; protected ListenableFuture removeAlarmsByEntityId(TenantId tenantId, EntityId entityId) { ListenableFuture> alarmsFuture = diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java index ae508f5e3a..7abe44744b 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java @@ -17,38 +17,33 @@ package org.thingsboard.server.service.entitiy; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.thingsboard.rule.engine.api.msg.DeviceCredentialsUpdateNotificationMsg; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.PageDataIterableByTenantIdEntityId; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.service.action.EntityActionService; import org.thingsboard.server.service.gateway_device.GatewayNotificationsService; import org.thingsboard.server.service.security.model.SecurityUser; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; @Slf4j @@ -60,22 +55,48 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS private static final ObjectMapper json = new ObjectMapper(); - @Value("${edges.enabled}") - @Getter - protected boolean edgesEnabled; - private final EntityActionService entityActionService; private final TbClusterService tbClusterService; private final GatewayNotificationsService gatewayNotificationsService; - private final EdgeService edgeService; @Override - public void sendNotification(TenantId tenantId, I entityId, E entity, CustomerId customerId, - ActionType actionType, SecurityUser user, Exception e, - Object... additionalInfo) { + public void notifyEntity(TenantId tenantId, I entityId, E entity, CustomerId customerId, + ActionType actionType, SecurityUser user, Exception e, + Object... additionalInfo) { logEntityAction(tenantId, entityId, entity, customerId, actionType, user, e, additionalInfo); } + @Override + public void notifyDeleteEntity(TenantId tenantId, I entityId, E entity, + CustomerId customerId, List relatedEdgeIds, + SecurityUser user, Object... additionalInfo) { + logEntityAction(tenantId, entityId, entity, customerId, ActionType.DELETED, user, additionalInfo); + sendDeleteNotificationMsg(tenantId, entityId, relatedEdgeIds); + } + + public void notifyAssignOrUnassignEntityToCustomer(TenantId tenantId, I entityId, + CustomerId customerId, E entity, + ActionType actionType, + EdgeEventActionType edgeActionType, + SecurityUser user, boolean sendToEdge, + Object... additionalInfo) { + logEntityAction(tenantId, entityId, entity, customerId, actionType, user, additionalInfo); + + if (sendToEdge) { + sendEntityAssignToCustomerNotificationMsg(tenantId, entityId, customerId, edgeActionType); + } + } + + @Override + public void notifyAssignOrUnassignEntityToEdge(TenantId tenantId, I entityId, + CustomerId customerId, EdgeId edgeId, + E entity, ActionType actionType, + EdgeEventActionType edgeActionType, + SecurityUser user, Object... additionalInfo) { + logEntityAction(tenantId, entityId, entity, customerId, actionType, user, additionalInfo); + sendEntityAssignToEdgeNotificationMsg(tenantId, edgeId, entityId, edgeActionType); + } + //Device @Override public void notifyCreateOrUpdateDevice(TenantId tenantId, DeviceId deviceId, CustomerId customerId, @@ -87,25 +108,11 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS @Override public void notifyDeleteDevice(TenantId tenantId, DeviceId deviceId, CustomerId customerId, Device device, - SecurityUser user, Object... additionalInfo) { + List relatedEdgeIds, SecurityUser user, Object... additionalInfo) { gatewayNotificationsService.onDeviceDeleted(device); tbClusterService.onDeviceDeleted(device, null); - logEntityAction(tenantId, deviceId, device, customerId, ActionType.DELETED, user, additionalInfo); - - List relatedEdgeIds = findRelatedEdgeIds(tenantId, deviceId); - sendDeleteNotificationMsg(tenantId, deviceId, relatedEdgeIds); - } - - @Override - public void notifyAssignOrUnassignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, CustomerId customerId, - Device device, ActionType actionType, EdgeEventActionType edgeActionType, - SecurityUser user, boolean sendToEdge, Object... additionalInfo) { - logEntityAction(tenantId, deviceId, device, customerId, actionType, user, additionalInfo); - - if (sendToEdge) { - sendEntityAssignToCustomerNotificationMsg(tenantId, deviceId, customerId, edgeActionType); - } + notifyDeleteEntity(tenantId, deviceId, device, customerId, relatedEdgeIds, user, additionalInfo); } @Override @@ -123,12 +130,14 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS pushAssignedFromNotification(tenant, newTenantId, device); } - @Override - public void notifyAssignOrUnassignDeviceToEdge(TenantId tenantId, DeviceId deviceId, CustomerId customerId, EdgeId edgeId, - Device device, ActionType actionType, EdgeEventActionType edgeActionType, - SecurityUser user, Object... additionalInfo) { - logEntityAction(tenantId, deviceId, device, customerId, actionType, user, additionalInfo); - sendEntityAssignToEdgeNotificationMsg(tenantId, edgeId, deviceId, edgeActionType); + //Asset + public void notifyCreateOrUpdateAsset(TenantId tenantId, AssetId assetId, CustomerId customerId, Asset asset, + ActionType actionType, SecurityUser user, Object... additionalInfo) { + logEntityAction(tenantId, assetId, asset, customerId, actionType, user, additionalInfo); + + if (actionType == ActionType.UPDATED) { + sendEntityNotificationMsg(asset.getTenantId(), asset.getId(), EdgeEventActionType.UPDATED); + } } private void logEntityAction(TenantId tenantId, I entityId, E entity, CustomerId customerId, @@ -177,22 +186,6 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS tbClusterService.sendNotificationMsgToEdgeService(tenantId, edgeId, entityId, body, type, action); } - private List findRelatedEdgeIds(TenantId tenantId, EntityId entityId) { - if (!edgesEnabled) { - return null; - } - if (EntityType.EDGE.equals(entityId.getEntityType())) { - return Collections.singletonList(new EdgeId(entityId.getId())); - } - PageDataIterableByTenantIdEntityId relatedEdgeIdsIterator = - new PageDataIterableByTenantIdEntityId<>(edgeService::findRelatedEdgeIdsByEntityId, tenantId, entityId, DEFAULT_PAGE_SIZE); - List result = new ArrayList<>(); - for (EdgeId edgeId : relatedEdgeIdsIterator) { - result.add(edgeId); - } - return result; - } - private void pushAssignedFromNotification(Tenant currentTenant, TenantId newTenantId, Device assignedDevice) { String data = entityToStr(assignedDevice); if (data != null) { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java index 4cd56f47fb..d4b2f86a99 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java @@ -18,8 +18,10 @@ package org.thingsboard.server.service.entitiy; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; @@ -28,21 +30,36 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.service.security.model.SecurityUser; +import java.util.List; + public interface TbNotificationEntityService { - void sendNotification(TenantId tenantId, I entityId, E entity, CustomerId customerId, - ActionType actionType, SecurityUser user, Exception e, - Object... additionalInfo); + void notifyEntity(TenantId tenantId, I entityId, E entity, CustomerId customerId, + ActionType actionType, SecurityUser user, Exception e, + Object... additionalInfo); + + void notifyDeleteEntity(TenantId tenantId, I entityId, E entity, CustomerId customerId, + List relatedEdgeIds, SecurityUser user, + Object... additionalInfo); + + void notifyAssignOrUnassignEntityToCustomer(TenantId tenantId, I entityId, + CustomerId customerId, E entity, + ActionType actionType, + EdgeEventActionType edgeActionType, + SecurityUser user, boolean sendToEdge, + Object... additionalInfo); + + void notifyAssignOrUnassignEntityToEdge(TenantId tenantId, I entityId, + CustomerId customerId, EdgeId edgeId, + E entity, ActionType actionType, + EdgeEventActionType edgeActionType, + SecurityUser user, Object... additionalInfo); void notifyCreateOrUpdateDevice(TenantId tenantId, DeviceId deviceId, CustomerId customerId, Device device, Device oldDevice, ActionType actionType, SecurityUser user, Object... additionalInfo); void notifyDeleteDevice(TenantId tenantId, DeviceId deviceId, CustomerId customerId, Device device, - SecurityUser user, Object... additionalInfo); - - void notifyAssignOrUnassignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, CustomerId customerId, - Device device, ActionType actionType, EdgeEventActionType edgeActionType, - SecurityUser user, boolean sendToEdge, Object... additionalInfo); + List relatedEdgeIds, SecurityUser user, Object... additionalInfo); void notifyUpdateDeviceCredentials(TenantId tenantId, DeviceId deviceId, CustomerId customerId, Device device, DeviceCredentials deviceCredentials, SecurityUser user); @@ -50,7 +67,7 @@ public interface TbNotificationEntityService { void notifyAssignDeviceToTenant(TenantId tenantId, TenantId newTenantId, DeviceId deviceId, CustomerId customerId, Device device, Tenant tenant, SecurityUser user, Object... additionalInfo); - void notifyAssignOrUnassignDeviceToEdge(TenantId tenantId, DeviceId deviceId, CustomerId customerId, EdgeId edgeId, - Device device, ActionType actionType, EdgeEventActionType edgeActionType, - SecurityUser user, Object... additionalInfo); + void notifyCreateOrUpdateAsset(TenantId tenantId, AssetId assetId, CustomerId customerId, Asset asset, + ActionType actionType, SecurityUser user, Object... additionalInfo); + } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java new file mode 100644 index 0000000000..84b9c707d0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java @@ -0,0 +1,163 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.entitiy.asset; + +import com.google.common.util.concurrent.ListenableFuture; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.AbstractTbEntityService; +import org.thingsboard.server.service.security.model.SecurityUser; + +import java.util.List; + +@Service +@TbCoreComponent +@AllArgsConstructor +public class DefaultTbAssetService extends AbstractTbEntityService implements TbAssetService { + @Override + public Asset save(Asset asset, SecurityUser user) throws ThingsboardException { + ActionType actionType = asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED; + TenantId tenantId = asset.getTenantId(); + try { + Asset savedAsset = checkNotNull(assetService.saveAsset(asset)); + notificationEntityService.notifyCreateOrUpdateAsset(tenantId, savedAsset.getId(), savedAsset.getCustomerId(), asset, actionType, user); + return savedAsset; + } catch (Exception e) { + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.ASSET), asset, null, actionType, user, e); + throw handleException(e); + } + } + + @Override + public ListenableFuture delete(Asset asset, SecurityUser user) throws ThingsboardException { + TenantId tenantId = asset.getTenantId(); + AssetId assetId = asset.getId(); + try { + List relatedEdgeIds = findRelatedEdgeIds(tenantId, assetId); + assetService.deleteAsset(tenantId, assetId); + notificationEntityService.notifyDeleteEntity(tenantId, assetId, asset, asset.getCustomerId(), relatedEdgeIds, user, asset.toString()); + + return removeAlarmsByEntityId(tenantId, assetId); + } catch (Exception e) { + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.ASSET), null, null, + ActionType.DELETED, user, e, asset.toString()); + throw handleException(e); + } + } + + @Override + public Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, Customer customer, SecurityUser user) throws ThingsboardException { + ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER; + CustomerId customerId = customer.getId(); + try { + Asset savedAsset = checkNotNull(assetService.assignAssetToCustomer(tenantId, assetId, customerId)); + notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, assetId, customerId, savedAsset, + actionType, EdgeEventActionType.ASSIGNED_TO_CUSTOMER, user, true, customerId.toString(), customer.getName()); + + return savedAsset; + } catch (Exception e) { + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.ASSET), null, null, + actionType, user, e, assetId.toString(), customerId.toString()); + + throw handleException(e); + } + } + + @Override + public Asset unassignAssetToCustomer(TenantId tenantId, AssetId assetId, Customer customer, SecurityUser user) throws ThingsboardException { + ActionType actionType = ActionType.UNASSIGNED_FROM_CUSTOMER; + try { + Asset savedAsset = checkNotNull(assetService.unassignAssetFromCustomer(tenantId, assetId)); + CustomerId customerId = customer.getId(); + + notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, assetId, customerId, savedAsset, + actionType, EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER, user, + true, customerId.toString(), customer.getName()); + + return savedAsset; + } catch (Exception e) { + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.ASSET), null, null, + actionType, user, e, assetId.toString()); + throw handleException(e); + } + } + + @Override + public Asset assignAssetToPublicCustomer(TenantId tenantId, AssetId assetId, SecurityUser user) throws ThingsboardException { + ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER; + try { + Customer publicCustomer = customerService.findOrCreatePublicCustomer(tenantId); + Asset savedAsset = checkNotNull(assetService.assignAssetToCustomer(tenantId, assetId, publicCustomer.getId())); + + notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, assetId, savedAsset.getCustomerId(), savedAsset, + actionType, null, user, false, actionType.toString(), + publicCustomer.getId().toString(), publicCustomer.getName()); + + return savedAsset; + } catch (Exception e) { + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.ASSET), null, null, + actionType, user, e, assetId.toString()); + throw handleException(e); + } + } + + @Override + public Asset assignAssetToEdge(TenantId tenantId, AssetId assetId, Edge edge, SecurityUser user) throws ThingsboardException { + ActionType actionType = ActionType.ASSIGNED_TO_EDGE; + EdgeId edgeId = edge.getId(); + try { + Asset savedAsset = checkNotNull(assetService.assignAssetToEdge(tenantId, assetId, edgeId)); + notificationEntityService.notifyAssignOrUnassignEntityToEdge(tenantId, assetId, savedAsset.getCustomerId(), + edgeId, savedAsset, actionType, EdgeEventActionType.ASSIGNED_TO_EDGE, user, assetId.toString(), edgeId.toString(), edge.getName()); + + return savedAsset; + } catch (Exception e) { + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.ASSET), null, null, + actionType, user, e, assetId.toString(), edgeId.toString()); + throw handleException(e); + } + } + + @Override + public Asset unassignAssetFromEdge(TenantId tenantId, Asset asset, Edge edge, SecurityUser user) throws ThingsboardException { + ActionType actionType = ActionType.UNASSIGNED_FROM_EDGE; + AssetId assetId = asset.getId(); + EdgeId edgeId = edge.getId(); + try { + Asset savedAsset = checkNotNull(assetService.unassignAssetFromEdge(tenantId, assetId, edgeId)); + + notificationEntityService.notifyAssignOrUnassignEntityToEdge(tenantId, assetId, asset.getCustomerId(), + edgeId, asset, actionType, EdgeEventActionType.UNASSIGNED_FROM_EDGE, user, assetId.toString(), edgeId.toString(), edge.getName()); + return savedAsset; + } catch (Exception e) { + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.ASSET), null, null, + actionType, user, e, assetId.toString(), edgeId.toString()); + throw handleException(e); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java new file mode 100644 index 0000000000..c6e1c29940 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.entitiy.asset; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.service.security.model.SecurityUser; + +public interface TbAssetService { + Asset save(Asset asset, SecurityUser user) throws ThingsboardException; + + ListenableFuture delete(Asset asset, SecurityUser user) throws ThingsboardException; + + Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, Customer customer, SecurityUser user) throws ThingsboardException; + + Asset unassignAssetToCustomer(TenantId tenantId, AssetId assetId, Customer customer, SecurityUser user) throws ThingsboardException; + + Asset assignAssetToPublicCustomer(TenantId tenantId, AssetId assetId, SecurityUser user) throws ThingsboardException; + + Asset assignAssetToEdge(TenantId tenantId, AssetId assetId, Edge edge, SecurityUser user) throws ThingsboardException; + + Asset unassignAssetFromEdge(TenantId tenantId, Asset asset, Edge edge, SecurityUser user) throws ThingsboardException; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java index 6e13b89358..1bed17f941 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java @@ -41,6 +41,8 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import org.thingsboard.server.service.security.model.SecurityUser; +import java.util.List; + @AllArgsConstructor @TbCoreComponent @Service @@ -48,7 +50,7 @@ import org.thingsboard.server.service.security.model.SecurityUser; public class DefaultTbDeviceService extends AbstractTbEntityService implements TbDeviceService { @Override - public Device save(SecurityUser user, TenantId tenantId, Device device, Device oldDevice, String accessToken) throws ThingsboardException { + public Device save(TenantId tenantId, Device device, Device oldDevice, String accessToken, SecurityUser user) throws ThingsboardException { ActionType actionType = device.getId() == null ? ActionType.ADDED : ActionType.UPDATED; try { Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken)); @@ -57,13 +59,13 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T return savedDevice; } catch (Exception e) { - notificationEntityService.sendNotification(tenantId, emptyId(EntityType.DEVICE), device, null, actionType, user, e); + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DEVICE), device, null, actionType, user, e); throw handleException(e); } } @Override - public Device saveDeviceWithCredentials(SecurityUser user, TenantId tenantId, Device device, DeviceCredentials credentials) throws ThingsboardException { + public Device saveDeviceWithCredentials(TenantId tenantId, Device device, DeviceCredentials credentials, SecurityUser user) throws ThingsboardException { ActionType actionType = device.getId() == null ? ActionType.ADDED : ActionType.UPDATED; try { Device savedDevice = checkNotNull(deviceService.saveDeviceWithCredentials(device, credentials)); @@ -72,110 +74,113 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T return savedDevice; } catch (Exception e) { - notificationEntityService.sendNotification(tenantId, emptyId(EntityType.DEVICE), device, null, actionType, user, e); + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DEVICE), device, null, actionType, user, e); throw handleException(e); } } @Override - public ListenableFuture deleteDevice(SecurityUser user, TenantId tenantId, DeviceId deviceId) throws ThingsboardException { + public ListenableFuture deleteDevice(Device device, SecurityUser user) throws ThingsboardException { + TenantId tenantId = device.getTenantId(); + DeviceId deviceId = device.getId(); try { - Device device = deviceService.findDeviceById(tenantId, deviceId); + List relatedEdgeIds = findRelatedEdgeIds(tenantId, deviceId); deviceService.deleteDevice(tenantId, deviceId); - notificationEntityService.notifyDeleteDevice(tenantId, deviceId, device.getCustomerId(), device, user, deviceId.toString()); + notificationEntityService.notifyDeleteDevice(tenantId, deviceId, device.getCustomerId(), device, + relatedEdgeIds, user, deviceId.toString()); + return removeAlarmsByEntityId(tenantId, deviceId); } catch (Exception e) { - notificationEntityService.sendNotification(tenantId, emptyId(EntityType.DEVICE), null, null, + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DEVICE), null, null, ActionType.DELETED, user, e, deviceId.toString()); throw handleException(e); } } @Override - public Device assignDeviceToCustomer(SecurityUser user, TenantId tenantId, DeviceId deviceId, CustomerId customerId) throws ThingsboardException { + public Device assignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, Customer customer, SecurityUser user) throws ThingsboardException { ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER; + CustomerId customerId = customer.getId(); try { Device savedDevice = checkNotNull(deviceService.assignDeviceToCustomer(user.getTenantId(), deviceId, customerId)); - - Customer customer = customerService.findCustomerById(user.getTenantId(), customerId); - - notificationEntityService.notifyAssignOrUnassignDeviceToCustomer(tenantId, deviceId, customerId, savedDevice, + notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, deviceId, customerId, savedDevice, actionType, EdgeEventActionType.ASSIGNED_TO_CUSTOMER, user, true, customerId.toString(), customer.getName()); return savedDevice; } catch (Exception e) { - notificationEntityService.sendNotification(tenantId, emptyId(EntityType.DEVICE), null, null, + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DEVICE), null, null, actionType, user, e, deviceId.toString(), customerId.toString()); throw handleException(e); } } @Override - public Device unassignDeviceFromCustomer(SecurityUser user, TenantId tenantId, DeviceId deviceId) throws ThingsboardException { + public Device unassignDeviceFromCustomer(Device device, Customer customer, SecurityUser user) throws ThingsboardException { ActionType actionType = ActionType.UNASSIGNED_FROM_CUSTOMER; + TenantId tenantId = device.getTenantId(); + DeviceId deviceId = device.getId(); try { - Device device = deviceService.findDeviceById(tenantId, deviceId); - Customer customer = customerService.findCustomerById(tenantId, device.getCustomerId()); Device savedDevice = checkNotNull(deviceService.unassignDeviceFromCustomer(tenantId, deviceId)); CustomerId customerId = customer.getId(); - notificationEntityService.notifyAssignOrUnassignDeviceToCustomer(tenantId, deviceId, customerId, savedDevice, + notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, deviceId, customerId, savedDevice, actionType, EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER, user, true, customerId.toString(), customer.getName()); return savedDevice; } catch (Exception e) { - notificationEntityService.sendNotification(tenantId, emptyId(EntityType.DEVICE), null, null, - actionType, user, e, false, deviceId.toString()); + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DEVICE), null, null, + actionType, user, e, deviceId.toString()); throw handleException(e); } } @Override - public Device assignDeviceToPublicCustomer(SecurityUser user, TenantId tenantId, DeviceId deviceId) throws ThingsboardException { + public Device assignDeviceToPublicCustomer(TenantId tenantId, DeviceId deviceId, SecurityUser user) throws ThingsboardException { ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER; try { Customer publicCustomer = customerService.findOrCreatePublicCustomer(tenantId); Device savedDevice = checkNotNull(deviceService.assignDeviceToCustomer(tenantId, deviceId, publicCustomer.getId())); - notificationEntityService.notifyAssignOrUnassignDeviceToCustomer(tenantId, deviceId, savedDevice.getCustomerId(), savedDevice, + notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, deviceId, savedDevice.getCustomerId(), savedDevice, actionType, null, user, false, deviceId.toString(), publicCustomer.getId().toString(), publicCustomer.getName()); return savedDevice; } catch (Exception e) { - notificationEntityService.sendNotification(tenantId, emptyId(EntityType.DEVICE), null, null, - actionType, user, e, false, deviceId.toString()); + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DEVICE), null, null, + actionType, user, e, deviceId.toString()); throw handleException(e); } } @Override - public DeviceCredentials getDeviceCredentialsByDeviceId(SecurityUser user, TenantId tenantId, DeviceId deviceId) throws ThingsboardException { + public DeviceCredentials getDeviceCredentialsByDeviceId(Device device, SecurityUser user) throws ThingsboardException { ActionType actionType = ActionType.CREDENTIALS_READ; + TenantId tenantId = device.getTenantId(); + DeviceId deviceId = device.getId(); try { - Device device = deviceService.findDeviceById(tenantId, deviceId); DeviceCredentials deviceCredentials = checkNotNull(deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, deviceId)); - notificationEntityService.sendNotification(tenantId, deviceId, device, device.getCustomerId(), + notificationEntityService.notifyEntity(tenantId, deviceId, device, device.getCustomerId(), actionType, user, null, deviceId.toString()); return deviceCredentials; } catch (Exception e) { - notificationEntityService.sendNotification(tenantId, emptyId(EntityType.DEVICE), null, null, + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DEVICE), null, null, actionType, user, e, deviceId.toString()); throw handleException(e); } } @Override - public DeviceCredentials updateDeviceCredentials(SecurityUser user, TenantId tenantId, DeviceCredentials deviceCredentials) throws ThingsboardException { + public DeviceCredentials updateDeviceCredentials(Device device, DeviceCredentials deviceCredentials, SecurityUser user) throws ThingsboardException { + TenantId tenantId = device.getTenantId(); + DeviceId deviceId = device.getId(); try { - DeviceId deviceId = deviceCredentials.getDeviceId(); - Device device = deviceService.findDeviceById(tenantId, deviceId); DeviceCredentials result = checkNotNull(deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials)); notificationEntityService.notifyUpdateDeviceCredentials(tenantId, deviceId, device.getCustomerId(), device, result, user); return result; } catch (Exception e) { - notificationEntityService.sendNotification(tenantId, emptyId(EntityType.DEVICE), null, null, + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DEVICE), null, null, ActionType.CREDENTIALS_UPDATED, user, e, deviceCredentials); throw handleException(e); } @@ -188,7 +193,7 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T return Futures.transform(future, result -> { if (result != null && result.getResponse().equals(ClaimResponse.SUCCESS)) { - notificationEntityService.sendNotification(tenantId, device.getId(), result.getDevice(), customerId, + notificationEntityService.notifyEntity(tenantId, device.getId(), result.getDevice(), customerId, ActionType.ASSIGNED_TO_CUSTOMER, user, null, device.getId().toString(), customerId.toString(), customerService.findCustomerById(tenantId, customerId).getName()); } @@ -207,7 +212,7 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T return Futures.transform(future, result -> { Customer unassignedCustomer = result.getUnassignedCustomer(); if (unassignedCustomer != null) { - notificationEntityService.sendNotification(tenantId, device.getId(), device, device.getCustomerId(), ActionType.UNASSIGNED_FROM_CUSTOMER, user, null, + notificationEntityService.notifyEntity(tenantId, device.getId(), device, device.getCustomerId(), ActionType.UNASSIGNED_FROM_CUSTOMER, user, null, device.getId().toString(), unassignedCustomer.getId().toString(), unassignedCustomer.getName()); } return result; @@ -218,53 +223,54 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T } @Override - public Device assignDeviceToTenant(SecurityUser user, TenantId tenantId, TenantId newTenantId, DeviceId deviceId) throws ThingsboardException { + public Device assignDeviceToTenant(Device device, Tenant newTenant, SecurityUser user) throws ThingsboardException { + TenantId tenantId = device.getTenantId(); + TenantId newTenantId = newTenant.getId(); try { Tenant tenant = tenantService.findTenantById(tenantId); - Tenant newTenant = tenantService.findTenantById(newTenantId); - Device device = deviceService.findDeviceById(tenantId, deviceId); Device assignedDevice = deviceService.assignDeviceToTenant(newTenantId, device); - notificationEntityService.notifyAssignDeviceToTenant(tenantId, newTenantId, deviceId, + notificationEntityService.notifyAssignDeviceToTenant(tenantId, newTenantId, device.getId(), assignedDevice.getCustomerId(), assignedDevice, tenant, user, newTenantId.toString(), newTenant.getName()); return assignedDevice; } catch (Exception e) { - notificationEntityService.sendNotification(tenantId, emptyId(EntityType.DEVICE), null, null, + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DEVICE), null, null, ActionType.ASSIGNED_TO_TENANT, user, e, newTenantId.toString()); throw handleException(e); } } @Override - public Device assignDeviceToEdge(SecurityUser user, TenantId tenantId, DeviceId deviceId, EdgeId edgeId) throws ThingsboardException { + public Device assignDeviceToEdge(TenantId tenantId, DeviceId deviceId, Edge edge, SecurityUser user) throws ThingsboardException { ActionType actionType = ActionType.ASSIGNED_TO_EDGE; + EdgeId edgeId = edge.getId(); try { Device savedDevice = checkNotNull(deviceService.assignDeviceToEdge(tenantId, deviceId, edgeId)); - Edge edge = edgeService.findEdgeById(tenantId, edgeId); - notificationEntityService.notifyAssignOrUnassignDeviceToEdge(tenantId, deviceId, savedDevice.getCustomerId(), + notificationEntityService.notifyAssignOrUnassignEntityToEdge(tenantId, deviceId, savedDevice.getCustomerId(), edgeId, savedDevice, actionType, EdgeEventActionType.ASSIGNED_TO_EDGE, user, deviceId.toString(), edgeId.toString(), edge.getName()); return savedDevice; } catch (Exception e) { - notificationEntityService.sendNotification(tenantId, emptyId(EntityType.DEVICE), null, null, + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DEVICE), null, null, actionType, user, e, deviceId.toString(), edgeId.toString()); throw handleException(e); } } @Override - public Device unassignDeviceFromEdge(SecurityUser user, TenantId tenantId, DeviceId deviceId, EdgeId edgeId) throws ThingsboardException { + public Device unassignDeviceFromEdge(Device device, Edge edge, SecurityUser user) throws ThingsboardException { ActionType actionType = ActionType.UNASSIGNED_FROM_EDGE; + TenantId tenantId = device.getTenantId(); + DeviceId deviceId = device.getId(); + EdgeId edgeId = edge.getId(); try { - Device device = deviceService.findDeviceById(tenantId, deviceId); Device savedDevice = checkNotNull(deviceService.unassignDeviceFromEdge(tenantId, deviceId, edgeId)); - Edge edge = edgeService.findEdgeById(tenantId, edgeId); - notificationEntityService.notifyAssignOrUnassignDeviceToEdge(tenantId, deviceId, device.getCustomerId(), + notificationEntityService.notifyAssignOrUnassignEntityToEdge(tenantId, deviceId, device.getCustomerId(), edgeId, device, actionType, EdgeEventActionType.UNASSIGNED_FROM_EDGE, user, deviceId.toString(), edgeId.toString(), edge.getName()); return savedDevice; } catch (Exception e) { - notificationEntityService.sendNotification(tenantId, emptyId(EntityType.DEVICE), null, null, + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DEVICE), null, null, actionType, user, e, deviceId.toString(), edgeId.toString()); throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java index 52fbf1da8a..16fd0c7448 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java @@ -16,11 +16,13 @@ package org.thingsboard.server.service.entitiy.device; import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.dao.device.claim.ClaimResult; @@ -29,29 +31,29 @@ import org.thingsboard.server.service.security.model.SecurityUser; public interface TbDeviceService { - Device save(SecurityUser user, TenantId tenantId, Device device, Device oldDevice, String accessToken) throws ThingsboardException; + Device save(TenantId tenantId, Device device, Device oldDevice, String accessToken, SecurityUser user) throws ThingsboardException; - Device saveDeviceWithCredentials(SecurityUser user, TenantId tenantId, Device device, DeviceCredentials deviceCredentials) throws ThingsboardException; + Device saveDeviceWithCredentials(TenantId tenantId, Device device, DeviceCredentials deviceCredentials, SecurityUser user) throws ThingsboardException; - ListenableFuture deleteDevice(SecurityUser user, TenantId tenantId, DeviceId deviceId) throws ThingsboardException; + ListenableFuture deleteDevice(Device device, SecurityUser user) throws ThingsboardException; - Device assignDeviceToCustomer(SecurityUser user, TenantId tenantId, DeviceId deviceId, CustomerId customerId) throws ThingsboardException; + Device assignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, Customer customer, SecurityUser user) throws ThingsboardException; - Device unassignDeviceFromCustomer(SecurityUser user, TenantId tenantId, DeviceId deviceId) throws ThingsboardException; + Device unassignDeviceFromCustomer(Device device, Customer customer, SecurityUser user) throws ThingsboardException; - Device assignDeviceToPublicCustomer(SecurityUser user, TenantId tenantId, DeviceId deviceId) throws ThingsboardException; + Device assignDeviceToPublicCustomer(TenantId tenantId, DeviceId deviceId, SecurityUser user) throws ThingsboardException; - DeviceCredentials getDeviceCredentialsByDeviceId(SecurityUser user, TenantId tenantId, DeviceId deviceId) throws ThingsboardException; + DeviceCredentials getDeviceCredentialsByDeviceId(Device device, SecurityUser user) throws ThingsboardException; - DeviceCredentials updateDeviceCredentials(SecurityUser user, TenantId tenantId, DeviceCredentials deviceCredentials) throws ThingsboardException; + DeviceCredentials updateDeviceCredentials(Device device, DeviceCredentials deviceCredentials, SecurityUser user) throws ThingsboardException; ListenableFuture claimDevice(TenantId tenantId, Device device, CustomerId customerId, String secretKey, SecurityUser user) throws ThingsboardException; ListenableFuture reclaimDevice(TenantId tenantId, Device device, SecurityUser user) throws ThingsboardException; - Device assignDeviceToTenant(SecurityUser user, TenantId tenantId, TenantId newTenantId, DeviceId deviceId) throws ThingsboardException; + Device assignDeviceToTenant(Device device, Tenant newTenant, SecurityUser user) throws ThingsboardException; - Device assignDeviceToEdge(SecurityUser user, TenantId tenantId, DeviceId deviceId, EdgeId edgeId) throws ThingsboardException; + Device assignDeviceToEdge(TenantId tenantId, DeviceId deviceId, Edge edge, SecurityUser user) throws ThingsboardException; - Device unassignDeviceFromEdge(SecurityUser user, TenantId tenantId, DeviceId deviceId, EdgeId edgeId) throws ThingsboardException; + Device unassignDeviceFromEdge(Device device, Edge edge, SecurityUser user) throws ThingsboardException; } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java new file mode 100644 index 0000000000..6e7fa9ea2e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java @@ -0,0 +1,68 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.entitiy.tenant; + +import lombok.AllArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.AbstractTbEntityService; +import org.thingsboard.server.service.install.InstallScripts; + +@Service +@TbCoreComponent +@AllArgsConstructor +public class DefaultTbTenantService extends AbstractTbEntityService implements TbTenantService { + + @Autowired + private InstallScripts installScripts; + + @Override + public Tenant save(Tenant tenant) throws ThingsboardException { + try { + boolean newTenant = tenant.getId() == null; + Tenant savedTenant = checkNotNull(tenantService.saveTenant(tenant)); + if (newTenant) { + installScripts.createDefaultRuleChains(savedTenant.getId()); + installScripts.createDefaultEdgeRuleChains(savedTenant.getId()); + } + tenantProfileCache.evict(savedTenant.getId()); + tbClusterService.onTenantChange(savedTenant, null); + tbClusterService.broadcastEntityStateChangeEvent(savedTenant.getId(), savedTenant.getId(), + newTenant ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); + return savedTenant; + } catch (Exception e) { + throw handleException(e); + } + } + + @Override + public void delete(Tenant tenant) throws ThingsboardException { + try { + TenantId tenantId = tenant.getId(); + tenantService.deleteTenant(tenantId); + tenantProfileCache.evict(tenantId); + tbClusterService.onTenantDelete(tenant, null); + tbClusterService.broadcastEntityStateChangeEvent(tenantId, tenantId, ComponentLifecycleEvent.DELETED); + } catch (Exception e) { + throw handleException(e); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/TbTenantService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/TbTenantService.java new file mode 100644 index 0000000000..0ff7bc749c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/TbTenantService.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.entitiy.tenant; + +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.exception.ThingsboardException; + +public interface TbTenantService { + Tenant save(Tenant tenant) throws ThingsboardException; + + void delete(Tenant tenant) throws ThingsboardException; +} From 1bd4e6e3bf2de4b74cda74bf318e09095cb7329b Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Fri, 29 Apr 2022 12:51:30 +0200 Subject: [PATCH 069/276] added TbEdgeService --- .../server/controller/AssetController.java | 3 +- .../server/controller/DeviceController.java | 3 +- .../server/controller/EdgeController.java | 206 ++++-------------- .../service/edge/EdgeBulkImportService.java | 10 +- .../entitiy/AbstractTbEntityService.java | 6 + .../DefaultTbNotificationEntityService.java | 55 ++++- .../entitiy/TbNotificationEntityService.java | 3 + .../entitiy/edge/DefaultTbEdgeService.java | 150 +++++++++++++ .../service/entitiy/edge/TbEdgeService.java | 39 ++++ .../importing/AbstractBulkImportService.java | 5 +- 10 files changed, 297 insertions(+), 183 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/entitiy/edge/TbEdgeService.java diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java index f5654f601d..4718feb774 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java @@ -550,8 +550,7 @@ public class AssetController extends BaseController { @PostMapping("/asset/bulk_import") public BulkImportResult processAssetsBulkImport(@RequestBody BulkImportRequest request) throws Exception { SecurityUser user = getCurrentUser(); - return assetBulkImportService.processBulkImport(request, user, importedAssetInfo -> { - }); + return assetBulkImportService.processBulkImport(request, user); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index 09037f8445..119522a03c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -794,8 +794,7 @@ public class DeviceController extends BaseController { public BulkImportResult processDevicesBulkImport(@RequestBody BulkImportRequest request) throws Exception { SecurityUser user = getCurrentUser(); - return deviceBulkImportService.processBulkImport(request, user, importedDeviceInfo -> { - }); + return deviceBulkImportService.processBulkImport(request, user); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java index f3d73f13e5..8a01e9ef02 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java @@ -34,10 +34,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntitySubtype; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeInfo; import org.thingsboard.server.common.data.edge.EdgeSearchQuery; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; @@ -48,20 +45,19 @@ import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeBulkImportService; +import org.thingsboard.server.service.entitiy.edge.TbEdgeService; import org.thingsboard.server.service.importing.BulkImportRequest; import org.thingsboard.server.service.importing.BulkImportResult; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; -import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -90,6 +86,7 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI @RequiredArgsConstructor public class EdgeController extends BaseController { private final EdgeBulkImportService edgeBulkImportService; + private final TbEdgeService tbEdgeService; public static final String EDGE_ID = "edgeId"; public static final String EDGE_SECURITY_CHECK = "If the user has the authority of 'Tenant Administrator', the server checks that the edge is owned by the same tenant. " + @@ -144,84 +141,43 @@ public class EdgeController extends BaseController { "Specify existing Edge id to update the edge. " + "Referencing non-existing Edge Id will cause 'Not Found' error." + "\n\nEdge name is unique in the scope of tenant. Use unique identifiers like MAC or IMEI for the edge names and non-unique 'label' field for user-friendly visualization purposes." - + TENANT_AUTHORITY_PARAGRAPH, + + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/edge", method = RequestMethod.POST) @ResponseBody public Edge saveEdge(@ApiParam(value = "A JSON value representing the edge.", required = true) @RequestBody Edge edge) throws ThingsboardException { - try { - TenantId tenantId = getCurrentUser().getTenantId(); - edge.setTenantId(tenantId); - boolean created = edge.getId() == null; - - RuleChain edgeTemplateRootRuleChain = null; - if (created) { - edgeTemplateRootRuleChain = ruleChainService.getEdgeTemplateRootRuleChain(tenantId); - if (edgeTemplateRootRuleChain == null) { - throw new DataValidationException("Root edge rule chain is not available!"); - } + TenantId tenantId = getCurrentUser().getTenantId(); + edge.setTenantId(tenantId); + boolean created = edge.getId() == null; + + RuleChain edgeTemplateRootRuleChain = null; + if (created) { + edgeTemplateRootRuleChain = ruleChainService.getEdgeTemplateRootRuleChain(tenantId); + if (edgeTemplateRootRuleChain == null) { + throw new DataValidationException("Root edge rule chain is not available!"); } - - Operation operation = created ? Operation.CREATE : Operation.WRITE; - - accessControlService.checkPermission(getCurrentUser(), Resource.EDGE, operation, - edge.getId(), edge); - - Edge savedEdge = checkNotNull(edgeService.saveEdge(edge)); - onEdgeCreatedOrUpdated(tenantId, savedEdge, edgeTemplateRootRuleChain, !created, getCurrentUser()); - - return savedEdge; - } catch (Exception e) { - logEntityAction(emptyId(EntityType.EDGE), edge, - null, edge.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e); - throw handleException(e); } - } - private void onEdgeCreatedOrUpdated(TenantId tenantId, Edge edge, RuleChain edgeTemplateRootRuleChain, boolean updated, SecurityUser user) throws IOException, ThingsboardException { - if (!updated) { - ruleChainService.assignRuleChainToEdge(tenantId, edgeTemplateRootRuleChain.getId(), edge.getId()); - edgeNotificationService.setEdgeRootRuleChain(tenantId, edge, edgeTemplateRootRuleChain.getId()); - edgeService.assignDefaultRuleChainsToEdge(tenantId, edge.getId()); - } + Operation operation = created ? Operation.CREATE : Operation.WRITE; - tbClusterService.broadcastEntityStateChangeEvent(edge.getTenantId(), edge.getId(), - updated ? ComponentLifecycleEvent.UPDATED : ComponentLifecycleEvent.CREATED); + accessControlService.checkPermission(getCurrentUser(), Resource.EDGE, operation, edge.getId(), edge); - logEntityAction(user, edge.getId(), edge, null, updated ? ActionType.UPDATED : ActionType.ADDED, null); + return tbEdgeService.saveEdge(edge, edgeTemplateRootRuleChain, getCurrentUser()); } @ApiOperation(value = "Delete edge (deleteEdge)", - notes = "Deletes the edge. Referencing non-existing edge Id will cause an error."+ TENANT_AUTHORITY_PARAGRAPH) + notes = "Deletes the edge. Referencing non-existing edge Id will cause an error." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/edge/{edgeId}", method = RequestMethod.DELETE) @ResponseStatus(value = HttpStatus.OK) public void deleteEdge(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); - try { - EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); - Edge edge = checkEdgeId(edgeId, Operation.DELETE); - edgeService.deleteEdge(getTenantId(), edgeId); - - tbClusterService.broadcastEntityStateChangeEvent(getTenantId(), edgeId, - ComponentLifecycleEvent.DELETED); - - logEntityAction(edgeId, edge, - null, - ActionType.DELETED, null, strEdgeId); - - } catch (Exception e) { - - logEntityAction(emptyId(EntityType.EDGE), - null, - null, - ActionType.DELETED, e, strEdgeId); - - throw handleException(e); - } + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.DELETE); + tbEdgeService.deleteEdge(edge, getCurrentUser()); } @ApiOperation(value = "Get Tenant Edges (getEdges)", @@ -261,32 +217,11 @@ public class EdgeController extends BaseController { @PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { checkParameter("customerId", strCustomerId); checkParameter(EDGE_ID, strEdgeId); - try { - CustomerId customerId = new CustomerId(toUUID(strCustomerId)); - Customer customer = checkCustomerId(customerId, Operation.READ); - - EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); - checkEdgeId(edgeId, Operation.ASSIGN_TO_CUSTOMER); - - Edge savedEdge = checkNotNull(edgeService.assignEdgeToCustomer(getCurrentUser().getTenantId(), edgeId, customerId)); - - tbClusterService.broadcastEntityStateChangeEvent(getTenantId(), edgeId, - ComponentLifecycleEvent.UPDATED); - - logEntityAction(edgeId, savedEdge, - savedEdge.getCustomerId(), - ActionType.ASSIGNED_TO_CUSTOMER, null, strEdgeId, strCustomerId, customer.getName()); - - sendEntityAssignToCustomerNotificationMsg(savedEdge.getTenantId(), savedEdge.getId(), - customerId, EdgeEventActionType.ASSIGNED_TO_CUSTOMER); - - return savedEdge; - } catch (Exception e) { - logEntityAction(emptyId(EntityType.EDGE), null, - null, - ActionType.ASSIGNED_TO_CUSTOMER, e, strEdgeId, strCustomerId); - throw handleException(e); - } + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + Customer customer = checkCustomerId(customerId, Operation.READ); + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + checkEdgeId(edgeId, Operation.ASSIGN_TO_CUSTOMER); + return tbEdgeService.assignEdgeToCustomer(getTenantId(), edgeId, customer, getCurrentUser()); } @ApiOperation(value = "Unassign edge from customer (unassignEdgeFromCustomer)", @@ -298,33 +233,14 @@ public class EdgeController extends BaseController { public Edge unassignEdgeFromCustomer(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); - try { - EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); - Edge edge = checkEdgeId(edgeId, Operation.UNASSIGN_FROM_CUSTOMER); - if (edge.getCustomerId() == null || edge.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { - throw new IncorrectParameterException("Edge isn't assigned to any customer!"); - } - Customer customer = checkCustomerId(edge.getCustomerId(), Operation.READ); - - Edge savedEdge = checkNotNull(edgeService.unassignEdgeFromCustomer(getCurrentUser().getTenantId(), edgeId)); - - tbClusterService.broadcastEntityStateChangeEvent(getTenantId(), edgeId, - ComponentLifecycleEvent.UPDATED); - - logEntityAction(edgeId, edge, - edge.getCustomerId(), - ActionType.UNASSIGNED_FROM_CUSTOMER, null, strEdgeId, customer.getId().toString(), customer.getName()); - - sendEntityAssignToCustomerNotificationMsg(savedEdge.getTenantId(), savedEdge.getId(), - customer.getId(), EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER); - - return savedEdge; - } catch (Exception e) { - logEntityAction(emptyId(EntityType.EDGE), null, - null, - ActionType.UNASSIGNED_FROM_CUSTOMER, e, strEdgeId); - throw handleException(e); + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.UNASSIGN_FROM_CUSTOMER); + if (edge.getCustomerId() == null || edge.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { + throw new IncorrectParameterException("Edge isn't assigned to any customer!"); } + Customer customer = checkCustomerId(edge.getCustomerId(), Operation.READ); + + return tbEdgeService.unassignEdgeFromCustomer(edge, customer, getCurrentUser()); } @ApiOperation(value = "Make edge publicly available (assignEdgeToPublicCustomer)", @@ -338,26 +254,9 @@ public class EdgeController extends BaseController { public Edge assignEdgeToPublicCustomer(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); - try { - EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); - Edge edge = checkEdgeId(edgeId, Operation.ASSIGN_TO_CUSTOMER); - Customer publicCustomer = customerService.findOrCreatePublicCustomer(edge.getTenantId()); - Edge savedEdge = checkNotNull(edgeService.assignEdgeToCustomer(getCurrentUser().getTenantId(), edgeId, publicCustomer.getId())); - - tbClusterService.broadcastEntityStateChangeEvent(getTenantId(), edgeId, - ComponentLifecycleEvent.UPDATED); - - logEntityAction(edgeId, savedEdge, - savedEdge.getCustomerId(), - ActionType.ASSIGNED_TO_CUSTOMER, null, strEdgeId, publicCustomer.getId().toString(), publicCustomer.getName()); - - return savedEdge; - } catch (Exception e) { - logEntityAction(emptyId(EntityType.EDGE), null, - null, - ActionType.ASSIGNED_TO_CUSTOMER, e, strEdgeId); - throw handleException(e); - } + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + checkEdgeId(edgeId, Operation.ASSIGN_TO_CUSTOMER); + return tbEdgeService.assignEdgeToPublicCustomer(getTenantId(), edgeId, getCurrentUser()); } @ApiOperation(value = "Get Tenant Edges (getTenantEdges)", @@ -455,29 +354,12 @@ public class EdgeController extends BaseController { @PathVariable("ruleChainId") String strRuleChainId) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); checkParameter("ruleChainId", strRuleChainId); - try { - RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId)); - checkRuleChain(ruleChainId, Operation.WRITE); - - EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); - Edge edge = checkEdgeId(edgeId, Operation.WRITE); - accessControlService.checkPermission(getCurrentUser(), Resource.EDGE, Operation.WRITE, - edge.getId(), edge); - - Edge updatedEdge = edgeNotificationService.setEdgeRootRuleChain(getTenantId(), edge, ruleChainId); - - tbClusterService.broadcastEntityStateChangeEvent(updatedEdge.getTenantId(), updatedEdge.getId(), ComponentLifecycleEvent.UPDATED); - - logEntityAction(updatedEdge.getId(), updatedEdge, null, ActionType.UPDATED, null); - - return updatedEdge; - } catch (Exception e) { - logEntityAction(emptyId(EntityType.EDGE), - null, - null, - ActionType.UPDATED, e, strEdgeId); - throw handleException(e); - } + RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId)); + checkRuleChain(ruleChainId, Operation.WRITE); + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.WRITE); + accessControlService.checkPermission(getCurrentUser(), Resource.EDGE, Operation.WRITE, edge.getId(), edge); + return tbEdgeService.setEdgeRootRuleChain(edge, ruleChainId, getCurrentUser()); } @ApiOperation(value = "Get Customer Edges (getCustomerEdges)", @@ -693,12 +575,6 @@ public class EdgeController extends BaseController { throw new DataValidationException("Root edge rule chain is not available!"); } - return edgeBulkImportService.processBulkImport(request, user, importedAssetInfo -> { - try { - onEdgeCreatedOrUpdated(user.getTenantId(), importedAssetInfo.getEntity(), edgeTemplateRootRuleChain, importedAssetInfo.isUpdated(), user); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); + return edgeBulkImportService.processBulkImport(request, user); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java index 117b06d224..038dcadfc8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java @@ -18,13 +18,17 @@ package org.thingsboard.server.service.edge; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.dao.edge.EdgeService; +import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.edge.TbEdgeService; import org.thingsboard.server.service.importing.AbstractBulkImportService; import org.thingsboard.server.service.importing.BulkImportColumnType; import org.thingsboard.server.service.security.model.SecurityUser; @@ -37,6 +41,8 @@ import java.util.Optional; @RequiredArgsConstructor public class EdgeBulkImportService extends AbstractBulkImportService { private final EdgeService edgeService; + private final TbEdgeService tbEdgeService; + private final RuleChainService ruleChainService; @Override protected void setEntityFields(Edge entity, Map fields) { @@ -66,9 +72,11 @@ public class EdgeBulkImportService extends AbstractBulkImportService { entity.setAdditionalInfo(additionalInfo); } + @SneakyThrows @Override protected Edge saveEntity(SecurityUser user, Edge entity, Map fields) { - return edgeService.saveEdge(entity); + RuleChain edgeTemplateRootRuleChain = ruleChainService.getEdgeTemplateRootRuleChain(user.getTenantId()); + return tbEdgeService.saveEdge(entity, edgeTemplateRootRuleChain, user); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java index 9e597d729d..fcdf67449f 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java @@ -50,9 +50,11 @@ import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.service.action.EntityActionService; +import org.thingsboard.server.service.edge.EdgeNotificationService; import org.thingsboard.server.service.executors.DbCallbackExecutorService; import javax.mail.MessagingException; @@ -102,6 +104,10 @@ public abstract class AbstractTbEntityService { protected ClaimDevicesService claimDevicesService; @Autowired protected TbTenantProfileCache tenantProfileCache; + @Autowired + protected RuleChainService ruleChainService; + @Autowired + protected EdgeNotificationService edgeNotificationService; protected ListenableFuture removeAlarmsByEntityId(TenantId tenantId, EntityId entityId) { ListenableFuture> alarmsFuture = diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java index 7abe44744b..f247710b91 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.AssetId; @@ -36,6 +37,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; @@ -50,9 +52,6 @@ import java.util.List; @Service @RequiredArgsConstructor public class DefaultTbNotificationEntityService implements TbNotificationEntityService { - - protected static final int DEFAULT_PAGE_SIZE = 1000; - private static final ObjectMapper json = new ObjectMapper(); private final EntityActionService entityActionService; @@ -74,6 +73,7 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS sendDeleteNotificationMsg(tenantId, entityId, relatedEdgeIds); } + @Override public void notifyAssignOrUnassignEntityToCustomer(TenantId tenantId, I entityId, CustomerId customerId, E entity, ActionType actionType, @@ -97,7 +97,6 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS sendEntityAssignToEdgeNotificationMsg(tenantId, edgeId, entityId, edgeActionType); } - //Device @Override public void notifyCreateOrUpdateDevice(TenantId tenantId, DeviceId deviceId, CustomerId customerId, Device device, Device oldDevice, ActionType actionType, @@ -130,7 +129,7 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS pushAssignedFromNotification(tenant, newTenantId, device); } - //Asset + @Override public void notifyCreateOrUpdateAsset(TenantId tenantId, AssetId assetId, CustomerId customerId, Asset asset, ActionType actionType, SecurityUser user, Object... additionalInfo) { logEntityAction(tenantId, assetId, asset, customerId, actionType, user, additionalInfo); @@ -140,6 +139,42 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS } } + @Override + public void notifyEdge(TenantId tenantId, EdgeId edgeId, CustomerId customerId, Edge edge, ActionType actionType, + SecurityUser user, Object... additionalInfo) { + ComponentLifecycleEvent lifecycleEvent; + EdgeEventActionType edgeEventActionType = null; + switch (actionType) { + case ADDED: + lifecycleEvent = ComponentLifecycleEvent.CREATED; + break; + case UPDATED: + lifecycleEvent = ComponentLifecycleEvent.UPDATED; + break; + case ASSIGNED_TO_CUSTOMER: + lifecycleEvent = ComponentLifecycleEvent.UPDATED; + edgeEventActionType = EdgeEventActionType.ASSIGNED_TO_CUSTOMER; + break; + case UNASSIGNED_FROM_CUSTOMER: + lifecycleEvent = ComponentLifecycleEvent.UPDATED; + edgeEventActionType = EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER; + break; + case DELETED: + lifecycleEvent = ComponentLifecycleEvent.DELETED; + break; + default: + throw new IllegalArgumentException("Unknown actionType: " + actionType); + } + + tbClusterService.broadcastEntityStateChangeEvent(tenantId, edgeId, lifecycleEvent); + logEntityAction(tenantId, edgeId, edge, customerId, actionType, user, additionalInfo); + + //Send notification to edge + if (edgeEventActionType != null) { + sendEntityAssignToCustomerNotificationMsg(tenantId, edgeId, customerId, edgeEventActionType); + } + } + private void logEntityAction(TenantId tenantId, I entityId, E entity, CustomerId customerId, ActionType actionType, SecurityUser user, Object... additionalInfo) { logEntityAction(tenantId, entityId, entity, customerId, actionType, user, null, additionalInfo); @@ -154,11 +189,11 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS } } - protected void sendEntityNotificationMsg(TenantId tenantId, EntityId entityId, EdgeEventActionType action) { + private void sendEntityNotificationMsg(TenantId tenantId, EntityId entityId, EdgeEventActionType action) { sendNotificationMsgToEdgeService(tenantId, null, entityId, null, null, action); } - protected void sendEntityAssignToCustomerNotificationMsg(TenantId tenantId, EntityId entityId, CustomerId customerId, EdgeEventActionType action) { + private void sendEntityAssignToCustomerNotificationMsg(TenantId tenantId, EntityId entityId, CustomerId customerId, EdgeEventActionType action) { try { sendNotificationMsgToEdgeService(tenantId, null, entityId, json.writeValueAsString(customerId), null, action); } catch (Exception e) { @@ -166,11 +201,11 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS } } - protected void sendDeleteNotificationMsg(TenantId tenantId, EntityId entityId, List edgeIds) { + private void sendDeleteNotificationMsg(TenantId tenantId, EntityId entityId, List edgeIds) { sendDeleteNotificationMsg(tenantId, entityId, edgeIds, null); } - protected void sendDeleteNotificationMsg(TenantId tenantId, EntityId entityId, List edgeIds, String body) { + private void sendDeleteNotificationMsg(TenantId tenantId, EntityId entityId, List edgeIds, String body) { if (edgeIds != null && !edgeIds.isEmpty()) { for (EdgeId edgeId : edgeIds) { sendNotificationMsgToEdgeService(tenantId, edgeId, entityId, body, null, EdgeEventActionType.DELETED); @@ -178,7 +213,7 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS } } - protected void sendEntityAssignToEdgeNotificationMsg(TenantId tenantId, EdgeId edgeId, EntityId entityId, EdgeEventActionType action) { + private void sendEntityAssignToEdgeNotificationMsg(TenantId tenantId, EdgeId edgeId, EntityId entityId, EdgeEventActionType action) { sendNotificationMsgToEdgeService(tenantId, edgeId, entityId, null, null, action); } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java index d4b2f86a99..36058908a1 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java @@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; @@ -70,4 +71,6 @@ public interface TbNotificationEntityService { void notifyCreateOrUpdateAsset(TenantId tenantId, AssetId assetId, CustomerId customerId, Asset asset, ActionType actionType, SecurityUser user, Object... additionalInfo); + void notifyEdge(TenantId tenantId, EdgeId edgeId, CustomerId customerId, Edge edge, ActionType actionType, SecurityUser user, Object... additionalInfo); + } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java new file mode 100644 index 0000000000..93487ac5cd --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java @@ -0,0 +1,150 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.entitiy.edge; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.AbstractTbEntityService; +import org.thingsboard.server.service.security.model.SecurityUser; + +@AllArgsConstructor +@TbCoreComponent +@Service +@Slf4j +public class DefaultTbEdgeService extends AbstractTbEntityService implements TbEdgeService { + + @Override + public Edge saveEdge(Edge edge, RuleChain edgeTemplateRootRuleChain, SecurityUser user) throws ThingsboardException { + ActionType actionType = edge.getId() == null ? ActionType.ADDED : ActionType.UPDATED; + TenantId tenantId = edge.getTenantId(); + try { + Edge savedEdge = checkNotNull(edgeService.saveEdge(edge)); + EdgeId edgeId = savedEdge.getId(); + + if (actionType == ActionType.ADDED) { + ruleChainService.assignRuleChainToEdge(tenantId, edgeTemplateRootRuleChain.getId(), edgeId); + edgeNotificationService.setEdgeRootRuleChain(tenantId, savedEdge, edgeTemplateRootRuleChain.getId()); + edgeService.assignDefaultRuleChainsToEdge(tenantId, edgeId); + } + + notificationEntityService.notifyEdge(tenantId, edgeId, savedEdge.getCustomerId(), savedEdge, actionType, user); + + return savedEdge; + } catch (Exception e) { + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.EDGE), edge, null, actionType, user, e); + throw handleException(e); + } + } + + @Override + public void deleteEdge(Edge edge, SecurityUser user) throws ThingsboardException { + ActionType actionType = ActionType.DELETED; + EdgeId edgeId = edge.getId(); + TenantId tenantId = edge.getTenantId(); + try { + edgeService.deleteEdge(tenantId, edgeId); + notificationEntityService.notifyEdge(tenantId, edgeId, edge.getCustomerId(), edge, actionType, user, edgeId.toString()); + + } catch (Exception e) { + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.EDGE), edge, null, actionType, user, e); + throw handleException(e); + } + } + + @Override + public Edge assignEdgeToCustomer(TenantId tenantId, EdgeId edgeId, Customer customer, SecurityUser user) throws ThingsboardException { + ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER; + CustomerId customerId = customer.getId(); + try { + Edge savedEdge = checkNotNull(edgeService.assignEdgeToCustomer(tenantId, edgeId, customerId)); + + notificationEntityService.notifyEdge(tenantId, edgeId, customerId, savedEdge, actionType, user, + edgeId.toString(), customerId.toString(), customer.getName()); + + return savedEdge; + } catch (Exception e) { + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.EDGE), null, null, + actionType, user, e, edgeId.toString(), customerId.toString()); + throw handleException(e); + } + } + + @Override + public Edge unassignEdgeFromCustomer(Edge edge, Customer customer, SecurityUser user) throws ThingsboardException { + ActionType actionType = ActionType.UNASSIGNED_FROM_CUSTOMER; + TenantId tenantId = edge.getTenantId(); + EdgeId edgeId = edge.getId(); + CustomerId customerId = customer.getId(); + try { + Edge savedEdge = checkNotNull(edgeService.unassignEdgeFromCustomer(tenantId, edgeId)); + + notificationEntityService.notifyEdge(tenantId, edgeId, customerId, savedEdge, actionType, user, + edgeId.toString(), customerId.toString(), customer.getName()); + return savedEdge; + } catch (Exception e) { + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.EDGE), null, null, + actionType, user, e, edgeId.toString()); + throw handleException(e); + } + } + + @Override + public Edge assignEdgeToPublicCustomer(TenantId tenantId, EdgeId edgeId, SecurityUser user) throws ThingsboardException { + ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER; + try { + Customer publicCustomer = customerService.findOrCreatePublicCustomer(tenantId); + CustomerId customerId = publicCustomer.getId(); + Edge savedEdge = checkNotNull(edgeService.assignEdgeToCustomer(tenantId, edgeId, customerId)); + + notificationEntityService.notifyEdge(tenantId, edgeId, customerId, savedEdge, actionType, user, + edgeId.toString(), customerId.toString(), publicCustomer.getName()); + + return savedEdge; + } catch (Exception e) { + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.EDGE), null, null, + actionType, user, e, edgeId.toString()); + throw handleException(e); + } + } + + @Override + public Edge setEdgeRootRuleChain(Edge edge, RuleChainId ruleChainId, SecurityUser user) throws ThingsboardException { + ActionType actionType = ActionType.UPDATED; + TenantId tenantId = edge.getTenantId(); + EdgeId edgeId = edge.getId(); + try { + Edge updatedEdge = edgeNotificationService.setEdgeRootRuleChain(tenantId, edge, ruleChainId); + notificationEntityService.notifyEdge(tenantId, edgeId, null, updatedEdge, actionType, user); + return updatedEdge; + } catch (Exception e) { + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.EDGE), null, null, + actionType, user, e, edgeId.toString()); + throw handleException(e); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/TbEdgeService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/TbEdgeService.java new file mode 100644 index 0000000000..5ed27fbbb3 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/TbEdgeService.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.entitiy.edge; + +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.service.security.model.SecurityUser; + +public interface TbEdgeService { + Edge saveEdge(Edge edge, RuleChain edgeTemplateRootRuleChain, SecurityUser user) throws ThingsboardException; + + void deleteEdge(Edge edge, SecurityUser user) throws ThingsboardException; + + Edge assignEdgeToCustomer(TenantId tenantId, EdgeId edgeId, Customer customer, SecurityUser user) throws ThingsboardException; + + Edge unassignEdgeFromCustomer(Edge edge, Customer customer, SecurityUser user) throws ThingsboardException; + + Edge assignEdgeToPublicCustomer(TenantId tenantId, EdgeId edgeId, SecurityUser user) throws ThingsboardException; + + Edge setEdgeRootRuleChain(Edge edge, RuleChainId ruleChainId, SecurityUser user) throws ThingsboardException; +} diff --git a/application/src/main/java/org/thingsboard/server/service/importing/AbstractBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/importing/AbstractBulkImportService.java index f2d8ded680..81a0068a4d 100644 --- a/application/src/main/java/org/thingsboard/server/service/importing/AbstractBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/importing/AbstractBulkImportService.java @@ -95,7 +95,7 @@ public abstract class AbstractBulkImportService processBulkImport(BulkImportRequest request, SecurityUser user, Consumer> onEntityImported) throws Exception { + public final BulkImportResult processBulkImport(BulkImportRequest request, SecurityUser user) throws Exception { List entitiesData = parseData(request); BulkImportResult result = new BulkImportResult<>(); @@ -106,10 +106,9 @@ public abstract class AbstractBulkImportService DonAsynchron.submit(() -> { SecurityContextHolder.setContext(securityContext); - ImportedEntityInfo importedEntityInfo = saveEntity(entityData.getFields(), user); + ImportedEntityInfo importedEntityInfo = saveEntity(entityData.getFields(), user); E entity = importedEntityInfo.getEntity(); - onEntityImported.accept(importedEntityInfo); saveKvs(user, entity, entityData.getKvs()); return importedEntityInfo; From 15104028ea640503c6b38b02887ad9703e468d68 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Fri, 29 Apr 2022 16:12:22 +0300 Subject: [PATCH 070/276] Investigating the cache issues --- .../device/DeviceProvisionServiceImpl.java | 2 +- .../service/edge/rpc/EdgeGrpcSession.java | 6 +- .../rpc/processor/TelemetryEdgeProcessor.java | 6 +- .../DefaultSystemDataLoaderService.java | 2 +- .../DefaultTelemetrySubscriptionService.java | 4 +- .../AbstractRuleEngineControllerTest.java | 8 ++- ...actRuleEngineLifecycleIntegrationTest.java | 26 ++++---- .../src/test/resources/logback-test.xml | 3 +- .../dao/attributes/AttributesService.java | 4 +- .../attributes/AttributesCacheWrapper.java | 58 ++--------------- .../server/dao/attributes/AttributesDao.java | 4 +- .../dao/attributes/BaseAttributesService.java | 13 ++-- .../attributes/CachedAttributesService.java | 55 ++++++++-------- .../DefaultAttributesCacheWrapper.java | 63 +++++++++++++++++++ .../dao/relation/BaseRelationService.java | 28 ++++----- .../server/dao/sql/TbSqlBlockingQueue.java | 7 ++- .../server/dao/sql/TbSqlQueueElement.java | 2 + .../dao/sql/attributes/JpaAttributeDao.java | 26 ++++---- .../dao/service/BaseEntityServiceTest.java | 30 ++++----- .../metadata/TbAbstractGetAttributesNode.java | 4 +- 20 files changed, 188 insertions(+), 163 deletions(-) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/attributes/DefaultAttributesCacheWrapper.java diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java index ad734a5c07..7fd42740c4 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java @@ -210,7 +210,7 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { } } - private ListenableFuture> saveProvisionStateAttribute(Device device) { + private ListenableFuture> saveProvisionStateAttribute(Device device) { return attributesService.save(device.getTenantId(), device.getId(), DataConstants.SERVER_SCOPE, Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry(DEVICE_PROVISION_STATE, PROVISIONED_STATE), System.currentTimeMillis()))); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index de0cea14d6..6c991444b2 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -309,10 +309,10 @@ public final class EdgeGrpcSession implements Closeable { public void onSuccess(@Nullable UUID ifOffset) { if (ifOffset != null) { Long newStartTs = Uuids.unixTimestamp(ifOffset); - ListenableFuture> updateFuture = updateQueueStartTs(newStartTs); + ListenableFuture> updateFuture = updateQueueStartTs(newStartTs); Futures.addCallback(updateFuture, new FutureCallback<>() { @Override - public void onSuccess(@Nullable List list) { + public void onSuccess(@Nullable List list) { log.debug("[{}] queue offset was updated [{}][{}]", sessionId, ifOffset, newStartTs); result.set(null); } @@ -496,7 +496,7 @@ public final class EdgeGrpcSession implements Closeable { }, ctx.getGrpcCallbackExecutorService()); } - private ListenableFuture> updateQueueStartTs(Long newStartTs) { + private ListenableFuture> updateQueueStartTs(Long newStartTs) { log.trace("[{}] updating QueueStartTs [{}][{}]", this.sessionId, edge.getId(), newStartTs); List attributes = Collections.singletonList( new BaseAttributeKvEntry( diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java index 9a9b568a23..a72f41c887 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java @@ -206,10 +206,10 @@ public class TelemetryEdgeProcessor extends BaseEdgeProcessor { SettableFuture futureToSet = SettableFuture.create(); JsonObject json = JsonUtils.getJsonObject(msg.getKvList()); Set attributes = JsonConverter.convertToAttributes(json); - ListenableFuture> future = attributesService.save(tenantId, entityId, metaData.getValue("scope"), new ArrayList<>(attributes)); - Futures.addCallback(future, new FutureCallback>() { + ListenableFuture> future = attributesService.save(tenantId, entityId, metaData.getValue("scope"), new ArrayList<>(attributes)); + Futures.addCallback(future, new FutureCallback<>() { @Override - public void onSuccess(@Nullable List voids) { + public void onSuccess(@Nullable List keys) { Pair defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId); String queueName = defaultQueueAndRuleChain.getKey(); RuleChainId ruleChainId = defaultQueueAndRuleChain.getValue(); diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index c5bdf722af..9e7636879f 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -532,7 +532,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry(key, value))), 0L); addTsCallback(saveFuture, new TelemetrySaveCallback<>(deviceId, key, value)); } else { - ListenableFuture> saveFuture = attributesService.save(TenantId.SYS_TENANT_ID, deviceId, DataConstants.SERVER_SCOPE, + ListenableFuture> saveFuture = attributesService.save(TenantId.SYS_TENANT_ID, deviceId, DataConstants.SERVER_SCOPE, Collections.singletonList(new BaseAttributeKvEntry(new BooleanDataEntry(key, value) , System.currentTimeMillis()))); addTsCallback(saveFuture, new TelemetrySaveCallback<>(deviceId, key, value)); diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java index 440c9075e8..38275c09b6 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java @@ -243,7 +243,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer @Override public void saveAndNotifyInternal(TenantId tenantId, EntityId entityId, String scope, List attributes, boolean notifyDevice, FutureCallback callback) { - ListenableFuture> saveFuture = attrService.save(tenantId, entityId, scope, attributes); + ListenableFuture> saveFuture = attrService.save(tenantId, entityId, scope, attributes); addVoidCallback(saveFuture, callback); addWsCallback(saveFuture, success -> onAttributesUpdate(tenantId, entityId, scope, attributes, notifyDevice)); } @@ -269,7 +269,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer @Override public void deleteAndNotifyInternal(TenantId tenantId, EntityId entityId, String scope, List keys, FutureCallback callback) { - ListenableFuture> deleteFuture = attrService.removeAll(tenantId, entityId, scope, keys); + ListenableFuture> deleteFuture = attrService.removeAll(tenantId, entityId, scope, keys); addVoidCallback(deleteFuture, callback); addWsCallback(deleteFuture, success -> onAttributesDelete(tenantId, entityId, scope, keys)); } diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java index 79e19d5c82..40cce12a12 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java @@ -57,12 +57,18 @@ public abstract class AbstractRuleEngineControllerTest extends AbstractControlle } protected PageData getDebugEvents(TenantId tenantId, EntityId entityId, int limit) throws Exception { + return getEvents(tenantId, entityId, DataConstants.DEBUG_RULE_NODE, limit); + } + + protected PageData getEvents(TenantId tenantId, EntityId entityId, String eventType, int limit) throws Exception { TimePageLink pageLink = new TimePageLink(limit); return doGetTypedWithTimePageLink("/api/events/{entityType}/{entityId}/{eventType}?tenantId={tenantId}&", new TypeReference>() { - }, pageLink, entityId.getEntityType(), entityId.getId(), DataConstants.DEBUG_RULE_NODE, tenantId.getId()); + }, pageLink, entityId.getEntityType(), entityId.getId(), eventType, tenantId.getId()); } + + protected JsonNode getMetadata(Event outEvent) { String metaDataStr = outEvent.getBody().get("metadata").asText(); try { diff --git a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java index c3a9d9817a..0f08e618c3 100644 --- a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java @@ -105,17 +105,20 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac Assert.assertNotNull(ruleChainFinal.getFirstRuleNodeId()); //TODO find out why RULE_NODE update event did not appear all the time -// List rcEvents = Awaitility.await("get debug by rule chain") -// .pollInterval(10, MILLISECONDS) -// .atMost(TIMEOUT, TimeUnit.SECONDS) -// .until(() -> { -// List debugEvents = getDebugEvents(tenantId, ruleChainFinal.getFirstRuleNodeId(), 1000) -// .getData().stream().filter(x->true).collect(Collectors.toList()); -// log.warn("filtered debug events [{}]", debugEvents.size()); -// debugEvents.forEach((e) -> log.warn("event: {}", e)); -// return debugEvents; -// }, -// x -> x.size() >= 2); + List rcEvents = Awaitility.await("Rule Node started successfully") + .pollInterval(10, MILLISECONDS) + .atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> { + List debugEvents = getEvents(tenantId, ruleChainFinal.getFirstRuleNodeId(), DataConstants.LC_EVENT, 1000) + .getData().stream().filter(e-> { + var body = e.getBody(); + return body.has("event") && body.get("event").asText().equals("STARTED") + && body.has("success") && body.get("success").asBoolean(); + }).collect(Collectors.toList()); + debugEvents.forEach((e) -> log.trace("event: {}", e)); + return debugEvents; + }, + x -> x.size() == 1); // Saving the device Device device = new Device(); @@ -133,6 +136,7 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac TbMsg tbMsg = TbMsg.newMsg("CUSTOM", device.getId(), new TbMsgMetaData(), "{}", tbMsgCallback); QueueToRuleEngineMsg qMsg = new QueueToRuleEngineMsg(tenantId, tbMsg, null, null); // Pushing Message to the system + log.warn("before tell tbMsgCallback"); actorSystem.tell(qMsg); log.warn("awaiting tbMsgCallback"); Mockito.verify(tbMsgCallback, Mockito.timeout(TimeUnit.SECONDS.toMillis(TIMEOUT))).onSuccess(); diff --git a/application/src/test/resources/logback-test.xml b/application/src/test/resources/logback-test.xml index d3301bf660..f051721391 100644 --- a/application/src/test/resources/logback-test.xml +++ b/application/src/test/resources/logback-test.xml @@ -9,7 +9,8 @@ - + + diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java index 5ae8f68eb1..6497778676 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java @@ -37,9 +37,9 @@ public interface AttributesService { ListenableFuture> findAll(TenantId tenantId, EntityId entityId, String scope); - ListenableFuture> save(TenantId tenantId, EntityId entityId, String scope, List attributes); + ListenableFuture> save(TenantId tenantId, EntityId entityId, String scope, List attributes); - ListenableFuture> removeAll(TenantId tenantId, EntityId entityId, String scope, List attributeKeys); + ListenableFuture> removeAll(TenantId tenantId, EntityId entityId, String scope, List attributeKeys); List findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesCacheWrapper.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesCacheWrapper.java index 6d620800ca..466cd4b0f1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesCacheWrapper.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesCacheWrapper.java @@ -1,63 +1,15 @@ -/** - * Copyright © 2016-2022 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. - */ package org.thingsboard.server.dao.attributes; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.Cache; -import org.springframework.cache.CacheManager; -import org.springframework.context.annotation.Primary; -import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import static org.thingsboard.server.common.data.CacheConstants.ATTRIBUTES_CACHE; +public interface AttributesCacheWrapper { -@Service -@ConditionalOnProperty(prefix = "cache.attributes", value = "enabled", havingValue = "true") -@Primary -@Slf4j -public class AttributesCacheWrapper { - private final Cache attributesCache; + Cache.ValueWrapper get(AttributeCacheKey attributeCacheKey); - public AttributesCacheWrapper(CacheManager cacheManager) { - this.attributesCache = cacheManager.getCache(ATTRIBUTES_CACHE); - } + void put(AttributeCacheKey attributeCacheKey, AttributeKvEntry attributeKvEntry); - public Cache.ValueWrapper get(AttributeCacheKey attributeCacheKey) { - try { - return attributesCache.get(attributeCacheKey); - } catch (Exception e) { - log.debug("Failed to retrieve element from cache for key {}. Reason - {}.", attributeCacheKey, e.getMessage()); - return null; - } - } + void putIfAbsent(AttributeCacheKey attributeCacheKey, AttributeKvEntry attributeKvEntry); - public void put(AttributeCacheKey attributeCacheKey, AttributeKvEntry attributeKvEntry) { - try { - attributesCache.put(attributeCacheKey, attributeKvEntry); - } catch (Exception e) { - log.debug("Failed to put element from cache for key {}. Reason - {}.", attributeCacheKey, e.getMessage()); - } - } - - public void evict(AttributeCacheKey attributeCacheKey) { - try { - attributesCache.evict(attributeCacheKey); - } catch (Exception e) { - log.debug("Failed to evict element from cache for key {}. Reason - {}.", attributeCacheKey, e.getMessage()); - } - } + void evict(AttributeCacheKey attributeCacheKey); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java index e3819489de..efa4a30ce4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java @@ -37,9 +37,9 @@ public interface AttributesDao { ListenableFuture> findAll(TenantId tenantId, EntityId entityId, String attributeType); - ListenableFuture save(TenantId tenantId, EntityId entityId, String attributeType, AttributeKvEntry attribute); + ListenableFuture save(TenantId tenantId, EntityId entityId, String attributeType, AttributeKvEntry attribute); - ListenableFuture> removeAll(TenantId tenantId, EntityId entityId, String attributeType, List keys); + List> removeAll(TenantId tenantId, EntityId entityId, String attributeType, List keys); List findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java index 8d6c57e709..508cf4c16a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -80,17 +80,16 @@ public class BaseAttributesService implements AttributesService { } @Override - public ListenableFuture> save(TenantId tenantId, EntityId entityId, String scope, List attributes) { + public ListenableFuture> save(TenantId tenantId, EntityId entityId, String scope, List attributes) { validate(entityId, scope); - attributes.forEach(attribute -> validate(attribute)); - - List> saveFutures = attributes.stream().map(attribute -> attributesDao.save(tenantId, entityId, scope, attribute)).collect(Collectors.toList()); + attributes.forEach(AttributeUtils::validate); + List> saveFutures = attributes.stream().map(attribute -> attributesDao.save(tenantId, entityId, scope, attribute)).collect(Collectors.toList()); return Futures.allAsList(saveFutures); } @Override - public ListenableFuture> removeAll(TenantId tenantId, EntityId entityId, String scope, List attributeKeys) { + public ListenableFuture> removeAll(TenantId tenantId, EntityId entityId, String scope, List attributeKeys) { validate(entityId, scope); - return attributesDao.removeAll(tenantId, entityId, scope, attributeKeys); + return Futures.allAsList(attributesDao.removeAll(tenantId, entityId, scope, attributeKeys)); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index feb4b0e275..abe741d6d7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -30,7 +30,6 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.stats.DefaultCounter; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.cache.CacheExecutorService; @@ -88,9 +87,9 @@ public class CachedAttributesService implements AttributesService { /** * Will return: - * - for the local cache type (cache.type="coffeine"): directExecutor (run callback immediately in the same thread) - * - for the remote cache: dedicated thread pool for the cache IO calls to unblock any caller thread - * */ + * - for the local cache type (cache.type="coffeine"): directExecutor (run callback immediately in the same thread) + * - for the remote cache: dedicated thread pool for the cache IO calls to unblock any caller thread + */ Executor getExecutor(String cacheType, CacheExecutorService cacheExecutorService) { if (StringUtils.isEmpty(cacheType) || LOCAL_CACHE_TYPE.equals(cacheType)) { log.info("Going to use directExecutor for the local cache type {}", cacheType); @@ -116,7 +115,7 @@ public class CachedAttributesService implements AttributesService { ListenableFuture> result = attributesDao.find(tenantId, entityId, scope, attributeKey); return Futures.transform(result, foundAttrKvEntry -> { // TODO: think if it's a good idea to store 'empty' attributes - cacheWrapper.put(attributeCacheKey, foundAttrKvEntry.orElse(null)); + cacheWrapper.putIfAbsent(attributeCacheKey, foundAttrKvEntry.orElse(null)); return foundAttrKvEntry; }, cacheExecutor); } @@ -162,12 +161,12 @@ public class CachedAttributesService implements AttributesService { private List mergeDbAndCacheAttributes(EntityId entityId, String scope, List cachedAttributes, Set notFoundAttributeKeys, List foundInDbAttributes) { for (AttributeKvEntry foundInDbAttribute : foundInDbAttributes) { AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, foundInDbAttribute.getKey()); - cacheWrapper.put(attributeCacheKey, foundInDbAttribute); + cacheWrapper.putIfAbsent(attributeCacheKey, foundInDbAttribute); notFoundAttributeKeys.remove(foundInDbAttribute.getKey()); } - for (String key : notFoundAttributeKeys){ - cacheWrapper.put(new AttributeCacheKey(scope, entityId, key), null); - } +// for (String key : notFoundAttributeKeys) { +// cacheWrapper.putIfAbsent(new AttributeCacheKey(scope, entityId, key), null); +// } List mergedAttributes = new ArrayList<>(cachedAttributes); mergedAttributes.addAll(foundInDbAttributes); return mergedAttributes; @@ -190,34 +189,30 @@ public class CachedAttributesService implements AttributesService { } @Override - public ListenableFuture> save(TenantId tenantId, EntityId entityId, String scope, List attributes) { + public ListenableFuture> save(TenantId tenantId, EntityId entityId, String scope, List attributes) { validate(entityId, scope); attributes.forEach(AttributeUtils::validate); - List> saveFutures = attributes.stream().map(attribute -> attributesDao.save(tenantId, entityId, scope, attribute)).collect(Collectors.toList()); - ListenableFuture> future = Futures.allAsList(saveFutures); + List> futures = new ArrayList<>(attributes.size()); + for (var attribute : attributes) { + ListenableFuture future = attributesDao.save(tenantId, entityId, scope, attribute); + futures.add(Futures.transform(future, key -> { + cacheWrapper.evict(new AttributeCacheKey(scope, entityId, key)); + return key; + }, cacheExecutor)); + } - // TODO: can do if (attributesCache.get() != null) attributesCache.put() instead, but will be more twice more requests to cache - List attributeKeys = attributes.stream().map(KvEntry::getKey).collect(Collectors.toList()); - future.addListener(() -> evictAttributesFromCache(tenantId, entityId, scope, attributeKeys), cacheExecutor); - return future; + return Futures.allAsList(futures); } @Override - public ListenableFuture> removeAll(TenantId tenantId, EntityId entityId, String scope, List attributeKeys) { + public ListenableFuture> removeAll(TenantId tenantId, EntityId entityId, String scope, List attributeKeys) { validate(entityId, scope); - ListenableFuture> future = attributesDao.removeAll(tenantId, entityId, scope, attributeKeys); - future.addListener(() -> evictAttributesFromCache(tenantId, entityId, scope, attributeKeys), cacheExecutor); - return future; + List> futures = attributesDao.removeAll(tenantId, entityId, scope, attributeKeys); + return Futures.allAsList(futures.stream().map(future -> Futures.transform(future, key -> { + cacheWrapper.evict(new AttributeCacheKey(scope, entityId, key)); + return key; + }, cacheExecutor)).collect(Collectors.toList())); } - private void evictAttributesFromCache(TenantId tenantId, EntityId entityId, String scope, List attributeKeys) { - try { - for (String attributeKey : attributeKeys) { - cacheWrapper.evict(new AttributeCacheKey(scope, entityId, attributeKey)); - } - } catch (Exception e) { - log.error("[{}][{}] Failed to remove values from cache.", tenantId, entityId, e); - } - } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/DefaultAttributesCacheWrapper.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/DefaultAttributesCacheWrapper.java new file mode 100644 index 0000000000..aa0222551a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/DefaultAttributesCacheWrapper.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.attributes; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; + +import static org.thingsboard.server.common.data.CacheConstants.ATTRIBUTES_CACHE; + +@Service +@ConditionalOnProperty(prefix = "cache.attributes", value = "enabled", havingValue = "true") +@Primary +@Slf4j +public class DefaultAttributesCacheWrapper implements AttributesCacheWrapper { + private final Cache attributesCache; + + public DefaultAttributesCacheWrapper(CacheManager cacheManager) { + this.attributesCache = cacheManager.getCache(ATTRIBUTES_CACHE); + } + + @Override + public Cache.ValueWrapper get(AttributeCacheKey attributeCacheKey) { + var result = attributesCache.get(attributeCacheKey); + log.warn("[{}] Get = {}", attributeCacheKey, result); + return result; + } + + @Override + public void put(AttributeCacheKey attributeCacheKey, AttributeKvEntry attributeKvEntry) { + log.warn("[{}] Put = {}", attributeCacheKey, attributeKvEntry); + attributesCache.put(attributeCacheKey, attributeKvEntry); + } + + @Override + public void putIfAbsent(AttributeCacheKey attributeCacheKey, AttributeKvEntry attributeKvEntry) { + var result = attributesCache.putIfAbsent(attributeCacheKey, attributeKvEntry); + log.warn("[{}] Put if absent = {}, result = {}", attributeCacheKey, attributeKvEntry, result); + } + + @Override + public void evict(AttributeCacheKey attributeCacheKey) { + log.warn("[{}] Evict", attributeCacheKey); + attributesCache.evict(attributeCacheKey); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index da119d453e..a5f0e04700 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -29,6 +29,7 @@ import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Caching; import org.springframework.dao.ConcurrencyFailureException; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityId; @@ -48,6 +49,8 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.ConstraintValidator; import javax.annotation.Nullable; +import java.io.PrintWriter; +import java.io.StringWriter; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -86,11 +89,7 @@ public class BaseRelationService implements RelationService { @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType, #typeGroup}") @Override public EntityRelation getRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { - try { - return getRelationAsync(tenantId, from, to, relationType, typeGroup).get(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } + return getRelation(tenantId, from, to, relationType, typeGroup); } @Override @@ -108,6 +107,7 @@ public class BaseRelationService implements RelationService { @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup, 'TO'}") }) @Override + @Transactional public boolean saveRelation(TenantId tenantId, EntityRelation relation) { log.trace("Executing saveRelation [{}]", relation); validate(relation); @@ -181,6 +181,7 @@ public class BaseRelationService implements RelationService { public ListenableFuture deleteRelationAsync(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { log.trace("Executing deleteRelationAsync [{}][{}][{}][{}]", from, to, relationType, typeGroup); validate(from, to, relationType, typeGroup); + //TODO: clear cache using transform return relationDao.deleteRelationAsync(tenantId, from, to, relationType, typeGroup); } @@ -319,11 +320,8 @@ public class BaseRelationService implements RelationService { public List findByFrom(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup) { validate(from); validateTypeGroup(typeGroup); - try { - return relationDao.findAllByFromAsync(tenantId, from, typeGroup).get(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } + log.trace("[{}] Find by from: [{}][{}]: ", tenantId, from, typeGroup, new RuntimeException()); + return relationDao.findAllByFrom(tenantId, from, typeGroup); } @Override @@ -345,7 +343,7 @@ public class BaseRelationService implements RelationService { } else { ListenableFuture> relationsFuture = relationDao.findAllByFromAsync(tenantId, from, typeGroup); Futures.addCallback(relationsFuture, - new FutureCallback>() { + new FutureCallback<>() { @Override public void onSuccess(@Nullable List result) { cache.putIfAbsent(fromAndTypeGroup, result); @@ -401,11 +399,7 @@ public class BaseRelationService implements RelationService { public List findByTo(TenantId tenantId, EntityId to, RelationTypeGroup typeGroup) { validate(to); validateTypeGroup(typeGroup); - try { - return relationDao.findAllByToAsync(tenantId, to, typeGroup).get(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } + return relationDao.findAllByTo(tenantId, to, typeGroup); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java index 94ce9efd20..c1ee7f3fd3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -66,7 +66,10 @@ public class TbSqlBlockingQueue implements TbSqlQueue { } queue.drainTo(entities, batchSize - 1); boolean fullPack = entities.size() == batchSize; - log.debug("[{}] Going to save {} entities", logName, entities.size()); + if (log.isDebugEnabled()) { + log.debug("[{}] Going to save {} entities", logName, entities.size()); + log.trace("[{}] Going to save entities: {}", logName, entities); + } Stream entitiesStream = entities.stream().map(TbSqlQueueElement::getEntity); saveFunction.accept( (params.isBatchSortEnabled() ? entitiesStream.sorted(batchUpdateComparator) : entitiesStream) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueueElement.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueueElement.java index 0de2d839ed..db15026819 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueueElement.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueueElement.java @@ -17,7 +17,9 @@ package org.thingsboard.server.dao.sql; import com.google.common.util.concurrent.SettableFuture; import lombok.Getter; +import lombok.ToString; +@ToString(exclude = "future") public final class TbSqlQueueElement { @Getter private final SettableFuture future; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java index 20b9e0d1f7..185dae1c2f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.sql.attributes; import com.google.common.collect.Lists; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -39,6 +40,7 @@ import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; +import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.List; @@ -153,7 +155,7 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl } @Override - public ListenableFuture save(TenantId tenantId, EntityId entityId, String attributeType, AttributeKvEntry attribute) { + public ListenableFuture save(TenantId tenantId, EntityId entityId, String attributeType, AttributeKvEntry attribute) { AttributeKvEntity entity = new AttributeKvEntity(); entity.setId(new AttributeKvCompositeKey(entityId.getEntityType(), entityId.getId(), attributeType, attribute.getKey())); entity.setLastUpdateTs(attribute.getLastUpdateTs()); @@ -165,18 +167,20 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl return addToQueue(entity); } - private ListenableFuture addToQueue(AttributeKvEntity entity) { - return queue.add(entity); + private ListenableFuture addToQueue(AttributeKvEntity entity) { + return Futures.transform(queue.add(entity), v -> entity.getId().getAttributeKey(), MoreExecutors.directExecutor()); } @Override - public ListenableFuture> removeAll(TenantId tenantId, EntityId entityId, String attributeType, List keys) { - return service.submit(() -> { - keys.forEach(key -> - attributeKvRepository.delete(entityId.getEntityType(), entityId.getId(), attributeType, key) - ); - return null; - }); + public List> removeAll(TenantId tenantId, EntityId entityId, String attributeType, List keys) { + List> futuresList = new ArrayList<>(keys.size()); + for (String key : keys) { + futuresList.add(service.submit(() -> { + attributeKvRepository.delete(entityId.getEntityType(), entityId.getId(), attributeType, key); + return key; + })); + } + return futuresList; } private AttributeKvCompositeKey getAttributeKvCompositeKey(EntityId entityId, String attributeType, String attributeKey) { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java index 430ac85391..808e499239 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java @@ -339,12 +339,12 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { List highTemperatures = new ArrayList<>(); createTestHierarchy(tenantId, assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures); - List>> attributeFutures = new ArrayList<>(); + List>> attributeFutures = new ArrayList<>(); for (int i = 0; i < devices.size(); i++) { Device device = devices.get(i); attributeFutures.add(saveLongAttribute(device.getId(), "temperature", temperatures.get(i), DataConstants.CLIENT_SCOPE)); } - Futures.successfulAsList(attributeFutures).get(); + Futures.allAsList(attributeFutures).get(); RelationsQueryFilter filter = new RelationsQueryFilter(); filter.setRootEntity(tenantId); @@ -518,12 +518,12 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { List highTemperatures = new ArrayList<>(); createTestHierarchy(tenantId, assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures); - List>> attributeFutures = new ArrayList<>(); + List>> attributeFutures = new ArrayList<>(); for (int i = 0; i < devices.size(); i++) { Device device = devices.get(i); attributeFutures.add(saveLongAttribute(device.getId(), "temperature", temperatures.get(i), DataConstants.CLIENT_SCOPE)); } - Futures.successfulAsList(attributeFutures).get(); + Futures.allAsList(attributeFutures).get(); DeviceSearchQueryFilter filter = new DeviceSearchQueryFilter(); filter.setRootEntity(tenantId); @@ -593,12 +593,12 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { List highConsumptions = new ArrayList<>(); createTestHierarchy(tenantId, assets, devices, consumptions, highConsumptions, new ArrayList<>(), new ArrayList<>()); - List>> attributeFutures = new ArrayList<>(); + List>> attributeFutures = new ArrayList<>(); for (int i = 0; i < assets.size(); i++) { Asset asset = assets.get(i); attributeFutures.add(saveLongAttribute(asset.getId(), "consumption", consumptions.get(i), DataConstants.SERVER_SCOPE)); } - Futures.successfulAsList(attributeFutures).get(); + Futures.allAsList(attributeFutures).get(); AssetSearchQueryFilter filter = new AssetSearchQueryFilter(); filter.setRootEntity(tenantId); @@ -1048,14 +1048,14 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { } } - List>> attributeFutures = new ArrayList<>(); + List>> attributeFutures = new ArrayList<>(); for (int i = 0; i < devices.size(); i++) { Device device = devices.get(i); for (String currentScope : DataConstants.allScopes()) { attributeFutures.add(saveLongAttribute(device.getId(), "temperature", temperatures.get(i), currentScope)); } } - Futures.successfulAsList(attributeFutures).get(); + Futures.allAsList(attributeFutures).get(); DeviceTypeFilter filter = new DeviceTypeFilter(); filter.setDeviceType("default"); @@ -1150,12 +1150,12 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { } } - List>> attributeFutures = new ArrayList<>(); + List>> attributeFutures = new ArrayList<>(); for (int i = 0; i < devices.size(); i++) { Device device = devices.get(i); attributeFutures.add(saveLongAttribute(device.getId(), "temperature", temperatures.get(i), DataConstants.CLIENT_SCOPE)); } - Futures.successfulAsList(attributeFutures).get(); + Futures.allAsList(attributeFutures).get(); DeviceTypeFilter filter = new DeviceTypeFilter(); filter.setDeviceType("default"); @@ -1301,7 +1301,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { Device device = devices.get(i); timeseriesFutures.add(saveLongTimeseries(device.getId(), "temperature", temperatures.get(i))); } - Futures.successfulAsList(timeseriesFutures).get(); + Futures.allAsList(timeseriesFutures).get(); DeviceTypeFilter filter = new DeviceTypeFilter(); filter.setDeviceType("default"); @@ -1428,12 +1428,12 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { } } - List>> attributeFutures = new ArrayList<>(); + List>> attributeFutures = new ArrayList<>(); for (int i = 0; i < devices.size(); i++) { Device device = devices.get(i); attributeFutures.add(saveStringAttribute(device.getId(), "attributeString", attributeStrings.get(i), DataConstants.CLIENT_SCOPE)); } - Futures.successfulAsList(attributeFutures).get(); + Futures.allAsList(attributeFutures).get(); DeviceTypeFilter filter = new DeviceTypeFilter(); filter.setDeviceType("default"); @@ -1764,13 +1764,13 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { return filter; } - private ListenableFuture> saveLongAttribute(EntityId entityId, String key, long value, String scope) { + private ListenableFuture> saveLongAttribute(EntityId entityId, String key, long value, String scope) { KvEntry attrValue = new LongDataEntry(key, value); AttributeKvEntry attr = new BaseAttributeKvEntry(attrValue, 42L); return attributesService.save(SYSTEM_TENANT_ID, entityId, scope, Collections.singletonList(attr)); } - private ListenableFuture> saveStringAttribute(EntityId entityId, String key, String value, String scope) { + private ListenableFuture> saveStringAttribute(EntityId entityId, String key, String value, String scope) { KvEntry attrValue = new StringDataEntry(key, value); AttributeKvEntry attr = new BaseAttributeKvEntry(attrValue, 42L); return attributesService.save(SYSTEM_TENANT_ID, entityId, scope, Collections.singletonList(attr)); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java index 1c38a88709..f5752154c9 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java @@ -15,7 +15,6 @@ */ package org.thingsboard.rule.engine.metadata; -import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.json.JsonWriteFeature; import com.fasterxml.jackson.databind.ObjectMapper; @@ -24,6 +23,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.gson.JsonParseException; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.BooleanUtils; import org.thingsboard.rule.engine.api.TbContext; @@ -50,6 +50,7 @@ import static org.thingsboard.server.common.data.DataConstants.LATEST_TS; import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE; import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE; +@Slf4j public abstract class TbAbstractGetAttributesNode implements TbNode { private static ObjectMapper mapper = new ObjectMapper(); @@ -112,6 +113,7 @@ public abstract class TbAbstractGetAttributesNode> attributeKvEntryListFuture = ctx.getAttributesService().find(ctx.getTenantId(), entityId, scope, keys); return Futures.transform(attributeKvEntryListFuture, attributeKvEntryList -> { + log.warn("[{}][{}][{}][{}] Lookup attribute result: {}", ctx.getTenantId(), entityId, scope, keys, attributeKvEntryList); if (!CollectionUtils.isEmpty(attributeKvEntryList)) { List existingAttributesKvEntry = attributeKvEntryList.stream().filter(attributeKvEntry -> keys.contains(attributeKvEntry.getKey())).collect(Collectors.toList()); existingAttributesKvEntry.forEach(kvEntry -> msg.getMetaData().putValue(prefix + kvEntry.getKey(), kvEntry.getValueAsString())); From d9a2495ea43c4bc246dc7a2433d2b7d4d0d1288b Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 29 Apr 2022 17:25:06 +0300 Subject: [PATCH 071/276] Add upgrade script for 2FA --- .../main/data/upgrade/3.3.4/schema_update.sql | 22 +++++++++++++++++++ .../install/ThingsboardInstallService.java | 1 + .../install/SqlDatabaseUpgradeService.java | 14 ++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 application/src/main/data/upgrade/3.3.4/schema_update.sql diff --git a/application/src/main/data/upgrade/3.3.4/schema_update.sql b/application/src/main/data/upgrade/3.3.4/schema_update.sql new file mode 100644 index 0000000000..d2134bdc48 --- /dev/null +++ b/application/src/main/data/upgrade/3.3.4/schema_update.sql @@ -0,0 +1,22 @@ +-- +-- Copyright © 2016-2022 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. +-- + +CREATE TABLE IF NOT EXISTS user_auth_settings ( + id uuid NOT NULL CONSTRAINT user_auth_settings_pkey PRIMARY KEY, + created_time bigint NOT NULL, + user_id uuid UNIQUE NOT NULL CONSTRAINT fk_user_auth_settings_user_id REFERENCES tb_user(id), + mfa_account_config varchar +); diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index ebab403149..e728c004c0 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -219,6 +219,7 @@ public class ThingsboardInstallService { databaseEntitiesUpgradeService.upgradeDatabase("3.3.3"); case "3.3.4": log.info("Upgrading ThingsBoard from version 3.3.4 to 3.4.0 ..."); + databaseEntitiesUpgradeService.upgradeDatabase("3.3.4"); log.info("Updating system data..."); systemDataLoaderService.updateSystemWidgets(); break; diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index f7b9e5cad1..7be899f995 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -534,6 +534,20 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService log.error("Failed updating schema!!!", e); } break; + case "3.3.4": + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + log.info("Updating schema ..."); + + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.3.4", SCHEMA_UPDATE_SQL); + loadSql(schemaUpdateFile, conn); + + log.info("Updating schema settings..."); + conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3004000;"); + log.info("Schema updated."); + } catch (Exception e) { + log.error("Failed updating schema!!!", e); + } + break; default: throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); } From f71b1a63fda8fe2c13c99f037e6c9474139e8409 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 29 Apr 2022 17:34:08 +0300 Subject: [PATCH 072/276] UI: Implement input widgets settings forms --- .../system/widget_bundles/input_widgets.json | 66 ++++--- ...ce-claiming-widget-settings.component.html | 84 +++++++++ ...vice-claiming-widget-settings.component.ts | 110 +++++++++++ ...amera-input-widget-settings.component.html | 59 ++++++ ...-camera-input-widget-settings.component.ts | 67 +++++++ ...-attribute-general-settings.component.html | 47 +++++ ...te-attribute-general-settings.component.ts | 172 ++++++++++++++++++ ...n-attribute-widget-settings.component.html | 42 +++++ ...ean-attribute-widget-settings.component.ts | 58 ++++++ ...e-attribute-widget-settings.component.html | 30 +++ ...ate-attribute-widget-settings.component.ts | 73 ++++++++ ...e-attribute-widget-settings.component.html | 36 ++++ ...ble-attribute-widget-settings.component.ts | 77 ++++++++ ...e-attribute-widget-settings.component.html | 46 +++++ ...age-attribute-widget-settings.component.ts | 62 +++++++ ...r-attribute-widget-settings.component.html | 36 ++++ ...ger-attribute-widget-settings.component.ts | 77 ++++++++ ...n-attribute-widget-settings.component.html | 68 +++++++ ...son-attribute-widget-settings.component.ts | 93 ++++++++++ ...n-attribute-widget-settings.component.html | 84 +++++++++ ...ion-attribute-widget-settings.component.ts | 109 +++++++++++ ...g-attribute-widget-settings.component.html | 36 ++++ ...ing-attribute-widget-settings.component.ts | 77 ++++++++ .../lib/settings/widget-settings.module.ts | 71 +++++++- .../assets/locale/locale.constant-en_US.json | 66 ++++++- 25 files changed, 1720 insertions(+), 26 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/device-claiming-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/device-claiming-widget-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-attribute-general-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-attribute-general-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-boolean-attribute-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-boolean-attribute-widget-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-date-attribute-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-date-attribute-widget-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-double-attribute-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-double-attribute-widget-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-image-attribute-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-image-attribute-widget-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-integer-attribute-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-integer-attribute-widget-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-location-attribute-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-location-attribute-widget-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-string-attribute-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-string-attribute-widget-settings.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/input_widgets.json b/application/src/main/data/json/system/widget_bundles/input_widgets.json index 9def96fcf8..a398c2fc43 100644 --- a/application/src/main/data/json/system/widget_bundles/input_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/input_widgets.json @@ -55,8 +55,9 @@ "templateHtml": "
\n
\n \n {{deviceLabel}}\n \n \n {{requiredErrorDevice}}\n \n \n \n {{secretKeyLabel}}\n \n \n {{requiredErrorSecretKey}}\n \n \n
\n
\n \n
\n
\n", "templateCss": ".claim-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n", "controllerScript": "let $scope;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n}\n\nfunction init() {\n $scope = self.ctx.$scope;\n let $injector = $scope.$injector;\n let utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n let $translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n let deviceService = $scope.$injector.get(self.ctx.servicesMap.get('deviceService'));\n let settings = self.ctx.settings || {};\n \n $scope.toastTargetId = 'device-claiming-widget' + utils.guid();\n $scope.secretKeyField = settings.deviceSecret;\n $scope.showLabel = settings.showLabel;\n\n let titleTemplate = \"\";\n let successfulClaim = utils.customTranslation(settings.successfulClaimDevice, settings.successfulClaimDevice) || $translate.instant('widgets.input-widgets.claim-successful');\n let failedClaimDevice = utils.customTranslation(settings.failedClaimDevice, settings.failedClaimDevice) || $translate.instant('widgets.input-widgets.claim-failed');\n let deviceNotFound = utils.customTranslation(settings.deviceNotFound, settings.deviceNotFound) || $translate.instant('widgets.input-widgets.claim-not-found');\n \n if (settings.widgetTitle && settings.widgetTitle.length) {\n titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n titleTemplate = self.ctx.widgetConfig.title;\n }\n self.ctx.widgetTitle = titleTemplate;\n \n $scope.deviceLabel = utils.customTranslation(settings.deviceLabel, settings.deviceLabel) || $translate.instant('widgets.input-widgets.device-name');\n $scope.requiredErrorDevice= utils.customTranslation(settings.requiredErrorDevice, settings.requiredErrorDevice) || $translate.instant('widgets.input-widgets.device-name-required');\n \n $scope.secretKeyLabel = utils.customTranslation(settings.secretKeyLabel, settings.secretKeyLabel) || $translate.instant('widgets.input-widgets.secret-key');\n $scope.requiredErrorSecretKey= utils.customTranslation(settings.requiredErrorSecretKey, settings.requiredErrorSecretKey) || $translate.instant('widgets.input-widgets.secret-key-required');\n \n $scope.labelClaimButon = utils.customTranslation(settings.labelClaimButon, settings.labelClaimButon) || $translate.instant('widgets.input-widgets.claim-device');\n \n $scope.claimDeviceFormGroup = $scope.fb.group(\n {deviceName: ['', [$scope.validators.required]]}\n );\n if ($scope.secretKeyField) {\n $scope.claimDeviceFormGroup.addControl('deviceSecret', $scope.fb.control('', [$scope.validators.required]));\n }\n \n $scope.claim = function(claimDeviceForm) {\n $scope.loading = true;\n\n let deviceName = $scope.claimDeviceFormGroup.get('deviceName').value;\n let claimRequest = {};\n if ($scope.secretKeyField) {\n claimRequest.secretKey = $scope.claimDeviceFormGroup.get('deviceSecret').value;\n }\n deviceService.claimDevice(deviceName, claimRequest, { ignoreErrors: true }).subscribe(\n function (data) {\n successClaim(claimDeviceForm);\n self.ctx.detectChanges();\n },\n function (error) {\n $scope.loading = false;\n if(error.status == 404) {\n $scope.showErrorToast(deviceNotFound, 'bottom', 'left', $scope.toastTargetId);\n } else {\n let errorMessage = failedClaimDevice;\n if (error.status !== 400) {\n if (error.error && error.error.message) {\n errorMessage = error.error.message;\n }\n }\n $scope.showErrorToast(errorMessage, 'bottom', 'left', $scope.toastTargetId);\n } \n self.ctx.detectChanges();\n }\n );\n }\n\n function successClaim(claimDeviceForm) {\n let deviceObj = {\n deviceName: ''\n };\n if ($scope.secretKeyField) {\n deviceObj.deviceSecret = '';\n } \n claimDeviceForm.resetForm(); \n $scope.claimDeviceFormGroup.reset(deviceObj);\n $scope.loading = false;\n $scope.showSuccessToast(successfulClaim, 2000, 'bottom', 'left', $scope.toastTargetId);\n self.ctx.updateAliases();\n }\n \n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"deviceSecret\": {\n \"title\": \"Show 'Secret key' input field\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"deviceLabel\": {\n \"title\": \"Label for device name\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorDevice\": {\n \"title\": \"'Device name required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"secretKeyLabel\": {\n \"title\": \"Label for secret key\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorSecretKey\": {\n \"title\": \"'Secret key required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"labelClaimButon\": {\n \"title\": \"Label for claiming button\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"successfulClaimDevice\": {\n \"title\": \"Text message of successful device claiming\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"deviceNotFound\": {\n \"title\": \"Text message when device not found\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"failedClaimDevice\": {\n \"title\": \"Text message of failed device claiming\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n [\n \"widgetTitle\",\n \"labelClaimButon\",\n \"deviceSecret\",\n \"showLabel\",\n \"deviceLabel\",\n \"secretKeyLabel\"\n ],\n [\n \"deviceNotFound\",\n \"failedClaimDevice\",\n \"successfulClaimDevice\",\n \"requiredErrorDevice\",\n \"requiredErrorSecretKey\"\n ]\n ],\n \"groupInfoes\": [{\n \"formIndex\": 0,\n \"GroupTitle\": \"General settings\"\n }, {\n \"formIndex\": 1,\n \"GroupTitle\": \"Message settings\"\n }]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-device-claiming-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"deviceSecret\":true,\"showLabel\":true},\"title\":\"Device claiming widget\",\"dropShadow\":true,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"enableDataExport\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -91,8 +92,9 @@ "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n\n
\n \n \n
\n
\n\n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.attribute-not-allowed' | translate }}\n
\n
\n
\n
", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}\n", "controllerScript": "let $scope;\nlet settings;\nlet utils;\nlet translate;\nlet http;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n http = $scope.$injector.get(self.ctx.servicesMap.get('http'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-timeseries-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n\n $scope.attributeUpdateFormGroup = $scope.fb.group(\n {currentValue: [undefined, [$scope.validators.required,\n $scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue),\n $scope.validators.pattern(/^-?[0-9]+$/)]]}\n );\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"timeseries\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n let observable = saveEntityTimeseries(\n datasource.entityType,\n datasource.entityId,\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n );\n if (observable) {\n observable.subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n\n function saveEntityTimeseries(entityType, entityId, telemetries) {\n var telemetriesData = {};\n for (var a = 0; a < telemetries.length; a++) {\n if (typeof telemetries[a].value !== 'undefined' && telemetries[a].value !== null) {\n telemetriesData[telemetries[a].key] = telemetries[a].value;\n }\n }\n if (Object.keys(telemetriesData).length) {\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n return http.post(url, telemetriesData);\n }\n return null;\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(correctValue($scope.originalValue));\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n return 0;\n }\n return value;\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n dataKeyOptional: true\n }\n}\n\nself.onDestroy = function() {\n\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValue\": {\n \"title\": \"Max value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minValue\": {\n \"title\": \"Min value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxValue\",\n \"minValue\"\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-integer-attribute-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update integer timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -109,8 +111,9 @@ "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.attribute-not-allowed' | translate }}\n
\n
\n
\n
", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", "controllerScript": "let $scope;\nlet settings;\nlet utils;\nlet translate;\nlet http;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n http = $scope.$injector.get(self.ctx.servicesMap.get('http'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-timeseries-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n\n $scope.attributeUpdateFormGroup = $scope.fb.group(\n {currentValue: [undefined, [$scope.validators.required,\n $scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue)]]}\n );\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"timeseries\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n let observable = saveEntityTimeseries(\n datasource.entityType,\n datasource.entityId,\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n );\n if (observable) {\n observable.subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n\n function saveEntityTimeseries(entityType, entityId, telemetries) {\n var telemetriesData = {};\n for (var a = 0; a < telemetries.length; a++) {\n if (typeof telemetries[a].value !== 'undefined' && telemetries[a].value !== null) {\n telemetriesData[telemetries[a].key] = telemetries[a].value;\n }\n }\n if (Object.keys(telemetriesData).length) {\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n return http.post(url, telemetriesData);\n }\n return null;\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(correctValue($scope.originalValue));\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n return 0;\n }\n return value;\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n dataKeyOptional: true\n }\n}\n\nself.onDestroy = function() {\n\n}", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValue\": {\n \"title\": \"Max value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minValue\": {\n \"title\": \"Min value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxValue\",\n \"minValue\"\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-double-attribute-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update double timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -127,8 +130,9 @@ "templateHtml": "
\n
\n
\n
\n
\n \n {{currentValue}}\n \n
\n
\n\n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.attribute-not-allowed' | translate }}\n
\n
\n
\n
", "templateCss": ".attribute-update-form {\r\n overflow: hidden;\r\n height: 100%;\r\n display: flex;\r\n flex-direction: column;\r\n}\r\n\r\n.attribute-update-form__grid {\r\n display: flex;\r\n}\r\n.grid__element:first-child {\r\n flex: 1;\r\n}\r\n\r\n.grid__element {\r\n display: flex;\r\n}\r\n\r\n.attribute-update-form .mat-button.mat-icon-button {\r\n width: 32px;\r\n min-width: 32px;\r\n height: 32px;\r\n min-height: 32px;\r\n padding: 0 !important;\r\n margin: 0 !important;\r\n line-height: 20px;\r\n}\r\n\r\n.attribute-update-form .mat-icon-button mat-icon {\r\n width: 20px;\r\n min-width: 20px;\r\n height: 20px;\r\n min-height: 20px;\r\n font-size: 20px;\r\n}\r\n\r\n.tb-toast {\r\n font-size: 14px!important;\r\n}", "controllerScript": "let settings;\nlet utils;\nlet translate;\nlet http;\nlet $scope;\nlet map;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init();\n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n http = $scope.$injector.get(self.ctx.servicesMap.get('http'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n\n settings.trueValue = utils.defaultValue(utils.customTranslation(settings.trueValue, settings.trueValue), true);\n settings.falseValue = utils.defaultValue(utils.customTranslation(settings.falseValue, settings.falseValue), false);\n\n map = {\n true: settings.trueValue,\n false: settings.falseValue\n };\n \n $scope.checkboxValue = false;\n $scope.currentValue = map[$scope.checkboxValue];\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({checkboxValue: [$scope.checkboxValue]});\n\n $scope.changed = function() {\n $scope.checkboxValue = $scope.attributeUpdateFormGroup.get('checkboxValue').value;\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.updateAttribute();\n };\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"timeseries\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function() {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n let observable = saveEntityTimeseries(\n datasource.entityType,\n datasource.entityId,\n [{\n key: $scope.currentKey,\n value: $scope.checkboxValue\n }]\n );\n\n if (observable) {\n observable.subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('checkboxValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n }\n };\n\n function saveEntityTimeseries(entityType, entityId, telemetries) {\n var telemetriesData = {};\n for (var a = 0; a < telemetries.length; a++) {\n if (typeof telemetries[a].value !== 'undefined' && telemetries[a].value !== null) {\n telemetriesData[telemetries[a].key] = telemetries[a].value;\n }\n }\n if (Object.keys(telemetriesData).length) {\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n return http.post(url, telemetriesData);\n }\n return null;\n }\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n $scope.checkboxValue = self.ctx.data[0].data[0][1] === 'true';\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.attributeUpdateFormGroup.get('checkboxValue').patchValue($scope.checkboxValue);\n self.ctx.detectChanges();\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {}", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"trueValue\": {\n \"title\": \"True value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"falseValue\": {\n \"title\": \"False value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"trueValue\",\n \"falseValue\"\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-boolean-attribute-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update boolean timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -145,8 +149,9 @@ "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.attribute-not-allowed' | translate }}\n
\n
\n
\n
", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", "controllerScript": "let $scope;\nlet settings;\nlet utils;\nlet translate;\nlet http;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n http = $scope.$injector.get(self.ctx.servicesMap.get('http'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-timeseries-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n\n $scope.attributeUpdateFormGroup = $scope.fb.group(\n {currentValue: [undefined, [$scope.validators.required,\n $scope.validators.minLength(settings.minLength),\n $scope.validators.maxLength(settings.maxLength)]]}\n );\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"timeseries\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n let observable = saveEntityTimeseries(\n datasource.entityType,\n datasource.entityId,\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n );\n if (observable) {\n observable.subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n\n function saveEntityTimeseries(entityType, entityId, telemetries) {\n var telemetriesData = {};\n for (var a = 0; a < telemetries.length; a++) {\n if (typeof telemetries[a].value !== 'undefined' && telemetries[a].value !== null) {\n telemetriesData[telemetries[a].key] = telemetries[a].value;\n }\n }\n if (Object.keys(telemetriesData).length) {\n var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n return http.post(url, telemetriesData);\n }\n return null;\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxLength\": {\n \"title\": \"Max length\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minLength\": {\n \"title\": \"Min length\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxLength\",\n \"minLength\"\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-string-attribute-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update string timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -163,8 +168,9 @@ "templateHtml": "
\n
\n
\n
\n
\n \n \n
\n \n
\n \n \n
\n
\n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.tb-image-preview-container div,\n.tb-flow-drop label {\n font-size: 16px !important;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\nfunction init() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n settings.displayPreview = utils.defaultValue(settings.displayPreview, true);\r\n settings.displayClearButton = utils.defaultValue(settings.displayClearButton, false);\r\n settings.displayApplyButton = utils.defaultValue(settings.displayApplyButton, true);\r\n settings.displayDiscardButton = utils.defaultValue(settings.displayDiscardButton, true);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.entityDetected = false;\r\n \r\n $scope.attributeUpdateFormGroup = $scope.fb.group(\r\n {currentValue: [undefined, []]}\r\n );\r\n \r\n $scope.attributeUpdateFormGroup.valueChanges.subscribe( () => {\r\n self.ctx.detectChanges();\r\n if (!settings.displayApplyButton) {\r\n $scope.updateAttribute();\r\n }\r\n });\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n \r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type !== \"attribute\") {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n \r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entity.id,\r\n 'SERVER_SCOPE',\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n if (settings.displayApplyButton) {\r\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\r\n }\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n var value = self.ctx.data[0].data[0][1];\r\n if (settings.displayApplyButton || !$scope.originalValue) {\r\n $scope.originalValue = value;\r\n }\r\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(value, {emitEvent: false});\r\n self.ctx.detectChanges();\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nself.onResize = function() {\r\n\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n $scope.attributeUpdateFormGroup.valueChanges.unsubscribe();\r\n}", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"UpdateImageAttributeSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"displayPreview\":{\n \"title\":\"Display preview\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"displayClearButton\":{\n \"title\":\"Display clear button\",\n \"type\":\"boolean\",\n \"default\":false\n },\n \"displayApplyButton\":{\n \"title\":\"Display apply button\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"displayDiscardButton\":{\n \"title\":\"Display discard button\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"displayPreview\",\n \"displayClearButton\",\n \"displayApplyButton\",\n \"displayDiscardButton\"\n \n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-image-attribute-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showResultMessage\":true,\"displayPreview\":true,\"displayClearButton\":false,\"displayApplyButton\":true,\"displayDiscardButton\":true},\"title\":\"Update server image attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -181,8 +187,9 @@ "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n \n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\nfunction init() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.entityDetected = false;\r\n\r\n $scope.datePickerType = settings.showTimeInput ? 'datetime' : 'date'; \r\n \r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\r\n $scope.labelValue = translate.instant('widgets.input-widgets.date');\r\n \r\n if (settings.showTimeInput) {\r\n $scope.labelValue += \" & \" + translate.instant('widgets.input-widgets.time');\r\n }\r\n \r\n var validators = [];\r\n \r\n if (settings.isRequired) {\r\n validators.push($scope.validators.required);\r\n }\r\n \r\n $scope.attributeUpdateFormGroup = $scope.fb.group(\r\n {currentValue: [undefined, validators]}\r\n );\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n \r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type !== \"attribute\") {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n \r\n $scope.clear = function(event) {\r\n event.stopPropagation();\r\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(undefined);\r\n }\r\n \r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n var currentValueInMilliseconds;\r\n \r\n if (!$scope.attributeUpdateFormGroup.get('currentValue').value) {\r\n currentValueInMilliseconds = undefined;\r\n } else {\r\n currentValueInMilliseconds = $scope.attributeUpdateFormGroup.get('currentValue').value.getTime();\r\n }\r\n \r\n attributeService.saveEntityAttributes(\r\n datasource.entity.id,\r\n 'SERVER_SCOPE',\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: currentValueInMilliseconds\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n \r\n $scope.isValidDate = function(date) {\r\n return date instanceof Date && !isNaN(date);\r\n }\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n $scope.originalValue = moment(self.ctx.data[0].data[0][1]).toDate();\r\n\r\n if (!$scope.isValidDate($scope.originalValue)) {\r\n $scope.originalValue = undefined;\r\n }\r\n \r\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\r\n self.ctx.detectChanges();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nself.onResize = function() {\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n}\r\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"UpdateDateAttributeSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"showTimeInput\":{\n \"title\":\"Show select time\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"inputFieldsAlignment\": {\n \"title\": \"Input fields alignment\",\n \"type\": \"string\",\n \"default\": \"column\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"isRequired\":{\n \"title\":\"Required\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"isRequired\",\n \"showLabel\",\n \"showTimeInput\",\n \"requiredErrorMessage\"\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-date-attribute-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update server date attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -199,8 +206,9 @@ "templateHtml": "
\r\n
\r\n
\r\n
\r\n
\r\n \r\n {{currentValue}}\r\n \r\n
\r\n
\r\n\r\n
\r\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\r\n
\r\n
\r\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\r\n
\r\n
\r\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\r\n
\r\n
\r\n
\r\n
", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", "controllerScript": "let settings;\nlet attributeService;\nlet utils;\nlet translate;\nlet $scope;\nlet map;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init();\n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n\n settings.trueValue = utils.defaultValue(utils.customTranslation(settings.trueValue, settings.trueValue), true);\n settings.falseValue = utils.defaultValue(utils.customTranslation(settings.falseValue, settings.falseValue), false);\n\n map = {\n true: settings.trueValue,\n false: settings.falseValue\n };\n \n $scope.checkboxValue = false;\n $scope.currentValue = map[$scope.checkboxValue];\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({checkboxValue: [$scope.checkboxValue]});\n\n $scope.changed = function() {\n $scope.checkboxValue = $scope.attributeUpdateFormGroup.get('checkboxValue').value;\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.updateAttribute();\n };\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function() {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SERVER_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.checkboxValue\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('checkboxValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n $scope.checkboxValue = self.ctx.data[0].data[0][1] === 'true';\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.attributeUpdateFormGroup.get('checkboxValue').patchValue($scope.checkboxValue);\n self.ctx.detectChanges();\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {}", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"trueValue\": {\n \"title\": \"True value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"falseValue\": {\n \"title\": \"False value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"trueValue\",\n \"falseValue\"\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-boolean-attribute-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update server boolean attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -217,8 +225,9 @@ "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false; \n\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n \n var validators = [$scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue)];\n \n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n \n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\n });\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SERVER_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(correctValue($scope.originalValue));\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n return 0;\n }\n return value;\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValue\": {\n \"title\": \"Max value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minValue\": {\n \"title\": \"Min value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"isRequired\":{\n \"title\":\"Required\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"isRequired\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxValue\",\n \"minValue\"\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-double-attribute-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showResultMessage\":true,\"showLabel\":true,\"isRequired\":true},\"title\":\"Update server double attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -235,8 +244,9 @@ "templateHtml": "
\n
\n
\n
\n
\n \n \n
\n \n
\n \n \n
\n
\n \n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.tb-image-preview-container div,\n.tb-flow-drop label {\n font-size: 16px !important;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\nfunction init() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n settings.displayPreview = utils.defaultValue(settings.displayPreview, true);\r\n settings.displayClearButton = utils.defaultValue(settings.displayClearButton, false);\r\n settings.displayApplyButton = utils.defaultValue(settings.displayApplyButton, true);\r\n settings.displayDiscardButton = utils.defaultValue(settings.displayDiscardButton, true);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.entityDetected = false;\r\n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\r\n \r\n $scope.attributeUpdateFormGroup = $scope.fb.group(\r\n {currentValue: [undefined, []]}\r\n );\r\n \r\n $scope.attributeUpdateFormGroup.valueChanges.subscribe( () => {\r\n self.ctx.detectChanges();\r\n if (!settings.displayApplyButton) {\r\n $scope.updateAttribute();\r\n }\r\n });\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n \r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType === 'DEVICE') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n \r\n $scope.entityDetected = true;\r\n }\r\n } else {\r\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type !== \"attribute\") {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n \r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entity.id,\r\n 'SHARED_SCOPE',\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n if (settings.displayApplyButton) {\r\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\r\n }\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n var value = self.ctx.data[0].data[0][1];\r\n if (settings.displayApplyButton || !$scope.originalValue) {\r\n $scope.originalValue = value;\r\n }\r\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(value, {emitEvent: false});\r\n self.ctx.detectChanges();\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nself.onResize = function() {\r\n\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n $scope.attributeUpdateFormGroup.valueChanges.unsubscribe();\r\n}", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"UpdateImageAttributeSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"displayPreview\":{\n \"title\":\"Display preview\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"displayClearButton\":{\n \"title\":\"Display clear button\",\n \"type\":\"boolean\",\n \"default\":false\n },\n \"displayApplyButton\":{\n \"title\":\"Display apply button\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"displayDiscardButton\":{\n \"title\":\"Display discard button\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"displayPreview\",\n \"displayClearButton\",\n \"displayApplyButton\",\n \"displayDiscardButton\"\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-image-attribute-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showResultMessage\":true,\"displayPreview\":true,\"displayClearButton\":false,\"displayApplyButton\":true,\"displayDiscardButton\":true},\"title\":\"Update shared image attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -253,8 +263,9 @@ "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n \n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\nfunction init() {\r\n\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.entityDetected = false;\r\n\r\n $scope.datePickerType = settings.showTimeInput ? 'datetime' : 'date'; \r\n \r\n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\r\n $scope.labelValue = translate.instant('widgets.input-widgets.date');\r\n \r\n \r\n if (settings.showTimeInput) {\r\n $scope.labelValue += \" & \" + translate.instant('widgets.input-widgets.time');\r\n }\r\n \r\n var validators = [];\r\n \r\n if (settings.isRequired) {\r\n validators.push($scope.validators.required);\r\n }\r\n \r\n $scope.attributeUpdateFormGroup = $scope.fb.group(\r\n {currentValue: [undefined, validators]}\r\n );\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n \r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType === 'DEVICE') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n \r\n $scope.entityDetected = true;\r\n }\r\n } else {\r\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\r\n }\r\n }\r\n if (datasource.dataKeys.length) {\r\n if (datasource.dataKeys[0].type !== \"attribute\") {\r\n $scope.isValidParameter = false;\r\n } else {\r\n $scope.currentKey = datasource.dataKeys[0].name;\r\n $scope.dataKeyType = datasource.dataKeys[0].type;\r\n $scope.dataKeyDetected = true;\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n \r\n $scope.clear = function(event) {\r\n event.stopPropagation();\r\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(undefined);\r\n }\r\n \r\n $scope.updateAttribute = function () {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n var currentValueInMilliseconds;\r\n \r\n if (!$scope.attributeUpdateFormGroup.get('currentValue').value) {\r\n currentValueInMilliseconds = undefined;\r\n } else {\r\n currentValueInMilliseconds = $scope.attributeUpdateFormGroup.get('currentValue').value.getTime();\r\n }\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entity.id,\r\n 'SHARED_SCOPE',\r\n [\r\n {\r\n key: $scope.currentKey,\r\n value: currentValueInMilliseconds\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n \r\n $scope.isValidDate = function(date) {\r\n return date instanceof Date && !isNaN(date);\r\n }\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n $scope.originalValue = moment(self.ctx.data[0].data[0][1]).toDate();\r\n \r\n if (!$scope.isValidDate($scope.originalValue)) {\r\n $scope.originalValue = undefined;\r\n }\r\n \r\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\r\n self.ctx.detectChanges();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nself.onResize = function() {\r\n\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 1,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n\r\n}\r\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"UpdateDateAttributeSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"showTimeInput\":{\n \"title\":\"Show select time\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"inputFieldsAlignment\": {\n \"title\": \"Input fields alignment\",\n \"type\": \"string\",\n \"default\": \"column\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"isRequired\":{\n \"title\":\"Required\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"isRequired\",\n \"showLabel\",\n \"showTimeInput\",\n \"requiredErrorMessage\"\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-date-attribute-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showResultMessage\":true,\"isRequired\":true,\"showLabel\":true,\"showTimeInput\":true},\"title\":\"Update shared date attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -271,8 +282,9 @@ "templateHtml": "
\r\n
\r\n
\r\n
\r\n
\r\n \r\n {{currentValue}}\r\n \r\n
\r\n
\r\n\r\n
\r\n
\r\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\r\n
\r\n
\r\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\r\n
\r\n
\r\n
\r\n
", "templateCss": ".attribute-update-form {\r\n overflow: hidden;\r\n height: 100%;\r\n display: flex;\r\n flex-direction: column;\r\n}\r\n\r\n.attribute-update-form__grid {\r\n display: flex;\r\n}\r\n.grid__element:first-child {\r\n flex: 1;\r\n}\r\n\r\n.grid__element {\r\n display: flex;\r\n}\r\n\r\n.attribute-update-form .mat-button.mat-icon-button {\r\n width: 32px;\r\n min-width: 32px;\r\n height: 32px;\r\n min-height: 32px;\r\n padding: 0 !important;\r\n margin: 0 !important;\r\n line-height: 20px;\r\n}\r\n\r\n.attribute-update-form .mat-icon-button mat-icon {\r\n width: 20px;\r\n min-width: 20px;\r\n height: 20px;\r\n min-height: 20px;\r\n font-size: 20px;\r\n}\r\n\r\n.tb-toast {\r\n font-size: 14px!important;\r\n}", "controllerScript": "let settings;\nlet attributeService;\nlet utils;\nlet translate;\nlet $scope;\nlet map;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init();\n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\n\n settings.trueValue = utils.defaultValue(utils.customTranslation(settings.trueValue, settings.trueValue), true);\n settings.falseValue = utils.defaultValue(utils.customTranslation(settings.falseValue, settings.falseValue), false);\n\n map = {\n true: settings.trueValue,\n false: settings.falseValue\n };\n \n $scope.checkboxValue = false;\n $scope.currentValue = map[$scope.checkboxValue];\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({checkboxValue: [$scope.checkboxValue]});\n\n $scope.changed = function() {\n $scope.checkboxValue = $scope.attributeUpdateFormGroup.get('checkboxValue').value;\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.updateAttribute();\n };\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType === 'DEVICE') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n \n $scope.entityDetected = true;\n }\n } else {\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function() {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SHARED_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.checkboxValue || false\n }\n ]\n ).subscribe(\n function success() {\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n $scope.checkboxValue = self.ctx.data[0].data[0][1] === 'true';\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.attributeUpdateFormGroup.get('checkboxValue').patchValue($scope.checkboxValue);\n self.ctx.detectChanges();\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {}", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"trueValue\": {\n \"title\": \"True value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"falseValue\": {\n \"title\": \"False value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"trueValue\",\n \"falseValue\"\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-boolean-attribute-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update shared boolean attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -289,8 +301,9 @@ "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false; \n\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n \n var validators = [\n $scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue),\n $scope.validators.pattern(/^-?[0-9]+$/)\n ];\n \n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\n });\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n \n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SERVER_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(correctValue($scope.originalValue));\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n return 0;\n }\n return value;\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValue\": {\n \"title\": \"Max value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minValue\": {\n \"title\": \"Min value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"isRequired\":{\n \"title\":\"Required\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"isRequired\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxValue\",\n \"minValue\"\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-integer-attribute-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update server integer attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -307,8 +320,9 @@ "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n \n var validators = [$scope.validators.minLength(settings.minLength),\n $scope.validators.maxLength(settings.maxLength)];\n \n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n \n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\n });\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n \n var value = $scope.attributeUpdateFormGroup.get('currentValue').value;\n \n if (!$scope.attributeUpdateFormGroup.get('currentValue').value.length) {\n value = null;\n }\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SERVER_SCOPE',\n [\n {\n key: $scope.currentKey,\n value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxLength\": {\n \"title\": \"Max length\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minLength\": {\n \"title\": \"Min length\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"isRequired\":{\n \"title\":\"Required\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"isRequired\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxLength\",\n \"minLength\"\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-string-attribute-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showResultMessage\":true,\"showLabel\":true,\"isRequired\":true},\"title\":\"Update server string attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -325,8 +339,9 @@ "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? latLabel : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n \n \n {{ settings.showLabel ? lngLabel : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-timeseries-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-coordinate-specified' | translate }}\n
\n
\n
\n
", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex-direction: column;\n flex: 1;\n}\n\n.grid__element.horizontal-alignment {\n flex-direction: row;\n}\n\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-button.getLocation {\n margin-right: 10px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.attribute-update-form mat-form-field{\n width: 100%;\n padding-right: 5px;\n}\n\n.attribute-update-form.small-width mat-form-field{\n width: 150px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\n\r\nfunction init() {\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n \r\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n settings.showGetLocation = utils.defaultValue(settings.showGetLocation, true);\r\n settings.enableHighAccuracy = utils.defaultValue(settings.enableHighAccuracy, false);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false; \r\n\r\n $scope.isHorizontal = (settings.inputFieldsAlignment === 'row');\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-coordinate-required');\r\n $scope.latLabel = utils.customTranslation(settings.latLabel, settings.latLabel) || translate.instant('widgets.input-widgets.latitude');\r\n $scope.lngLabel = utils.customTranslation(settings.lngLabel, settings.lngLabel) || translate.instant('widgets.input-widgets.longitude');\r\n\r\n $scope.attributeUpdateFormGroup = $scope.fb.group(\r\n {currentLat: [undefined, [$scope.validators.required,\r\n $scope.validators.min(-90),\r\n $scope.validators.max(90)]],\r\n currentLng: [undefined, [$scope.validators.required,\r\n $scope.validators.min(-180),\r\n $scope.validators.max(180)]]}\r\n );\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length > 1) {\r\n $scope.dataKeyDetected = true;\r\n for (let i = 0; i < datasource.dataKeys.length; i++) {\r\n if (datasource.dataKeys[i].type != \"timeseries\"){\r\n $scope.isValidParameter = false;\r\n }\r\n if (datasource.dataKeys[i].name !== settings.latKeyName && datasource.dataKeys[i].name !== settings.lngKeyName){\r\n $scope.dataKeyDetected = false;\r\n }\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function () {\r\n $scope.isFocused = false;\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityTimeseries(\r\n datasource.entity.id,\r\n 'scope',\r\n [\r\n {\r\n key: settings.latKeyName,\r\n value: $scope.attributeUpdateFormGroup.get('currentLat').value\r\n },{\r\n key: settings.lngKeyName,\r\n value: $scope.attributeUpdateFormGroup.get('currentLng').value\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n $scope.originalLat = $scope.attributeUpdateFormGroup.get('currentLat').value;\r\n $scope.originalLng = $scope.attributeUpdateFormGroup.get('currentLng').value;\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function () {\r\n if ($scope.attributeUpdateFormGroup.get('currentLat').value === $scope.originalLat && $scope.attributeUpdateFormGroup.get('currentLng').value === $scope.originalLng) {\r\n $scope.isFocused = false;\r\n }\r\n };\r\n \r\n $scope.discardChange = function() {\r\n $scope.attributeUpdateFormGroup.setValue({\r\n 'currentLat': $scope.originalLat,\r\n 'currentLng': $scope.originalLng\r\n });\r\n $scope.isFocused = false;\r\n $scope.attributeUpdateFormGroup.markAsPristine();\r\n self.onDataUpdated();\r\n };\r\n \r\n $scope.disableButton = function () {\r\n return $scope.attributeUpdateFormGroup.get('currentLat').value === $scope.originalLat && $scope.attributeUpdateFormGroup.get('currentLng').value === $scope.originalLng || $scope.currentLng === null || $scope.currentLat === null;\r\n };\r\n \r\n $scope.getCoordinate = function() {\r\n if (navigator.geolocation) {\r\n navigator.geolocation.getCurrentPosition(showPosition, function (){\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.blocked-location'), \r\n 'bottom', 'left', $scope.toastTargetId);\r\n }, {\r\n enableHighAccuracy: settings.enableHighAccuracy\r\n });\r\n } else {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.no-support-geolocation'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n };\r\n \r\n function showPosition(position) {\r\n $scope.attributeUpdateFormGroup.setValue({\r\n currentLat: correctValue(position.coords.latitude),\r\n currentLng: correctValue(position.coords.longitude)\r\n });\r\n $scope.attributeUpdateFormGroup.markAsDirty();\r\n $scope.isFocused = true;\r\n }\r\n \r\n self.onResize();\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n for(let i = 0; i < self.typeParameters().maxDataKeys; i++){\r\n if(self.ctx.data[i].dataKey.name === self.ctx.settings.latKeyName && $scope.attributeUpdateFormGroup.get('currentLat').pristine){\r\n $scope.originalLat = self.ctx.data[i].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentLat').patchValue(correctValue($scope.originalLat));\r\n } else if(self.ctx.data[i].dataKey.name === self.ctx.settings.lngKeyName && $scope.attributeUpdateFormGroup.get('currentLng').pristine){\r\n $scope.originalLng = self.ctx.data[i].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentLng').patchValue(correctValue($scope.originalLng));\r\n }\r\n }\r\n self.ctx.detectChanges();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nfunction correctValue(value) {\r\n if (typeof value !== \"number\") {\r\n return 0;\r\n }\r\n return value;\r\n}\r\n\r\nself.onResize = function() {\r\n $scope.smallWidthContainer = (self.ctx.$container && self.ctx.$container[0].offsetWidth < 320);\r\n $scope.changeAlignment = ($scope.isHorizontal && self.ctx.$container && self.ctx.$container[0].offsetWidth < 480);\r\n self.ctx.detectChanges();\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 2,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n\r\n};", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"latitude\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"longitude\"\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"latLabel\": {\n \"title\": \"Label for latitude\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"lngLabel\": {\n \"title\": \"Label for longitude\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\": {\n \"title\": \"Show result message\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableHighAccuracy\": {\n \"title\": \"Use high accuracy\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showGetLocation\": {\n \"title\": \"Show button 'Get current location'\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"inputFieldsAlignment\": {\n \"title\": \"Input fields alignment\",\n \"type\": \"string\",\n \"default\": \"column\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"latKeyName\",\n \"lngKeyName\",\n \"enableHighAccuracy\",\n \"showGetLocation\",\n \"showResultMessage\",\n {\n \"key\": \"inputFieldsAlignment\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"column\",\n \"label\": \"Column (default)\"\n },\n {\n \"value\": \"row\",\n \"label\": \"Row\"\n }\n ]\n },\n \"showLabel\",\n \"latLabel\",\n \"lngLabel\",\n \"requiredErrorMessage\"\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-location-attribute-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update location timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -343,8 +358,9 @@ "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false; \n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\n \n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n \n var validators = [\n $scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue)\n ];\n \n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\n \n });\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType === 'DEVICE') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n \n $scope.entityDetected = true;\n }\n } else {\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SHARED_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(correctValue($scope.originalValue));\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n return 0;\n }\n return value;\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValue\": {\n \"title\": \"Max value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minValue\": {\n \"title\": \"Min value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"isRequired\":{\n \"title\":\"Required\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"isRequired\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxValue\",\n \"minValue\"\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-double-attribute-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update shared double attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -361,8 +377,9 @@ "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false; \n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\n \n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n \n var validators = [\n $scope.validators.min(settings.minValue),\n $scope.validators.max(settings.maxValue),\n $scope.validators.pattern(/^-?[0-9]+$/)\n ];\n \n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\n });\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType === 'DEVICE') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n \n $scope.entityDetected = true;\n }\n } else {\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SHARED_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.attributeUpdateFormGroup.get('currentValue').value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue(correctValue($scope.originalValue));\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nfunction correctValue(value) {\n if (typeof value !== \"number\") {\n return 0;\n }\n return value;\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValue\": {\n \"title\": \"Max value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minValue\": {\n \"title\": \"Min value\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"isRequired\":{\n \"title\":\"Required\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"isRequired\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxValue\",\n \"minValue\"\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-integer-attribute-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update shared integer attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -379,8 +396,9 @@ "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false; \n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\n \n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n \n var validators = [$scope.validators.minLength(settings.minLength),\n $scope.validators.maxLength(settings.maxLength)];\n \n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n \n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\n });\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType === 'DEVICE') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n \n $scope.entityDetected = true;\n }\n } else {\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n var value = $scope.attributeUpdateFormGroup.get('currentValue').value;\n \n if (!$scope.attributeUpdateFormGroup.get('currentValue').value.length) {\n value = null;\n }\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SHARED_SCOPE',\n [\n {\n key: $scope.currentKey,\n value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxLength\": {\n \"title\": \"Max length\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minLength\": {\n \"title\": \"Min length\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"isRequired\":{\n \"title\":\"Required\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"isRequired\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxLength\",\n \"minLength\"\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-string-attribute-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update shared string attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -397,8 +415,9 @@ "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? latLabel : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n \n \n {{ settings.showLabel ? lngLabel : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-coordinate-specified' | translate }}\n
\n
\n
\n
", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex-direction: column;\n flex: 1;\n}\n\n.grid__element.horizontal-alignment {\n flex-direction: row;\n}\n\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-button.getLocation {\n margin-right: 10px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.attribute-update-form mat-form-field{\n width: 100%;\n padding-right: 5px;\n}\n\n.attribute-update-form.small-width mat-form-field{\n width: 150px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\nfunction init() {\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n \r\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n settings.showGetLocation = utils.defaultValue(settings.showGetLocation, true);\r\n settings.enableHighAccuracy = utils.defaultValue(settings.enableHighAccuracy, false);\r\n settings.isLatRequired = utils.defaultValue(settings.isLatRequired, true);\r\n settings.isLngRequired = utils.defaultValue(settings.isLngRequired, true);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false; \r\n\r\n $scope.isHorizontal = (settings.inputFieldsAlignment === 'row');\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-coordinate-required');\r\n $scope.latLabel = utils.customTranslation(settings.latLabel, settings.latLabel) || translate.instant('widgets.input-widgets.latitude');\r\n $scope.lngLabel = utils.customTranslation(settings.lngLabel, settings.lngLabel) || translate.instant('widgets.input-widgets.longitude');\r\n \r\n var validatorsLat = [$scope.validators.min(-90), $scope.validators.max(90)];\r\n var validatorsLng = [$scope.validators.min(-180), $scope.validators.max(180)];\r\n \r\n if (settings.isLatRequired) {\r\n validatorsLat.push($scope.validators.required);\r\n }\r\n \r\n if (settings.isLngRequired) {\r\n validatorsLng.push($scope.validators.required);\r\n }\r\n\r\n $scope.attributeUpdateFormGroup = $scope.fb.group({\r\n currentLat: [undefined, validatorsLat],\r\n currentLng: [undefined, validatorsLng]\r\n });\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length > 1) {\r\n $scope.dataKeyDetected = true;\r\n for (let i = 0; i < datasource.dataKeys.length; i++) {\r\n if (datasource.dataKeys[i].type != \"attribute\") {\r\n $scope.isValidParameter = false;\r\n }\r\n if (datasource.dataKeys[i].name !== settings.latKeyName && datasource.dataKeys[i].name !== settings.lngKeyName){\r\n $scope.dataKeyDetected = false;\r\n }\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function () {\r\n $scope.isFocused = false;\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entity.id,\r\n 'SERVER_SCOPE',\r\n [\r\n {\r\n key: settings.latKeyName,\r\n value: $scope.attributeUpdateFormGroup.get('currentLat').value\r\n },{\r\n key: settings.lngKeyName,\r\n value: $scope.attributeUpdateFormGroup.get('currentLng').value\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n $scope.originalLat = $scope.attributeUpdateFormGroup.get('currentLat').value;\r\n $scope.originalLng = $scope.attributeUpdateFormGroup.get('currentLng').value;\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function () {\r\n if ($scope.attributeUpdateFormGroup.get('currentLat').value === $scope.originalLat && $scope.attributeUpdateFormGroup.get('currentLng').value === $scope.originalLng) {\r\n $scope.isFocused = false;\r\n }\r\n };\r\n \r\n $scope.discardChange = function() {\r\n $scope.attributeUpdateFormGroup.setValue({\r\n 'currentLat': $scope.originalLat,\r\n 'currentLng': $scope.originalLng\r\n });\r\n $scope.isFocused = false;\r\n $scope.attributeUpdateFormGroup.markAsPristine();\r\n self.onDataUpdated();\r\n };\r\n \r\n $scope.disableButton = function () {\r\n return $scope.attributeUpdateFormGroup.get('currentLat').value === $scope.originalLat && $scope.attributeUpdateFormGroup.get('currentLng').value === $scope.originalLng || $scope.currentLng === null || $scope.currentLat === null;\r\n };\r\n \r\n $scope.getCoordinate = function() {\r\n if (navigator.geolocation) {\r\n navigator.geolocation.getCurrentPosition(showPosition, function (){\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.blocked-location'), \r\n 'bottom', 'left', $scope.toastTargetId);\r\n }, {\r\n enableHighAccuracy: settings.enableHighAccuracy\r\n });\r\n } else {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.no-support-geolocation'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n };\r\n \r\n function showPosition(position) {\r\n $scope.attributeUpdateFormGroup.setValue({\r\n currentLat: correctValue(position.coords.latitude),\r\n currentLng: correctValue(position.coords.longitude)\r\n });\r\n $scope.attributeUpdateFormGroup.markAsDirty();\r\n $scope.isFocused = true;\r\n }\r\n \r\n self.onResize();\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n for(let i = 0; i < self.typeParameters().maxDataKeys; i++){\r\n if(self.ctx.data[i].dataKey.name === self.ctx.settings.latKeyName && $scope.attributeUpdateFormGroup.get('currentLat').pristine){\r\n $scope.originalLat = self.ctx.data[i].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentLat').patchValue(correctValue($scope.originalLat));\r\n } else if(self.ctx.data[i].dataKey.name === self.ctx.settings.lngKeyName && $scope.attributeUpdateFormGroup.get('currentLng').pristine){\r\n $scope.originalLng = self.ctx.data[i].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentLng').patchValue(correctValue($scope.originalLng));\r\n }\r\n }\r\n self.ctx.detectChanges();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nfunction correctValue(value) {\r\n if (typeof value !== \"number\") {\r\n return 0;\r\n }\r\n return value;\r\n}\r\n\r\nself.onResize = function() {\r\n $scope.smallWidthContainer = (self.ctx.$container && self.ctx.$container[0].offsetWidth < 320);\r\n $scope.changeAlignment = ($scope.isHorizontal && self.ctx.$container && self.ctx.$container[0].offsetWidth < 480);\r\n self.ctx.detectChanges();\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 2,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n\r\n};", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"latitude\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"longitude\"\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"latLabel\": {\n \"title\": \"Label for latitude\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"lngLabel\": {\n \"title\": \"Label for longitude\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\": {\n \"title\": \"Show result message\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableHighAccuracy\": {\n \"title\": \"Use high accuracy\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showGetLocation\": {\n \"title\": \"Show button 'Get current location'\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"inputFieldsAlignment\": {\n \"title\": \"Input fields alignment\",\n \"type\": \"string\",\n \"default\": \"column\"\n },\n \"isLatRequired\": {\n \"title\": \"Latitude field required\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"isLngRequired\": {\n \"title\": \"Longitude field required\",\n \"type\": \"boolean\",\n \"default\": true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"latKeyName\",\n \"lngKeyName\",\n \"isLatRequired\",\n \"isLngRequired\",\n \"enableHighAccuracy\",\n \"showGetLocation\",\n \"showResultMessage\",\n {\n \"key\": \"inputFieldsAlignment\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"column\",\n \"label\": \"Column (default)\"\n },\n {\n \"value\": \"row\",\n \"label\": \"Row\"\n }\n ]\n },\n \"showLabel\",\n \"latLabel\",\n \"lngLabel\",\n \"requiredErrorMessage\"\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-location-attribute-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update server location attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -433,8 +452,9 @@ "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? latLabel : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n \n \n {{ settings.showLabel ? lngLabel : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n \n
\n
\n \n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-coordinate-specified' | translate }}\n
\n
\n
\n
", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex-direction: column;\n flex: 1;\n}\n\n.grid__element.horizontal-alignment {\n flex-direction: row;\n}\n\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-button.getLocation {\n margin-right: 10px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.attribute-update-form mat-form-field{\n width: 100%;\n padding-right: 5px;\n}\n\n.attribute-update-form.small-width mat-form-field{\n width: 150px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet utils;\r\nlet translate;\r\n\r\nself.onInit = function() {\r\n self.ctx.ngZone.run(function() {\r\n init(); \r\n self.ctx.detectChanges(true);\r\n });\r\n};\r\n\r\n\r\nfunction init() {\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\r\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\r\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\r\n $scope.toastTargetId = 'input-widget' + utils.guid();\r\n settings = utils.deepClone(self.ctx.settings) || {};\r\n \r\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\r\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\r\n settings.showGetLocation = utils.defaultValue(settings.showGetLocation, true);\r\n settings.enableHighAccuracy = utils.defaultValue(settings.enableHighAccuracy, false);\r\n settings.isLatRequired = utils.defaultValue(settings.isLatRequired, true);\r\n settings.isLngRequired = utils.defaultValue(settings.isLngRequired, true);\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false; \r\n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\r\n\r\n $scope.isHorizontal = (settings.inputFieldsAlignment === 'row');\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-coordinate-required');\r\n $scope.latLabel = utils.customTranslation(settings.latLabel, settings.latLabel) || translate.instant('widgets.input-widgets.latitude');\r\n $scope.lngLabel = utils.customTranslation(settings.lngLabel, settings.lngLabel) || translate.instant('widgets.input-widgets.longitude');\r\n\r\n var validatorsLat = [$scope.validators.min(-90), $scope.validators.max(90)];\r\n var validatorsLng = [$scope.validators.min(-180), $scope.validators.max(180)];\r\n \r\n if (settings.isLatRequired) {\r\n validatorsLat.push($scope.validators.required);\r\n }\r\n \r\n if (settings.isLngRequired) {\r\n validatorsLng.push($scope.validators.required);\r\n }\r\n\r\n $scope.attributeUpdateFormGroup = $scope.fb.group({\r\n currentLat: [undefined, validatorsLat],\r\n currentLng: [undefined, validatorsLng],\r\n });\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === 'entity') {\r\n if (datasource.entityType === 'DEVICE') {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n \r\n $scope.entityDetected = true;\r\n }\r\n } else {\r\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\r\n }\r\n }\r\n if (datasource.dataKeys.length > 1) {\r\n $scope.dataKeyDetected = true;\r\n for (let i = 0; i < datasource.dataKeys.length; i++) {\r\n if (datasource.dataKeys[i].type != \"attribute\"){\r\n $scope.isValidParameter = false;\r\n }\r\n if (datasource.dataKeys[i].name !== settings.latKeyName && datasource.dataKeys[i].name !== settings.lngKeyName){\r\n $scope.dataKeyDetected = false;\r\n }\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function () {\r\n $scope.isFocused = false;\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entity.id,\r\n 'SHARED_SCOPE',\r\n [\r\n {\r\n key: settings.latKeyName,\r\n value: $scope.attributeUpdateFormGroup.get('currentLat').value\r\n },{\r\n key: settings.lngKeyName,\r\n value: $scope.attributeUpdateFormGroup.get('currentLng').value\r\n }\r\n ]\r\n ).subscribe(\r\n function success() {\r\n $scope.originalLat = $scope.attributeUpdateFormGroup.get('currentLat').value;\r\n $scope.originalLng = $scope.attributeUpdateFormGroup.get('currentLng').value;\r\n if (settings.showResultMessage) {\r\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function () {\r\n if ($scope.attributeUpdateFormGroup.get('currentLat').value === $scope.originalLat && $scope.attributeUpdateFormGroup.get('currentLng').value === $scope.originalLng) {\r\n $scope.isFocused = false;\r\n }\r\n };\r\n \r\n $scope.discardChange = function() {\r\n $scope.attributeUpdateFormGroup.setValue({\r\n 'currentLat': $scope.originalLat,\r\n 'currentLng': $scope.originalLng\r\n });\r\n $scope.isFocused = false;\r\n $scope.attributeUpdateFormGroup.markAsPristine();\r\n self.onDataUpdated();\r\n };\r\n \r\n $scope.disableButton = function () {\r\n return $scope.attributeUpdateFormGroup.get('currentLat').value === $scope.originalLat && $scope.attributeUpdateFormGroup.get('currentLng').value === $scope.originalLng || $scope.currentLng === null || $scope.currentLat === null;\r\n };\r\n \r\n $scope.getCoordinate = function() {\r\n if (navigator.geolocation) {\r\n navigator.geolocation.getCurrentPosition(showPosition, function (){\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.blocked-location'), \r\n 'bottom', 'left', $scope.toastTargetId);\r\n }, {\r\n enableHighAccuracy: settings.enableHighAccuracy\r\n });\r\n } else {\r\n $scope.showErrorToast(translate.instant('widgets.input-widgets.no-support-geolocation'), 'bottom', 'left', $scope.toastTargetId);\r\n }\r\n };\r\n \r\n function showPosition(position) {\r\n $scope.attributeUpdateFormGroup.setValue({\r\n currentLat: correctValue(position.coords.latitude),\r\n currentLng: correctValue(position.coords.longitude)\r\n });\r\n $scope.attributeUpdateFormGroup.markAsDirty();\r\n $scope.isFocused = true;\r\n }\r\n \r\n self.onResize();\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n for(let i = 0; i < self.typeParameters().maxDataKeys; i++){\r\n if(self.ctx.data[i].dataKey.name === self.ctx.settings.latKeyName && $scope.attributeUpdateFormGroup.get('currentLat').pristine){\r\n $scope.originalLat = self.ctx.data[i].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentLat').patchValue(correctValue($scope.originalLat));\r\n } else if(self.ctx.data[i].dataKey.name === self.ctx.settings.lngKeyName && $scope.attributeUpdateFormGroup.get('currentLng').pristine){\r\n $scope.originalLng = self.ctx.data[i].data[0][1];\r\n $scope.attributeUpdateFormGroup.get('currentLng').patchValue(correctValue($scope.originalLng));\r\n }\r\n }\r\n self.ctx.detectChanges();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n};\r\n\r\nfunction correctValue(value) {\r\n if (typeof value !== \"number\") {\r\n return 0;\r\n }\r\n return value;\r\n}\r\n\r\nself.onResize = function() {\r\n $scope.smallWidthContainer = (self.ctx.$container && self.ctx.$container[0].offsetWidth < 320);\r\n $scope.changeAlignment = ($scope.isHorizontal && self.ctx.$container && self.ctx.$container[0].offsetWidth < 480);\r\n self.ctx.detectChanges();\r\n};\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 2,\r\n singleEntity: true\r\n };\r\n};\r\n\r\nself.onDestroy = function() {\r\n\r\n};", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"latitude\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"longitude\"\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"latLabel\": {\n \"title\": \"Label for latitude\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"lngLabel\": {\n \"title\": \"Label for longitude\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\": {\n \"title\": \"Show result message\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableHighAccuracy\": {\n \"title\": \"Use high accuracy\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showGetLocation\": {\n \"title\": \"Show button 'Get current location'\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"inputFieldsAlignment\": {\n \"title\": \"Input fields alignment\",\n \"type\": \"string\",\n \"default\": \"column\"\n },\n \"isLatRequired\": {\n \"title\": \"Latitude field required\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"isLngRequired\": {\n \"title\": \"Longitude field required\",\n \"type\": \"boolean\",\n \"default\": true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"latKeyName\",\n \"lngKeyName\",\n \"isLatRequired\",\n \"isLngRequired\",\n \"enableHighAccuracy\",\n \"showGetLocation\",\n \"showResultMessage\",\n {\n \"key\": \"inputFieldsAlignment\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"column\",\n \"label\": \"Column (default)\"\n },\n {\n \"value\": \"row\",\n \"label\": \"Row\"\n }\n ]\n },\n \"showLabel\",\n \"latLabel\",\n \"lngLabel\",\n \"requiredErrorMessage\"\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-location-attribute-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update shared location attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -451,8 +471,9 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.photoCameraInputWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Photo Camera\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"imageFormat\": {\n \"title\": \"Image Format\",\n \"type\": \"string\",\n \"default\": \"image/png\"\n },\n \"imageQuality\":{\n \"title\":\"Image quality that use lossy compression such as jpeg and webp\",\n \"type\":\"number\",\n \"default\": 0.92,\n \"min\": 0,\n \"max\": 1\n },\n \"maxWidth\": {\n \"title\": \"The maximal image width\",\n \"type\": \"number\",\n \"default\": 640\n }, \n \"maxHeight\": {\n \"title\": \"The maximal image heigth\",\n \"type\": \"number\",\n \"default\": 480\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n {\n \"key\": \"imageFormat\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"image/jpeg\",\n \"label\": \"JPEG\"\n },\n {\n \"value\": \"image/png\",\n \"label\": \"PNG\"\n },\n {\n \"value\": \"image/webp\",\n \"label\": \"WEBP\"\n }\n ]\n },\n \"imageQuality\",\n \"maxWidth\",\n \"maxHeight\"\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-photo-camera-input-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Photo camera input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -469,8 +490,9 @@ "templateHtml": "\n", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.jsonInputWidget.onDataUpdated();\n}\n\nself.onResize = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n}", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"AdvancedSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"widgetMode\": {\n \"title\": \"Widget mode\",\n \"type\": \"string\",\n \"default\": \"ATTRIBUTE\"\n },\n \"attributeScope\": {\n \"title\": \"Attribute scope\",\n \"type\": \"string\",\n \"default\": \"SERVER_SCOPE\"\n },\n \"showLabel\":{\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"attributeRequired\": {\n \"title\": \"Value required\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showResultMessage\": {\n \"title\": \"Show result message\",\n \"type\": \"boolean\",\n \"default\": true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n {\n \"key\": \"widgetMode\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"ATTRIBUTE\",\n \"label\": \"Update attribute\"\n },\n {\n \"value\": \"TIME_SERIES\",\n \"label\": \"Update timeseries\"\n }\n ]\n },\n {\n \"key\": \"attributeScope\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"condition\": \"model.widgetMode === 'ATTRIBUTE'\",\n \"items\": [\n {\n \"value\": \"SERVER_SCOPE\",\n \"label\": \"Server attribute\"\n },\n {\n \"value\": \"SHARED_SCOPE\",\n \"label\": \"Shared attribute\"\n }\n ]\n },\n \"showLabel\",\n {\n \"key\": \"labelValue\",\n \"condition\": \"model.showLabel\"\n },\n \"attributeRequired\",\n \"showResultMessage\"\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}", + "settingsDirective": "tb-update-json-attribute-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"attributeScope\":\"SERVER_SCOPE\",\"showLabel\":true,\"attributeRequired\":true,\"showResultMessage\":true},\"title\":\"Update JSON attribute\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/device-claiming-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/device-claiming-widget-settings.component.html new file mode 100644 index 0000000000..9438d6d59f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/device-claiming-widget-settings.component.html @@ -0,0 +1,84 @@ + +
+
+ widgets.input-widgets.general-settings + + widgets.input-widgets.widget-title + + + + widgets.input-widgets.claim-button-label + + + + {{ 'widgets.input-widgets.show-secret-key-field' | translate }} + +
+
+ widgets.input-widgets.labels-settings + + + + + {{ 'widgets.input-widgets.show-labels' | translate }} + + + + widget-config.advanced-settings + + + +
+ + widgets.input-widgets.device-name-label + + + + widgets.input-widgets.secret-key-label + + +
+
+
+
+
+ widgets.input-widgets.messages-settings + + widgets.input-widgets.claim-device-success-message + + + + widgets.input-widgets.claim-device-not-found-message + + + + widgets.input-widgets.claim-device-failed-message + + + + widgets.input-widgets.claim-device-name-required-message + + + + widgets.input-widgets.claim-device-secret-key-required-message + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/device-claiming-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/device-claiming-widget-settings.component.ts new file mode 100644 index 0000000000..b245ce43bb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/device-claiming-widget-settings.component.ts @@ -0,0 +1,110 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-device-claiming-widget-settings', + templateUrl: './device-claiming-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class DeviceClaimingWidgetSettingsComponent extends WidgetSettingsComponent { + + deviceClaimingWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.deviceClaimingWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + widgetTitle: '', + labelClaimButon: '', + deviceSecret: false, + showLabel: true, + deviceLabel: '', + secretKeyLabel: '', + successfulClaimDevice: '', + deviceNotFound: '', + failedClaimDevice: '', + requiredErrorDevice: '', + requiredErrorSecretKey: '' + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.deviceClaimingWidgetSettingsForm = this.fb.group({ + + // General settings + + widgetTitle: [settings.widgetTitle, []], + labelClaimButon: [settings.labelClaimButon, []], + deviceSecret: [settings.deviceSecret, []], + + // Labels settings + + showLabel: [settings.showLabel, []], + deviceLabel: [settings.deviceLabel, []], + secretKeyLabel: [settings.secretKeyLabel, []], + + // Message settings + + successfulClaimDevice: [settings.successfulClaimDevice, []], + deviceNotFound: [settings.deviceNotFound, []], + failedClaimDevice: [settings.failedClaimDevice, []], + requiredErrorDevice: [settings.requiredErrorDevice, []], + requiredErrorSecretKey: [settings.requiredErrorSecretKey, []] + }); + } + + protected validatorTriggers(): string[] { + return ['deviceSecret', 'showLabel']; + } + + protected updateValidators(emitEvent: boolean) { + const deviceSecret: boolean = this.deviceClaimingWidgetSettingsForm.get('deviceSecret').value; + const showLabel: boolean = this.deviceClaimingWidgetSettingsForm.get('showLabel').value; + if (deviceSecret) { + if (showLabel) { + this.deviceClaimingWidgetSettingsForm.get('secretKeyLabel').enable(); + } else { + this.deviceClaimingWidgetSettingsForm.get('secretKeyLabel').disable(); + } + this.deviceClaimingWidgetSettingsForm.get('requiredErrorSecretKey').enable(); + } else { + this.deviceClaimingWidgetSettingsForm.get('requiredErrorSecretKey').disable(); + this.deviceClaimingWidgetSettingsForm.get('secretKeyLabel').disable(); + } + if (showLabel) { + this.deviceClaimingWidgetSettingsForm.get('deviceLabel').enable(); + } else { + this.deviceClaimingWidgetSettingsForm.get('deviceLabel').disable(); + } + this.deviceClaimingWidgetSettingsForm.get('secretKeyLabel').updateValueAndValidity({emitEvent}); + this.deviceClaimingWidgetSettingsForm.get('deviceLabel').updateValueAndValidity({emitEvent}); + this.deviceClaimingWidgetSettingsForm.get('requiredErrorSecretKey').updateValueAndValidity({emitEvent}); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.html new file mode 100644 index 0000000000..d838c59bf7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.html @@ -0,0 +1,59 @@ + +
+
+ widgets.input-widgets.general-settings + + widgets.input-widgets.widget-title + + +
+
+ widgets.input-widgets.image-settings +
+ + widgets.input-widgets.image-format + + + {{ 'widgets.input-widgets.image-format-jpeg' | translate }} + + + {{ 'widgets.input-widgets.image-format-png' | translate }} + + + {{ 'widgets.input-widgets.image-format-webp' | translate }} + + + + + widgets.input-widgets.image-quality + + +
+
+ + widgets.input-widgets.max-image-width + + + + widgets.input-widgets.max-image-height + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.ts new file mode 100644 index 0000000000..44664d6b67 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.ts @@ -0,0 +1,67 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-photo-camera-input-widget-settings', + templateUrl: './photo-camera-input-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class PhotoCameraInputWidgetSettingsComponent extends WidgetSettingsComponent { + + photoCameraInputWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.photoCameraInputWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + widgetTitle: '', + + imageFormat: 'image/png', + imageQuality: 0.92, + maxWidth: 640, + maxHeight: 480 + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.photoCameraInputWidgetSettingsForm = this.fb.group({ + + // General settings + + widgetTitle: [settings.widgetTitle, []], + + // Image settings + + imageFormat: [settings.imageFormat, []], + imageQuality: [settings.imageQuality, [Validators.min(0), Validators.max(1)]], + maxWidth: [settings.maxWidth, [Validators.min(1)]], + maxHeight: [settings.maxHeight, [Validators.min(1)]] + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-attribute-general-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-attribute-general-settings.component.html new file mode 100644 index 0000000000..14b63d2f14 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-attribute-general-settings.component.html @@ -0,0 +1,47 @@ + +
+
+ widgets.input-widgets.general-settings + + widgets.input-widgets.widget-title + + +
+ + {{ 'widgets.input-widgets.show-label' | translate }} + + + widgets.input-widgets.label + + +
+
+ + {{ 'widgets.input-widgets.required' | translate }} + + + widgets.input-widgets.required-error-message + + +
+ + {{ 'widgets.input-widgets.show-result-message' | translate }} + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-attribute-general-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-attribute-general-settings.component.ts new file mode 100644 index 0000000000..6f12aa4463 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-attribute-general-settings.component.ts @@ -0,0 +1,172 @@ +/// +/// Copyright © 2016-2022 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 { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; + +export interface UpdateAttributeGeneralSettings { + widgetTitle: string; + showLabel: boolean; + labelValue?: string; + isRequired: boolean; + requiredErrorMessage: string; + showResultMessage: boolean; +} + +export function updateAttributeGeneralDefaultSettings(hasLabelValue = true): UpdateAttributeGeneralSettings { + const updateAttributeGeneralSettings: UpdateAttributeGeneralSettings = { + widgetTitle: '', + showLabel: true, + isRequired: true, + requiredErrorMessage: '', + showResultMessage: true + }; + if (hasLabelValue) { + updateAttributeGeneralSettings.labelValue = ''; + } + return updateAttributeGeneralSettings; +} + +@Component({ + selector: 'tb-update-attribute-general-settings', + templateUrl: './update-attribute-general-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => UpdateAttributeGeneralSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => UpdateAttributeGeneralSettingsComponent), + multi: true + } + ] +}) +export class UpdateAttributeGeneralSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { + + @Input() + disabled: boolean; + + @Input() + hasLabelValue = true; + + private modelValue: UpdateAttributeGeneralSettings; + + private propagateChange = null; + + public updateAttributeGeneralSettingsFormGroup: FormGroup; + + constructor(protected store: Store, + private translate: TranslateService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.updateAttributeGeneralSettingsFormGroup = this.fb.group({ + widgetTitle: ['', []], + showLabel: [true, []], + isRequired: [true, []], + requiredErrorMessage: ['', []], + showResultMessage: [true, []] + }); + if (this.hasLabelValue) { + this.updateAttributeGeneralSettingsFormGroup.addControl('labelValue', this.fb.control('', [])); + this.updateAttributeGeneralSettingsFormGroup.get('showLabel').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + } + this.updateAttributeGeneralSettingsFormGroup.get('isRequired').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.updateAttributeGeneralSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + this.updateValidators(false); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.updateAttributeGeneralSettingsFormGroup.disable({emitEvent: false}); + } else { + this.updateAttributeGeneralSettingsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: UpdateAttributeGeneralSettings): void { + this.modelValue = value; + this.updateAttributeGeneralSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + this.updateValidators(false); + } + + public validate(c: FormControl) { + return this.updateAttributeGeneralSettingsFormGroup.valid ? null : { + updateAttributeGeneralSettings: { + valid: false, + }, + }; + } + + private updateModel() { + const value: UpdateAttributeGeneralSettings = this.updateAttributeGeneralSettingsFormGroup.value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } + + private updateValidators(emitEvent?: boolean): void { + if (this.hasLabelValue) { + const showLabel: boolean = this.updateAttributeGeneralSettingsFormGroup.get('showLabel').value; + if (showLabel) { + this.updateAttributeGeneralSettingsFormGroup.get('labelValue').enable({emitEvent}); + } else { + this.updateAttributeGeneralSettingsFormGroup.get('labelValue').disable({emitEvent}); + } + this.updateAttributeGeneralSettingsFormGroup.get('labelValue').updateValueAndValidity({emitEvent: false}); + } + + const isRequired: boolean = this.updateAttributeGeneralSettingsFormGroup.get('isRequired').value; + if (isRequired) { + this.updateAttributeGeneralSettingsFormGroup.get('requiredErrorMessage').enable({emitEvent}); + } else { + this.updateAttributeGeneralSettingsFormGroup.get('requiredErrorMessage').disable({emitEvent}); + } + this.updateAttributeGeneralSettingsFormGroup.get('requiredErrorMessage').updateValueAndValidity({emitEvent: false}); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-boolean-attribute-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-boolean-attribute-widget-settings.component.html new file mode 100644 index 0000000000..d47e0a0d73 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-boolean-attribute-widget-settings.component.html @@ -0,0 +1,42 @@ + +
+
+ widgets.input-widgets.general-settings + + widgets.input-widgets.widget-title + + + + {{ 'widgets.input-widgets.show-result-message' | translate }} + +
+
+ widgets.input-widgets.checkbox-settings +
+ + widgets.input-widgets.true-label + + + + widgets.input-widgets.false-label + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-boolean-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-boolean-attribute-widget-settings.component.ts new file mode 100644 index 0000000000..66eded1107 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-boolean-attribute-widget-settings.component.ts @@ -0,0 +1,58 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-update-boolean-attribute-widget-settings', + templateUrl: './update-boolean-attribute-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class UpdateBooleanAttributeWidgetSettingsComponent extends WidgetSettingsComponent { + + updateBooleanAttributeWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.updateBooleanAttributeWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + widgetTitle: '', + showResultMessage: true, + trueValue: '', + falseValue: '' + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.updateBooleanAttributeWidgetSettingsForm = this.fb.group({ + widgetTitle: [settings.widgetTitle, []], + showResultMessage: [settings.showResultMessage, []], + trueValue: [settings.trueValue, []], + falseValue: [settings.falseValue, []] + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-date-attribute-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-date-attribute-widget-settings.component.html new file mode 100644 index 0000000000..5939daefc4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-date-attribute-widget-settings.component.html @@ -0,0 +1,30 @@ + + +
+ + +
+ widgets.input-widgets.datetime-field-settings + + {{ 'widgets.input-widgets.display-time-input' | translate }} + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-date-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-date-attribute-widget-settings.component.ts new file mode 100644 index 0000000000..898a645330 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-date-attribute-widget-settings.component.ts @@ -0,0 +1,73 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { deepClone } from '@core/utils'; +import { + updateAttributeGeneralDefaultSettings +} from '@home/components/widget/lib/settings/input/update-attribute-general-settings.component'; + +@Component({ + selector: 'tb-update-date-attribute-widget-settings', + templateUrl: './update-date-attribute-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class UpdateDateAttributeWidgetSettingsComponent extends WidgetSettingsComponent { + + updateDateAttributeWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.updateDateAttributeWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + ...updateAttributeGeneralDefaultSettings(false), + showTimeInput: true + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.updateDateAttributeWidgetSettingsForm = this.fb.group({ + updateAttributeGeneralSettings: [settings.updateAttributeGeneralSettings, []], + showTimeInput: [settings.showTimeInput, []] + }); + } + + protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { + const updateAttributeGeneralSettings = deepClone(settings, ['showTimeInput']); + return { + updateAttributeGeneralSettings, + showTimeInput: settings.showTimeInput + }; + } + + protected prepareOutputSettings(settings: any): WidgetSettings { + return { + ...settings.updateAttributeGeneralSettings, + showTimeInput: settings.showTimeInput + }; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-double-attribute-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-double-attribute-widget-settings.component.html new file mode 100644 index 0000000000..8f5b543e41 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-double-attribute-widget-settings.component.html @@ -0,0 +1,36 @@ + + +
+ + +
+ widgets.input-widgets.double-field-settings +
+ + widgets.input-widgets.min-value + + + + widgets.input-widgets.max-value + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-double-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-double-attribute-widget-settings.component.ts new file mode 100644 index 0000000000..ef71992aef --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-double-attribute-widget-settings.component.ts @@ -0,0 +1,77 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { deepClone } from '@core/utils'; +import { + updateAttributeGeneralDefaultSettings +} from '@home/components/widget/lib/settings/input/update-attribute-general-settings.component'; + +@Component({ + selector: 'tb-update-double-attribute-widget-settings', + templateUrl: './update-double-attribute-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class UpdateDoubleAttributeWidgetSettingsComponent extends WidgetSettingsComponent { + + updateDoubleAttributeWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.updateDoubleAttributeWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + ...updateAttributeGeneralDefaultSettings(), + minValue: null, + maxValue: null + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.updateDoubleAttributeWidgetSettingsForm = this.fb.group({ + updateAttributeGeneralSettings: [settings.updateAttributeGeneralSettings, []], + minValue: [settings.minValue, []], + maxValue: [settings.maxValue, []] + }); + } + + protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { + const updateAttributeGeneralSettings = deepClone(settings, ['minValue', 'maxValue']); + return { + updateAttributeGeneralSettings, + minValue: settings.minValue, + maxValue: settings.maxValue + }; + } + + protected prepareOutputSettings(settings: any): WidgetSettings { + return { + ...settings.updateAttributeGeneralSettings, + minValue: settings.minValue, + maxValue: settings.maxValue + }; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-image-attribute-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-image-attribute-widget-settings.component.html new file mode 100644 index 0000000000..ee86c62870 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-image-attribute-widget-settings.component.html @@ -0,0 +1,46 @@ + +
+
+ widgets.input-widgets.general-settings + + widgets.input-widgets.widget-title + + + + {{ 'widgets.input-widgets.show-result-message' | translate }} + +
+
+ widgets.input-widgets.image-input-settings +
+ + {{ 'widgets.input-widgets.display-preview' | translate }} + + + {{ 'widgets.input-widgets.display-clear-button' | translate }} + + + {{ 'widgets.input-widgets.display-apply-button' | translate }} + + + {{ 'widgets.input-widgets.display-discard-button' | translate }} + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-image-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-image-attribute-widget-settings.component.ts new file mode 100644 index 0000000000..755fa6fc94 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-image-attribute-widget-settings.component.ts @@ -0,0 +1,62 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-update-image-attribute-widget-settings', + templateUrl: './update-image-attribute-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class UpdateImageAttributeWidgetSettingsComponent extends WidgetSettingsComponent { + + updateImageAttributeWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.updateImageAttributeWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + widgetTitle: '', + showResultMessage: true, + displayPreview: true, + displayClearButton: false, + displayApplyButton: true, + displayDiscardButton: true + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.updateImageAttributeWidgetSettingsForm = this.fb.group({ + widgetTitle: [settings.widgetTitle, []], + showResultMessage: [settings.showResultMessage, []], + displayPreview: [settings.displayPreview, []], + displayClearButton: [settings.displayClearButton, []], + displayApplyButton: [settings.displayApplyButton, []], + displayDiscardButton: [settings.displayDiscardButton, []] + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-integer-attribute-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-integer-attribute-widget-settings.component.html new file mode 100644 index 0000000000..0e5c1deccd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-integer-attribute-widget-settings.component.html @@ -0,0 +1,36 @@ + + +
+ + +
+ widgets.input-widgets.integer-field-settings +
+ + widgets.input-widgets.min-value + + + + widgets.input-widgets.max-value + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-integer-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-integer-attribute-widget-settings.component.ts new file mode 100644 index 0000000000..d06601b468 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-integer-attribute-widget-settings.component.ts @@ -0,0 +1,77 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { deepClone } from '@core/utils'; +import { + updateAttributeGeneralDefaultSettings +} from '@home/components/widget/lib/settings/input/update-attribute-general-settings.component'; + +@Component({ + selector: 'tb-update-integer-attribute-widget-settings', + templateUrl: './update-integer-attribute-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class UpdateIntegerAttributeWidgetSettingsComponent extends WidgetSettingsComponent { + + updateIntegerAttributeWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.updateIntegerAttributeWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + ...updateAttributeGeneralDefaultSettings(), + minValue: null, + maxValue: null, + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.updateIntegerAttributeWidgetSettingsForm = this.fb.group({ + updateAttributeGeneralSettings: [settings.updateAttributeGeneralSettings, []], + minValue: [settings.minValue, []], + maxValue: [settings.maxValue, []] + }); + } + + protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { + const updateAttributeGeneralSettings = deepClone(settings, ['minValue', 'maxValue']); + return { + updateAttributeGeneralSettings, + minValue: settings.minValue, + maxValue: settings.maxValue + }; + } + + protected prepareOutputSettings(settings: any): WidgetSettings { + return { + ...settings.updateAttributeGeneralSettings, + minValue: settings.minValue, + maxValue: settings.maxValue + }; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.html new file mode 100644 index 0000000000..cd967cff33 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.html @@ -0,0 +1,68 @@ + +
+
+ widgets.input-widgets.general-settings + + widgets.input-widgets.widget-title + + +
+ + {{ 'widgets.input-widgets.show-label' | translate }} + + + widgets.input-widgets.label + + +
+ + {{ 'widgets.input-widgets.show-result-message' | translate }} + +
+
+ widgets.input-widgets.attribute-settings +
+ + widgets.input-widgets.widget-mode + + + {{ 'widgets.input-widgets.widget-mode-update-attribute' | translate }} + + + {{ 'widgets.input-widgets.widget-mode-update-timeseries' | translate }} + + + + + widgets.input-widgets.attribute-scope + + + {{ 'widgets.input-widgets.attribute-scope-server' | translate }} + + + {{ 'widgets.input-widgets.attribute-scope-shared' | translate }} + + + +
+ + {{ 'widgets.input-widgets.value-required' | translate }} + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.ts new file mode 100644 index 0000000000..af0c8bf474 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component.ts @@ -0,0 +1,93 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-update-json-attribute-widget-settings', + templateUrl: './update-json-attribute-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class UpdateJsonAttributeWidgetSettingsComponent extends WidgetSettingsComponent { + + updateJsonAttributeWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.updateJsonAttributeWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + widgetTitle: '', + showLabel: true, + labelValue: '', + showResultMessage: true, + + widgetMode: 'ATTRIBUTE', + attributeScope: 'SERVER_SCOPE', + attributeRequired: true + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.updateJsonAttributeWidgetSettingsForm = this.fb.group({ + + // General settings + + widgetTitle: [settings.widgetTitle, []], + showLabel: [settings.showLabel, []], + labelValue: [settings.labelValue, []], + showResultMessage: [settings.showResultMessage, []], + + // Attribute settings + + widgetMode: [settings.widgetMode, []], + attributeScope: [settings.attributeScope, []], + attributeRequired: [settings.attributeRequired, []] + }); + } + + protected validatorTriggers(): string[] { + return ['showLabel', 'widgetMode']; + } + + protected updateValidators(emitEvent: boolean) { + const showLabel: boolean = this.updateJsonAttributeWidgetSettingsForm.get('showLabel').value; + const widgetMode: string = this.updateJsonAttributeWidgetSettingsForm.get('widgetMode').value; + + if (showLabel) { + this.updateJsonAttributeWidgetSettingsForm.get('labelValue').enable(); + } else { + this.updateJsonAttributeWidgetSettingsForm.get('labelValue').disable(); + } + if (widgetMode === 'ATTRIBUTE') { + this.updateJsonAttributeWidgetSettingsForm.get('attributeScope').enable(); + } else { + this.updateJsonAttributeWidgetSettingsForm.get('attributeScope').disable(); + } + this.updateJsonAttributeWidgetSettingsForm.get('labelValue').updateValueAndValidity({emitEvent}); + this.updateJsonAttributeWidgetSettingsForm.get('attributeScope').updateValueAndValidity({emitEvent}); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-location-attribute-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-location-attribute-widget-settings.component.html new file mode 100644 index 0000000000..5afb4cfc9d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-location-attribute-widget-settings.component.html @@ -0,0 +1,84 @@ + +
+
+ widgets.input-widgets.general-settings + + widgets.input-widgets.widget-title + + + + {{ 'widgets.input-widgets.show-result-message' | translate }} + +
+ + widgets.input-widgets.latitude-key-name + + + + widgets.input-widgets.longitude-key-name + + +
+ + {{ 'widgets.input-widgets.show-get-location-button' | translate }} + + + {{ 'widgets.input-widgets.use-high-accuracy' | translate }} + +
+
+ widgets.input-widgets.location-fields-settings + + {{ 'widgets.input-widgets.show-label' | translate }} + +
+ + widgets.input-widgets.latitude-label + + + + widgets.input-widgets.longitude-label + + +
+ + widgets.input-widgets.input-fields-alignment + + + {{ 'widgets.input-widgets.input-fields-alignment-column' | translate }} + + + {{ 'widgets.input-widgets.input-fields-alignment-row' | translate }} + + + +
+ + {{ 'widgets.input-widgets.latitude-field-required' | translate }} + + + {{ 'widgets.input-widgets.longitude-field-required' | translate }} + +
+ + widgets.input-widgets.required-error-message + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-location-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-location-attribute-widget-settings.component.ts new file mode 100644 index 0000000000..a9fa723f29 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-location-attribute-widget-settings.component.ts @@ -0,0 +1,109 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-update-location-attribute-widget-settings', + templateUrl: './update-location-attribute-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class UpdateLocationAttributeWidgetSettingsComponent extends WidgetSettingsComponent { + + updateLocationAttributeWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.updateLocationAttributeWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + widgetTitle: '', + showResultMessage: true, + latKeyName: 'latitude', + lngKeyName: 'longitude', + showGetLocation: true, + enableHighAccuracy: false, + + showLabel: true, + latLabel: '', + lngLabel: '', + inputFieldsAlignment: 'column', + isLatRequired: true, + isLngRequired: true, + requiredErrorMessage: '' + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.updateLocationAttributeWidgetSettingsForm = this.fb.group({ + + // General settings + + widgetTitle: [settings.widgetTitle, []], + showResultMessage: [settings.showResultMessage, []], + latKeyName: [settings.latKeyName, []], + lngKeyName: [settings.lngKeyName, []], + showGetLocation: [settings.showGetLocation, []], + enableHighAccuracy: [settings.enableHighAccuracy, []], + + // Location fields settings + + showLabel: [settings.showLabel, []], + latLabel: [settings.latLabel, []], + lngLabel: [settings.lngLabel, []], + inputFieldsAlignment: [settings.inputFieldsAlignment, []], + isLatRequired: [settings.isLatRequired, []], + isLngRequired: [settings.isLngRequired, []], + requiredErrorMessage: [settings.requiredErrorMessage, []] + }); + } + + protected validatorTriggers(): string[] { + return ['showLabel', 'isLatRequired', 'isLngRequired']; + } + + protected updateValidators(emitEvent: boolean) { + const showLabel: boolean = this.updateLocationAttributeWidgetSettingsForm.get('showLabel').value; + const isLatRequired: boolean = this.updateLocationAttributeWidgetSettingsForm.get('isLatRequired').value; + const isLngRequired: boolean = this.updateLocationAttributeWidgetSettingsForm.get('isLngRequired').value; + + if (showLabel) { + this.updateLocationAttributeWidgetSettingsForm.get('latLabel').enable(); + this.updateLocationAttributeWidgetSettingsForm.get('lngLabel').enable(); + } else { + this.updateLocationAttributeWidgetSettingsForm.get('latLabel').disable(); + this.updateLocationAttributeWidgetSettingsForm.get('lngLabel').disable(); + } + if (isLatRequired || isLngRequired) { + this.updateLocationAttributeWidgetSettingsForm.get('requiredErrorMessage').enable(); + } else { + this.updateLocationAttributeWidgetSettingsForm.get('requiredErrorMessage').disable(); + } + this.updateLocationAttributeWidgetSettingsForm.get('latLabel').updateValueAndValidity({emitEvent}); + this.updateLocationAttributeWidgetSettingsForm.get('lngLabel').updateValueAndValidity({emitEvent}); + this.updateLocationAttributeWidgetSettingsForm.get('requiredErrorMessage').updateValueAndValidity({emitEvent}); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-string-attribute-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-string-attribute-widget-settings.component.html new file mode 100644 index 0000000000..f80d319dab --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-string-attribute-widget-settings.component.html @@ -0,0 +1,36 @@ + + +
+ + +
+ widgets.input-widgets.text-field-settings +
+ + widgets.input-widgets.min-length + + + + widgets.input-widgets.max-length + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-string-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-string-attribute-widget-settings.component.ts new file mode 100644 index 0000000000..13dd1ec453 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-string-attribute-widget-settings.component.ts @@ -0,0 +1,77 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { deepClone } from '@core/utils'; +import { + updateAttributeGeneralDefaultSettings +} from '@home/components/widget/lib/settings/input/update-attribute-general-settings.component'; + +@Component({ + selector: 'tb-update-string-attribute-widget-settings', + templateUrl: './update-string-attribute-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class UpdateStringAttributeWidgetSettingsComponent extends WidgetSettingsComponent { + + updateStringAttributeWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.updateStringAttributeWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + ...updateAttributeGeneralDefaultSettings(), + minLength: null, + maxLength: null, + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.updateStringAttributeWidgetSettingsForm = this.fb.group({ + updateAttributeGeneralSettings: [settings.updateAttributeGeneralSettings, []], + minLength: [settings.minLength, [Validators.min(0)]], + maxLength: [settings.maxLength, [Validators.min(1)]] + }); + } + + protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { + const updateAttributeGeneralSettings = deepClone(settings, ['minLength', 'maxLength']); + return { + updateAttributeGeneralSettings, + minLength: settings.minLength, + maxLength: settings.maxLength + }; + } + + protected prepareOutputSettings(settings: any): WidgetSettings { + return { + ...settings.updateAttributeGeneralSettings, + minLength: settings.minLength, + maxLength: settings.maxLength + }; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index 8ee0388bc8..bbf7e52a10 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -169,6 +169,39 @@ import { import { NavigationCardsWidgetSettingsComponent } from '@home/components/widget/lib/settings/navigation/navigation-cards-widget-settings.component'; +import { + DeviceClaimingWidgetSettingsComponent +} from '@home/components/widget/lib/settings/input/device-claiming-widget-settings.component'; +import { + UpdateAttributeGeneralSettingsComponent +} from '@home/components/widget/lib/settings/input/update-attribute-general-settings.component'; +import { + UpdateIntegerAttributeWidgetSettingsComponent +} from '@home/components/widget/lib/settings/input/update-integer-attribute-widget-settings.component'; +import { + UpdateDoubleAttributeWidgetSettingsComponent +} from '@home/components/widget/lib/settings/input/update-double-attribute-widget-settings.component'; +import { + UpdateStringAttributeWidgetSettingsComponent +} from '@home/components/widget/lib/settings/input/update-string-attribute-widget-settings.component'; +import { + UpdateBooleanAttributeWidgetSettingsComponent +} from '@home/components/widget/lib/settings/input/update-boolean-attribute-widget-settings.component'; +import { + UpdateImageAttributeWidgetSettingsComponent +} from '@home/components/widget/lib/settings/input/update-image-attribute-widget-settings.component'; +import { + UpdateDateAttributeWidgetSettingsComponent +} from '@home/components/widget/lib/settings/input/update-date-attribute-widget-settings.component'; +import { + UpdateLocationAttributeWidgetSettingsComponent +} from '@home/components/widget/lib/settings/input/update-location-attribute-widget-settings.component'; +import { + UpdateJsonAttributeWidgetSettingsComponent +} from '@home/components/widget/lib/settings/input/update-json-attribute-widget-settings.component'; +import { + PhotoCameraInputWidgetSettingsComponent +} from '@home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component'; @NgModule({ declarations: [ @@ -231,7 +264,18 @@ import { GpioControlWidgetSettingsComponent, GpioPanelWidgetSettingsComponent, NavigationCardWidgetSettingsComponent, - NavigationCardsWidgetSettingsComponent + NavigationCardsWidgetSettingsComponent, + DeviceClaimingWidgetSettingsComponent, + UpdateAttributeGeneralSettingsComponent, + UpdateIntegerAttributeWidgetSettingsComponent, + UpdateDoubleAttributeWidgetSettingsComponent, + UpdateStringAttributeWidgetSettingsComponent, + UpdateBooleanAttributeWidgetSettingsComponent, + UpdateImageAttributeWidgetSettingsComponent, + UpdateDateAttributeWidgetSettingsComponent, + UpdateLocationAttributeWidgetSettingsComponent, + UpdateJsonAttributeWidgetSettingsComponent, + PhotoCameraInputWidgetSettingsComponent ], imports: [ CommonModule, @@ -298,7 +342,18 @@ import { GpioControlWidgetSettingsComponent, GpioPanelWidgetSettingsComponent, NavigationCardWidgetSettingsComponent, - NavigationCardsWidgetSettingsComponent + NavigationCardsWidgetSettingsComponent, + DeviceClaimingWidgetSettingsComponent, + UpdateAttributeGeneralSettingsComponent, + UpdateIntegerAttributeWidgetSettingsComponent, + UpdateDoubleAttributeWidgetSettingsComponent, + UpdateStringAttributeWidgetSettingsComponent, + UpdateBooleanAttributeWidgetSettingsComponent, + UpdateImageAttributeWidgetSettingsComponent, + UpdateDateAttributeWidgetSettingsComponent, + UpdateLocationAttributeWidgetSettingsComponent, + UpdateJsonAttributeWidgetSettingsComponent, + PhotoCameraInputWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -350,5 +405,15 @@ export const widgetSettingsComponentsMap: {[key: string]: Type Date: Fri, 29 Apr 2022 18:12:35 +0300 Subject: [PATCH 073/276] UI: Add 2FA general setting form --- .../http/two-factor-authentication.service.ts | 16 ++ ui-ngx/src/app/core/services/menu.service.ts | 16 +- .../two-factor-auth-settings.component.html | 266 ++++++++++-------- .../two-factor-auth-settings.component.scss | 21 ++ .../two-factor-auth-settings.component.ts | 71 +++-- .../shared/models/two-factor-auth.models.ts | 22 +- .../assets/locale/locale.constant-en_US.json | 26 +- 7 files changed, 283 insertions(+), 155 deletions(-) diff --git a/ui-ngx/src/app/core/http/two-factor-authentication.service.ts b/ui-ngx/src/app/core/http/two-factor-authentication.service.ts index d608d3b362..230eddd208 100644 --- a/ui-ngx/src/app/core/http/two-factor-authentication.service.ts +++ b/ui-ngx/src/app/core/http/two-factor-authentication.service.ts @@ -1,3 +1,19 @@ +/// +/// Copyright © 2016-2022 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 { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index 294513c782..15ab552d53 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -364,7 +364,7 @@ export class MenuService { name: 'admin.system-settings', type: 'toggle', path: '/settings', - height: '80px', + height: '120px', icon: 'settings', pages: [ { @@ -374,6 +374,14 @@ export class MenuService { path: '/settings/home', icon: 'settings_applications' }, + { + id: guid(), + name: 'admin.2fa.2fa', + type: 'link', + path: '/settings/2fa', + icon: 'mdi:two-factor-authentication', + isMdiIcon: true + }, { id: guid(), name: 'resource.resources-library', @@ -510,6 +518,12 @@ export class MenuService { icon: 'settings_applications', path: '/settings/home' }, + { + name: 'admin.2fa.2fa', + icon: 'mdi:two-factor-authentication', + isMdiIcon: true, + path: '/settings/2fa' + }, { name: 'resource.resources-library', icon: 'folder', diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html index 9a507e45f2..c92fe6de74 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html @@ -1,3 +1,20 @@ +
@@ -11,131 +28,146 @@
-
- - {{ 'admin.2fa.use-system-two-factor-auth-settings' | translate }} - - - - admin.2fa.total-allowed-time-for-verification - - - {{ 'admin.2fa.total-allowed-time-for-verification-required' | translate }} - - - {{ 'admin.2fa.total-allowed-time-for-verification-pattern' | translate }} - - - - admin.2fa.max-verification-failures-before-user-lockout - - - {{ 'admin.2fa.max-verification-failures-before-user-lockout-required' | translate }} - - - {{ 'admin.2fa.max-verification-failures-before-user-lockout-pattern' | translate }} - - - - admin.2fa.verification-code-send-rate-limit - - admin.2fa.verification-code-send-rate-limit-hint - - {{ 'admin.2fa.verification-code-send-rate-limit-pattern' | translate }} - - - - admin.2fa.verification-code-check-rate-limit - - admin.2fa.verification-code-check-rate-limit-hint - - {{ 'admin.2fa.verification-code-check-rate-limit-pattern' | translate }} - - -
Providers
- -
- - - - {{ provider.value.providerType }} - - - - - + +
+ + {{ 'admin.2fa.use-system-two-factor-auth-settings' | translate }} + + + + admin.2fa.total-allowed-time-for-verification + + + {{ 'admin.2fa.total-allowed-time-for-verification-required' | translate }} + + + {{ 'admin.2fa.total-allowed-time-for-verification-pattern' | translate }} + + + + admin.2fa.max-verification-failures-before-user-lockout + + + {{ 'admin.2fa.max-verification-failures-before-user-lockout-required' | translate }} + + + {{ 'admin.2fa.max-verification-failures-before-user-lockout-pattern' | translate }} + + + + admin.2fa.verification-code-send-rate-limit + + + {{ 'admin.2fa.verification-code-send-rate-limit-required' | translate }} + + + {{ 'admin.2fa.verification-code-send-rate-limit-pattern' | translate }} + + + + admin.2fa.verification-code-check-rate-limit + + + {{ 'admin.2fa.verification-code-check-rate-limit-required' | translate }} + + + {{ 'admin.2fa.verification-code-check-rate-limit-pattern' | translate }} + + +
admin.2fa.available-providers
+ +
+ + + + + {{ provider.value.providerType }} + + + + + - -
-
-
+ +
- admin.oauth2.login-provider - - - {{ provider }} + admin.2fa.provider + + + {{ twoFactorAuthProviderType }} -
- - admin.oauth2.allowed-platforms - - - {{ platformTypeTranslations.get(platform) | translate }} - - - -
-
- - admin.oauth2.client-id - - - {{ 'admin.oauth2.client-id-required' | translate }} - - - {{ 'admin.oauth2.client-id-max-length' | translate }} - - + + + + admin.2fa.issuer-name + + + {{ "admin.2fa.issuer-name-required" | translate }} + + + +
+ + admin.2fa.verification-message-template + + + {{ "admin.2fa.verification-message-template-required" | translate }} + + + {{ "admin.2fa.verification-message-template-pattern" | translate }} + + - - admin.oauth2.client-secret - - - {{ 'admin.oauth2.client-secret-required' | translate }} - - - {{ 'admin.oauth2.client-secret-max-length' | translate }} - - -
+ + admin.2fa.verification-code-lifetime + + + {{ "admin.2fa.verification-code-lifetime-required" | translate }} + + + {{ "admin.2fa.verification-code-lifetime-pattern" | translate }} + + +
+ +
+
+
+
+
+
- - - -
- -
-
- -
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.scss b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.scss index e69de29bb2..8fb412f648 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.scss +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.scss @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2022 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. + */ + +:host{ + .container { + margin-bottom: 1em; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts index 1995605786..992c1c8ae4 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts @@ -1,10 +1,26 @@ +/// +/// Copyright © 2016-2022 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 { Component, Inject, OnDestroy, OnInit } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { ActivatedRoute } from '@angular/router'; -import { AbstractControl, FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { DialogService } from '@core/services/dialog.service'; import { TranslateService } from '@ngx-translate/core'; import { WINDOW } from '@core/services/window.service'; @@ -12,11 +28,7 @@ import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentica import { AuthState } from '@core/auth/auth.models'; import { getCurrentAuthState } from '@core/auth/auth.selectors'; import { Authority } from '@shared/models/authority.enum'; -import { - TwoFactorAuthProviderType, - TwoFactorAuthSettings, - TwoFactorAuthSettingsForm -} from '@shared/models/two-factor-auth.models'; +import { TwoFactorAuthProviderType, TwoFactorAuthSettings } from '@shared/models/two-factor-auth.models'; @Component({ selector: 'tb-2fa-settings', @@ -29,6 +41,8 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI private authUser = this.authState.authUser; twoFaFormGroup: FormGroup; + twoFactorAuthProviderTypes = Object.keys(TwoFactorAuthProviderType); + twoFactorAuthProviderType = TwoFactorAuthProviderType; constructor(protected store: Store, private route: ActivatedRoute, @@ -43,7 +57,7 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI ngOnInit() { this.build2faSettingsForm(); this.twoFaService.getTwoFaSettings().subscribe((setting) => { - console.log(this.formDataPreprocessing(setting)); + this.initTwoFactorAuthForm(setting); }); } @@ -60,12 +74,19 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI } save() { - + const setting = this.twoFaFormGroup.value; + this.twoFaService.saveTwoFaSettings(setting).subscribe( + (twoFactorAuthSettings) => { + this.twoFaFormGroup.patchValue(twoFactorAuthSettings, {emitEvent: false}); + this.twoFaFormGroup.markAsUntouched(); + this.twoFaFormGroup.markAsPristine(); + } + ); } private build2faSettingsForm(): void { this.twoFaFormGroup = this.fb.group({ - useSystemTwoFactorAuthSettings: [false], + useSystemTwoFactorAuthSettings: [this.isTenantAdmin()], maxVerificationFailuresBeforeUserLockout: [30, [ Validators.required, Validators.pattern(/^\d*$/), @@ -77,13 +98,20 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI Validators.min(1), Validators.pattern(/^\d*$/) ]], - verificationCodeCheckRateLimit: ['', Validators.pattern(/^[1-9]\d*:[1-9]\d*$/)], - verificationCodeSendRateLimit: ['', Validators.pattern(/^[1-9]\d*:[1-9]\d*$/)], + verificationCodeCheckRateLimit: ['3:900', [Validators.required, Validators.pattern(/^[1-9]\d*:[1-9]\d*$/)]], + verificationCodeSendRateLimit: ['1:60', [Validators.required, Validators.pattern(/^[1-9]\d*:[1-9]\d*$/)]], providers: this.fb.array([]) }); } - addProviders() { + private initTwoFactorAuthForm(settings: TwoFactorAuthSettings) { + settings.providers.forEach(() => { + this.addProvider(); + }); + this.twoFaFormGroup.patchValue(settings, {emitEvent: false}); + } + + addProvider() { const newProviders = this.fb.group({ providerType: [TwoFactorAuthProviderType.TOTP], issuerName: ['', Validators.required], @@ -119,8 +147,9 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI }); if (this.providersForm.length) { const selectProvidersType = this.providersForm.value[0].providerType; - if (selectProvidersType !== TwoFactorAuthProviderType.TOTP) { - newProviders.get('providerType').patchValue(TwoFactorAuthProviderType.SMS, {emitEvents: true}) + if (selectProvidersType === TwoFactorAuthProviderType.TOTP) { + newProviders.get('providerType').setValue(TwoFactorAuthProviderType.SMS); + newProviders.updateValueAndValidity(); } } this.providersForm.push(newProviders); @@ -140,16 +169,10 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI return this.twoFaFormGroup.get('providers') as FormArray; } - private formDataPreprocessing(data: TwoFactorAuthSettings): TwoFactorAuthSettingsForm { - return data; - } - - private formDataPostprocessing(data: TwoFactorAuthSettingsForm): TwoFactorAuthSettings{ - return data; - } - - trackByParams(index: number): number { - return index; + selectedTypes(type: TwoFactorAuthProviderType, index: number): boolean { + const selectedProviderTypes: TwoFactorAuthProviderType[] = this.providersForm.value.map(providers => providers.providerType); + selectedProviderTypes.splice(index, 1); + return selectedProviderTypes.includes(type); } } diff --git a/ui-ngx/src/app/shared/models/two-factor-auth.models.ts b/ui-ngx/src/app/shared/models/two-factor-auth.models.ts index b2c32e6156..ab64143a66 100644 --- a/ui-ngx/src/app/shared/models/two-factor-auth.models.ts +++ b/ui-ngx/src/app/shared/models/two-factor-auth.models.ts @@ -1,3 +1,19 @@ +/// +/// Copyright © 2016-2022 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 interface TwoFactorAuthSettings { maxVerificationFailuresBeforeUserLockout: number; providers: Array; @@ -7,7 +23,7 @@ export interface TwoFactorAuthSettings { verificationCodeSendRateLimit: string; } -export type TwoFactorAuthProviderConfig = Partial +export type TwoFactorAuthProviderConfig = Partial; export interface TotpTwoFactorAuthProviderConfig { providerType: TwoFactorAuthProviderType; @@ -24,7 +40,3 @@ export enum TwoFactorAuthProviderType{ TOTP = 'TOTP', SMS = 'SMS' } - -export interface TwoFactorAuthSettingsForm extends TwoFactorAuthSettings { - -} diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 642592b669..89b9bc5d2d 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -255,19 +255,29 @@ }, "2fa": { "2fa": "Two-factor authentication", - "use-system-two-factor-auth-settings": "Use system two factor auth settings", - "total-allowed-time-for-verification": "Total allowed time for verification", - "total-allowed-time-for-verification-required": "Total allowed time is required.", - "total-allowed-time-for-verification-pattern": "Total allowed time must be a positive integer.", + "available-providers": "Available providers:", + "issuer-name": "Issuer name", + "issuer-name-required": "Issuer name is required.", "max-verification-failures-before-user-lockout": "Max verification failures before user lockout", - "max-verification-failures-before-user-lockout-required": "Max verification failures is required.", "max-verification-failures-before-user-lockout-pattern": "Max verification failures must be a positive integer.", + "max-verification-failures-before-user-lockout-required": "Max verification failures is required.", + "provider": "Provider", + "total-allowed-time-for-verification": "Total allowed time for verification", + "total-allowed-time-for-verification-pattern": "Total allowed time must be a positive integer.", + "total-allowed-time-for-verification-required": "Total allowed time is required.", + "use-system-two-factor-auth-settings": "Use system two factor auth settings", "verification-code-check-rate-limit": "Verification code check rate limit", - "verification-code-check-rate-limit-hint": "If empty field, the limit not be apply", "verification-code-check-rate-limit-pattern": "Verification code check limit has invalid format", + "verification-code-check-rate-limit-required": "Verification code check rate limit is required.", + "verification-code-lifetime": "Verification code lifetime", + "verification-code-lifetime-pattern": "Verification code lifetime must be a positive integer.", + "verification-code-lifetime-required": "Verification code lifetime is required.", "verification-code-send-rate-limit": "Verification code send rate limit", - "verification-code-send-rate-limit-hint": "If empty field, the limit not be apply", - "verification-code-send-rate-limit-pattern": "Verification code send limit has invalid format" + "verification-code-send-rate-limit-pattern": "Verification code send limit has invalid format", + "verification-code-send-rate-limit-required": "Verification code send rate limit is required.", + "verification-message-template": "Verification message template", + "verification-message-template-pattern": "Verification message need to contains pattern: ${verificationCode}", + "verification-message-template-required": "Verification message template is required." } }, "alarm": { From 66a5222fcd125ae20af693fc5dcde8a5e96439f2 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Fri, 29 Apr 2022 18:13:33 +0300 Subject: [PATCH 074/276] @Transactional(propagation = Propagation.SUPPORTS) for the BaseRelationService --- .../AbstractRuleEngineControllerTest.java | 4 ++++ .../server/dao/relation/BaseRelationService.java | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java index 40cce12a12..ab902b82c6 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java @@ -18,6 +18,7 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.TestPropertySource; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Event; import org.thingsboard.server.common.data.id.EntityId; @@ -35,6 +36,9 @@ import java.util.function.Predicate; /** * Created by ashvayka on 20.03.18. */ +@TestPropertySource(properties = { + "js.evaluator=mock", +}) public abstract class AbstractRuleEngineControllerTest extends AbstractControllerTest { @Autowired diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index a5f0e04700..e62669586e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -29,6 +29,7 @@ import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Caching; import org.springframework.dao.ConcurrencyFailureException; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.EntityType; @@ -87,6 +88,7 @@ public class BaseRelationService implements RelationService { } @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType, #typeGroup}") + @Transactional(propagation = Propagation.SUPPORTS) @Override public EntityRelation getRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { return getRelation(tenantId, from, to, relationType, typeGroup); @@ -107,7 +109,7 @@ public class BaseRelationService implements RelationService { @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup, 'TO'}") }) @Override - @Transactional + @Transactional(propagation = Propagation.SUPPORTS) public boolean saveRelation(TenantId tenantId, EntityRelation relation) { log.trace("Executing saveRelation [{}]", relation); validate(relation); @@ -135,6 +137,7 @@ public class BaseRelationService implements RelationService { @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup, 'TO'}"), @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup, 'TO'}") }) + @Transactional(propagation = Propagation.SUPPORTS) @Override public boolean deleteRelation(TenantId tenantId, EntityRelation relation) { log.trace("Executing deleteRelation [{}]", relation); @@ -163,6 +166,7 @@ public class BaseRelationService implements RelationService { @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #typeGroup, 'TO'}"), @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType, #typeGroup, 'TO'}") }) + @Transactional(propagation = Propagation.SUPPORTS) @Override public boolean deleteRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { log.trace("Executing deleteRelation [{}][{}][{}][{}]", from, to, relationType, typeGroup); @@ -185,6 +189,7 @@ public class BaseRelationService implements RelationService { return relationDao.deleteRelationAsync(tenantId, from, to, relationType, typeGroup); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public void deleteEntityRelations(TenantId tenantId, EntityId entityId) { log.trace("Executing deleteEntityRelations [{}]", entityId); @@ -316,6 +321,7 @@ public class BaseRelationService implements RelationService { } @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup, 'FROM'}") + @Transactional(propagation = Propagation.SUPPORTS) @Override public List findByFrom(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup) { validate(from); @@ -379,6 +385,7 @@ public class BaseRelationService implements RelationService { @Override public List findByFromAndType(TenantId tenantId, EntityId from, String relationType, RelationTypeGroup typeGroup) { try { + //TODO refactor return findByFromAndTypeAsync(tenantId, from, relationType, typeGroup).get(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); @@ -395,6 +402,7 @@ public class BaseRelationService implements RelationService { } @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#to, #typeGroup, 'TO'}") + @Transactional(propagation = Propagation.SUPPORTS) @Override public List findByTo(TenantId tenantId, EntityId to, RelationTypeGroup typeGroup) { validate(to); @@ -468,6 +476,7 @@ public class BaseRelationService implements RelationService { @Override public List findByToAndType(TenantId tenantId, EntityId to, String relationType, RelationTypeGroup typeGroup) { try { + //TODO refactor return findByToAndTypeAsync(tenantId, to, relationType, typeGroup).get(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); @@ -539,8 +548,10 @@ public class BaseRelationService implements RelationService { }, MoreExecutors.directExecutor()); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public void removeRelations(TenantId tenantId, EntityId entityId) { + log.trace("removeRelations {}", entityId); Cache cache = cacheManager.getCache(RELATIONS_CACHE); List relations = new ArrayList<>(); @@ -550,8 +561,8 @@ public class BaseRelationService implements RelationService { } for (EntityRelation relation : relations) { - cacheEviction(relation, cache); deleteRelation(tenantId, relation); + cacheEviction(relation, cache); } } From aba3974a77e1fc465bc6c28dc25fe97d5157be31 Mon Sep 17 00:00:00 2001 From: fe-dev Date: Fri, 29 Apr 2022 19:18:11 +0300 Subject: [PATCH 075/276] UI: add queues to tenant profile --- ui-ngx/src/app/modules/common/modules-map.ts | 4 + .../home/components/home-components.module.ts | 10 +- .../profile/device-profile.component.ts | 4 +- .../queue/tenant-profile-queue.component.html | 194 +++++++++++++++++ .../queue/tenant-profile-queue.component.scss | 22 ++ .../queue/tenant-profile-queue.component.ts | 206 ++++++++++++++++++ .../tenant-profile-queues.component.html | 42 ++++ .../tenant-profile-queues.component.scss | 31 +++ .../queue/tenant-profile-queues.component.ts | 178 +++++++++++++++ .../tenant-profile-data.component.html | 5 - .../profile/tenant-profile-data.component.ts | 2 +- .../profile/tenant-profile.component.html | 24 +- .../profile/tenant-profile.component.scss | 10 + .../profile/tenant-profile.component.ts | 78 ++++--- .../pages/admin/queue/queue.component.html | 204 +++++++++-------- .../pages/admin/queue/queue.component.scss | 43 +--- .../home/pages/admin/queue/queue.component.ts | 6 +- ui-ngx/src/app/shared/models/queue.models.ts | 7 +- ui-ngx/src/app/shared/models/tenant.model.ts | 4 +- .../assets/locale/locale.constant-en_US.json | 5 +- 20 files changed, 878 insertions(+), 201 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queue.component.html create mode 100644 ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queue.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queue.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.html create mode 100644 ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index b4f4ea1328..bfc8f60604 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -287,6 +287,8 @@ import * as DisplayWidgetTypesPanelComponent from '@home/components/dashboard-pa import * as AlarmDurationPredicateValueComponent from '@home/components/profile/alarm/alarm-duration-predicate-value.component'; import * as DashboardImageDialogComponent from '@home/components/dashboard-page/dashboard-image-dialog.component'; import * as WidgetContainerComponent from '@home/components/widget/widget-container.component'; +import * as TenantProfileQueuesComponent from '@home/components/profile/queue/tenant-profile-queues.component'; +import { TenantProfileQueueComponent } from '@home/components/profile/queue/tenant-profile-queue.component'; import { IModulesMap } from '@modules/common/modules-map.models'; @@ -570,6 +572,8 @@ class ModulesMap implements IModulesMap { '@home/components/profile/alarm/alarm-duration-predicate-value.component': AlarmDurationPredicateValueComponent, '@home/components/dashboard-page/dashboard-image-dialog.component': DashboardImageDialogComponent, '@home/components/widget/widget-container.component': WidgetContainerComponent, + '@home/components/profile/queue/tenant-profile-queues.component': TenantProfileQueuesComponent, + '@home/components/profile/queue/tenant-profile-queue.component': TenantProfileQueueComponent }; init() { diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 0c58328e46..282a494026 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -148,6 +148,8 @@ import { } from '@home/components/tokens'; import { DashboardStateComponent } from '@home/components/dashboard-page/dashboard-state.component'; import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; +import { TenantProfileQueuesComponent } from '@home/components/profile/queue/tenant-profile-queues.component'; +import { TenantProfileQueueComponent } from '@home/components/profile/queue/tenant-profile-queue.component'; @NgModule({ declarations: @@ -267,7 +269,9 @@ import { EntityDetailsPageComponent } from '@home/components/entity/entity-detai DashboardStateDialogComponent, DashboardImageDialogComponent, EmbedDashboardDialogComponent, - DisplayWidgetTypesPanelComponent + DisplayWidgetTypesPanelComponent, + TenantProfileQueuesComponent, + TenantProfileQueueComponent ], imports: [ CommonModule, @@ -380,7 +384,9 @@ import { EntityDetailsPageComponent } from '@home/components/entity/entity-detai DashboardStateDialogComponent, DashboardImageDialogComponent, EmbedDashboardDialogComponent, - DisplayWidgetTypesPanelComponent + DisplayWidgetTypesPanelComponent, + TenantProfileQueuesComponent, + TenantProfileQueueComponent ], providers: [ WidgetComponentService, diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts index d30f5a8753..0d33079b0b 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts @@ -42,7 +42,7 @@ import { ServiceType } from '@shared/models/queue.models'; import { EntityId } from '@shared/models/id/entity-id'; import { OtaUpdateType } from '@shared/models/ota-package.models'; import { DashboardId } from '@shared/models/id/dashboard-id'; -import { QueueId } from "@shared/models/id/queue-id"; +import { QueueId } from '@shared/models/id/queue-id'; @Component({ selector: 'tb-device-profile', @@ -198,7 +198,7 @@ export class DeviceProfileComponent extends EntityComponent { }}, {emitEvent: false}); this.entityForm.patchValue({defaultRuleChainId: entity.defaultRuleChainId ? entity.defaultRuleChainId.id : null}, {emitEvent: false}); this.entityForm.patchValue({defaultDashboardId: entity.defaultDashboardId ? entity.defaultDashboardId.id : null}, {emitEvent: false}); - this.entityForm.patchValue({defaultQueueId: entity.defaultQueueId ? entity.defaultQueueId.id: null}, {emitEvent: false}); + this.entityForm.patchValue({defaultQueueId: entity.defaultQueueId ? entity.defaultQueueId.id : null}, {emitEvent: false}); this.entityForm.patchValue({firmwareId: entity.firmwareId}, {emitEvent: false}); this.entityForm.patchValue({softwareId: entity.softwareId}, {emitEvent: false}); this.entityForm.patchValue({description: entity.description}, {emitEvent: false}); diff --git a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queue.component.html b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queue.component.html new file mode 100644 index 0000000000..07bcb5d9e8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queue.component.html @@ -0,0 +1,194 @@ + + + +
+ +
+ {{ queueTitle }} +
+
+ + +
+
+ +
+ + + admin.queue-name + + + {{ 'queue.name-required' | translate }} + + + + queue.poll-interval + + + {{ 'queue.poll-interval-required' | translate }} + + + {{ 'queue.poll-interval-min-value' | translate }} + + + + queue.partitions + + + {{ 'queue.partitions-required' | translate }} + + + {{ 'queue.partitions-min-value' | translate }} + + + + +
{{ 'queue.consumer-per-partition' | translate }}
+
{{'queue.consumer-per-partition-hint' | translate}}
+
+ + + queue.processing-timeout + + + {{ 'queue.pack-processing-timeout-required' | translate }} + + + {{ 'queue.pack-processing-timeout-min-value' | translate }} + + + + + + + queue.submit-strategy + + + +
+ + queue.submit-strategy + + + {{ strategy }} + + + + {{ 'queue.submit-strategy-type-required' | translate }} + + + + queue.batch-size + + + {{ 'queue.batch-size-required' | translate }} + + + {{ 'queue.batch-size-min-value' | translate }} + + +
+
+
+ + + + queue.processing-strategy + + + +
+ + queue.processing-strategy + + + {{ strategy }} + + + + {{ 'queue.processing-strategy-type-required' | translate }} + + + + queue.retries + + + {{ 'queue.retries-required' | translate }} + + + {{ 'queue.retries-min-value' | translate }} + + + + queue.failure-percentage + + + {{ 'queue.failure-percentage-required' | translate }} + + + {{ 'queue.failure-percentage-min-value' | translate }} + + + {{ 'queue.failure-percentage-max-value' | translate }} + + + + queue.pause-between-retries + + + {{ 'queue.pause-between-retries-required' | translate }} + + + {{ 'queue.pause-between-retries-min-value' | translate }} + + + + queue.max-pause-between-retries + + + {{ 'queue.max-pause-between-retries-required' | translate }} + + + {{ 'queue.max-pause-between-retries-min-value' | translate }} + + +
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queue.component.scss b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queue.component.scss new file mode 100644 index 0000000000..d20faee49c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queue.component.scss @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2022 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. + */ +:host ::ng-deep { + .queue-strategy { + .mat-expansion-panel-body { + padding-bottom: 0 !important; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queue.component.ts b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queue.component.ts new file mode 100644 index 0000000000..7eb0062293 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queue.component.ts @@ -0,0 +1,206 @@ +/// +/// Copyright © 2016-2022 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 { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { DeviceProfileAlarm } from '@shared/models/device.models'; +import { MatDialog } from '@angular/material/dialog'; +import { UtilsService } from '@core/services/utils.service'; +import { QueueProcessingStrategyTypes, QueueSubmitStrategyTypes } from '@shared/models/queue.models'; + +@Component({ + selector: 'tb-tenant-profile-queue', + templateUrl: './tenant-profile-queue.component.html', + styleUrls: ['./tenant-profile-queue.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TenantProfileQueueComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TenantProfileQueueComponent), + multi: true, + } + ] +}) +export class TenantProfileQueueComponent implements ControlValueAccessor, OnInit, Validator { + + @Input() + disabled: boolean; + + @Output() + removeQueue = new EventEmitter(); + + @Input() + expanded = false; + + @Input() + mainQueue = false; + + @Input() + newQueue = false; + + private modelValue: DeviceProfileAlarm; + + queueFormGroup: FormGroup; + + submitStrategies: string[] = []; + processingStrategies: string[] = []; + + hideBatchSize = false; + + private propagateChange = null; + private propagateChangePending = false; + + constructor(private dialog: MatDialog, + private utils: UtilsService, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + if (this.propagateChangePending) { + this.propagateChangePending = false; + setTimeout(() => { + this.propagateChange(this.modelValue); + }, 0); + } + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.submitStrategies = Object.values(QueueSubmitStrategyTypes); + this.processingStrategies = Object.values(QueueProcessingStrategyTypes); + this.queueFormGroup = this.fb.group( + { + name: ['', [Validators.required]], + pollInterval: [25, [Validators.min(1), Validators.required]], + partitions: [10, [Validators.min(1), Validators.required]], + consumerPerPartition: [false, []], + packProcessingTimeout: [2000, [Validators.min(1), Validators.required]], + submitStrategy: this.fb.group({ + type: [null, [Validators.required]], + batchSize: [0, [Validators.min(1), Validators.required]], + }), + processingStrategy: this.fb.group({ + type: [null, [Validators.required]], + retries: [3, [Validators.min(0), Validators.required]], + failurePercentage: [ 0, [Validators.min(0), Validators.required, Validators.max(100)]], + pauseBetweenRetries: [3, [Validators.min(1), Validators.required]], + maxPauseBetweenRetries: [3, [Validators.min(1), Validators.required]], + }), + topic: [''] + }); + this.queueFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + this.queueFormGroup.get('name').valueChanges.subscribe((value) => this.queueFormGroup.patchValue({topic: `tb_rule_engine.${value}`})); + this.queueFormGroup.get('submitStrategy').get('type').valueChanges.subscribe(() => { + this.submitStrategyTypeChanged(); + }); + if (this.newQueue) { + this.queueFormGroup.get('name').enable({emitEvent: false}); + } else { + this.queueFormGroup.get('name').disable({emitEvent: false}); + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.queueFormGroup.disable({emitEvent: false}); + } else { + this.queueFormGroup.enable({emitEvent: false}); + this.queueFormGroup.get('name').disable({emitEvent: false}); + } + } + + writeValue(value: DeviceProfileAlarm): void { + this.propagateChangePending = false; + this.modelValue = value; + if (!this.modelValue.alarmType) { + this.expanded = true; + } + this.queueFormGroup.reset(this.modelValue || undefined, {emitEvent: false}); + if (!this.disabled && !this.queueFormGroup.valid) { + this.updateModel(); + } + } + + public validate(c: FormControl) { + if (c.parent) { + const queueName = c.value.name; + const profileQueues = []; + c.parent.getRawValue().forEach((queue) => { + profileQueues.push(queue.name); + } + ); + if (profileQueues.filter(profileQueue => profileQueue === queueName).length > 1) { + this.queueFormGroup.get('name').setErrors({ + unique: true + }); + } + } + return (this.queueFormGroup.valid) ? null : { + queue: { + valid: false, + }, + }; + } + + get queueTitle(): string { + const queueName = this.queueFormGroup.get('name').value; + return this.utils.customTranslation(queueName, queueName); + } + + private updateModel() { + const value = this.queueFormGroup.value; + this.modelValue = {...this.modelValue, ...value}; + if (this.propagateChange) { + this.propagateChange(this.modelValue); + } else { + this.propagateChangePending = true; + } + } + + submitStrategyTypeChanged() { + const form = this.queueFormGroup.get('submitStrategy') as FormGroup; + const type: QueueSubmitStrategyTypes = form.get('type').value; + const batchSizeField = form.get('batchSize'); + if (type === QueueSubmitStrategyTypes.BATCH) { + batchSizeField.enable(); + batchSizeField.patchValue(1000); + this.hideBatchSize = true; + } else { + batchSizeField.patchValue(null); + batchSizeField.disable(); + this.hideBatchSize = false; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.html b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.html new file mode 100644 index 0000000000..59b7e063a1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.html @@ -0,0 +1,42 @@ + +
+
+ +
+ + +
+
+
+
+ tenant-profile.no-queue +
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.scss b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.scss new file mode 100644 index 0000000000..9702d807a6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.scss @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2022 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 '../../../../../../scss/constants'; + +:host { + .tb-tenant-profile-queues { + &.mat-padding { + padding: 8px; + @media #{$mat-gt-sm} { + padding: 16px; + } + } + } + + .tb-prompt{ + margin: 30px 0; + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts new file mode 100644 index 0000000000..abcde44419 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts @@ -0,0 +1,178 @@ +/// +/// Copyright © 2016-2022 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 { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { Subscription } from 'rxjs'; +import { QueueInfo } from '@shared/models/queue.models'; + +@Component({ + selector: 'tb-tenant-profile-queues', + templateUrl: './tenant-profile-queues.component.html', + styleUrls: ['./tenant-profile-queues.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TenantProfileQueuesComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TenantProfileQueuesComponent), + multi: true, + } + ] +}) +export class TenantProfileQueuesComponent implements ControlValueAccessor, OnInit, Validator { + + tenantProfileQueuesFormGroup: FormGroup; + newQueue = false; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private valueChangeSubscription: Subscription = null; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.tenantProfileQueuesFormGroup = this.fb.group({ + queues: this.fb.array([]) + }); + } + + queuesFormArray(): FormArray { + return this.tenantProfileQueuesFormGroup.get('queues') as FormArray; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.tenantProfileQueuesFormGroup.disable({emitEvent: false}); + } else { + this.tenantProfileQueuesFormGroup.enable({emitEvent: false}); + } + } + + writeValue(queues: Array | null): void { + if (this.valueChangeSubscription) { + this.valueChangeSubscription.unsubscribe(); + } + const queuesControls: Array = []; + if (queues) { + queues.forEach((queue) => { + queuesControls.push(this.fb.control(queue, [Validators.required])); + }); + } + this.tenantProfileQueuesFormGroup.setControl('queues', this.fb.array(queuesControls)); + if (this.disabled) { + this.tenantProfileQueuesFormGroup.disable({emitEvent: false}); + } else { + this.tenantProfileQueuesFormGroup.enable({emitEvent: false}); + } + this.valueChangeSubscription = this.tenantProfileQueuesFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + public trackByQueue(index: number, queueControl: AbstractControl): string { + if (queueControl) { + return queueControl.value.id; + } else { + return null; + } + } + + public removeQueue(index: number) { + (this.tenantProfileQueuesFormGroup.get('queues') as FormArray).removeAt(index); + } + + public addQueue() { + const queue = { + consumerPerPartition: false, + name: '', + packProcessingTimeout: 2000, + partitions: 10, + pollInterval: 25, + processingStrategy: { + failurePercentage: 0, + maxPauseBetweenRetries: 3, + pauseBetweenRetries: 3, + retries: 3, + type: '' + }, + submitStrategy: { + batchSize: 0, + type: '' + }, + topic: '' + }; + this.newQueue = true; + const queuesArray = this.tenantProfileQueuesFormGroup.get('queues') as FormArray; + queuesArray.push(this.fb.control(queue, [])); + this.tenantProfileQueuesFormGroup.updateValueAndValidity(); + if (!this.tenantProfileQueuesFormGroup.valid) { + this.updateModel(); + } + } + + public validate(c: FormControl) { + return (this.tenantProfileQueuesFormGroup.valid) ? null : { + queues: { + valid: false, + }, + }; + } + + private updateModel() { + const queues: Array = this.tenantProfileQueuesFormGroup.get('queues').value; + this.propagateChange(queues); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.html index 115a224374..81ca768911 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.html @@ -16,11 +16,6 @@ -->
- - - - - diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.ts index 421915ed83..af59fbdf17 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.ts @@ -79,7 +79,7 @@ export class TenantProfileDataComponent implements ControlValueAccessor, OnInit } writeValue(value: TenantProfileData | null): void { - this.tenantProfileDataFormGroup.patchValue({configuration: value?.configuration}, {emitEvent: false}); + this.tenantProfileDataFormGroup.patchValue({configuration: value}, {emitEvent: false}); } private updateModel() { diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html index e2eb776e5b..4b511037e7 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html @@ -68,10 +68,26 @@
{{'tenant.isolated-tb-rule-engine-details' | translate}}
- - + + + + +
{{'tenant-profile.queues-with-count' | translate: + {count: entityForm.get('profileData').get('queueConfiguration').value ? + entityForm.get('profileData').get('queueConfiguration').value.length : 0} }}
+
+
+ + + +
+ + +
tenant-profile.description diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.scss b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.scss index 21c4f53c08..e6e5d3b9cf 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.scss +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.scss @@ -25,4 +25,14 @@ white-space: normal; } } + .fields-group { + padding: 0 16px 8px; + margin-bottom: 10px; + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + legend { + color: rgba(0, 0, 0, .7); + width: fit-content; + } + } } diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts index 9fd3bb47dd..285cecc5e6 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts @@ -18,12 +18,7 @@ import { ChangeDetectorRef, Component, Inject, Input, Optional } from '@angular/ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { - createTenantProfileConfiguration, - TenantProfile, - TenantProfileData, - TenantProfileType -} from '@shared/models/tenant.model'; +import { createTenantProfileConfiguration, TenantProfile, TenantProfileType } from '@shared/models/tenant.model'; import { ActionNotificationShow } from '@app/core/notification/notification.actions'; import { TranslateService } from '@ngx-translate/core'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; @@ -48,11 +43,6 @@ export class TenantProfileComponent extends EntityComponent { super(store, fb, entityValue, entitiesTableConfigValue, cd); } - ngOnInit() { - this.showQueueParams(); - this.entityForm.get('isolatedTbRuleEngine').valueChanges.subscribe(() => this.showQueueParams()); - } - hideDelete() { if (this.entitiesTableConfig) { return !this.entitiesTableConfig.deleteEnabled(this.entity); @@ -62,37 +52,61 @@ export class TenantProfileComponent extends EntityComponent { } buildForm(entity: TenantProfile): FormGroup { - return this.fb.group( + const mainQueue = [ + { + consumerPerPartition: true, + name: 'Main', + packProcessingTimeout: 2000, + partitions: 10, + pollInterval: 25, + processingStrategy: { + failurePercentage: 0, + maxPauseBetweenRetries: 3, + pauseBetweenRetries: 3, + retries: 3, + type: 'SKIP_ALL_FAILURES' + }, + submitStrategy: { + batchSize: 1000, + type: 'BURST' + }, + topic: 'tb_rule_engine.main' + } + ]; + const formGroup = this.fb.group( { name: [entity ? entity.name : '', [Validators.required, Validators.maxLength(255)]], isolatedTbCore: [entity ? entity.isolatedTbCore : false, []], isolatedTbRuleEngine: [entity ? entity.isolatedTbRuleEngine : false, []], - profileData: [entity && !this.isAdd ? entity.profileData : { - configuration: createTenantProfileConfiguration(TenantProfileType.DEFAULT) - } as TenantProfileData, []], + profileData: this.fb.group({ + configuration: [entity && !this.isAdd ? entity?.profileData.configuration + : createTenantProfileConfiguration(TenantProfileType.DEFAULT), []], + queueConfiguration: [null, []] + }), description: [entity ? entity.description : '', []], } ); + formGroup.get('isolatedTbRuleEngine').valueChanges.subscribe((value) => { + if (value) { + formGroup.get('profileData').patchValue({ + queueConfiguration: mainQueue + }, {emitEvent: false}); + } else { + formGroup.get('profileData').patchValue({ + queueConfiguration: null + }, {emitEvent: false}); + } + }); + return formGroup; } updateForm(entity: TenantProfile) { - this.entityForm.patchValue({name: entity.name}); - this.entityForm.patchValue({isolatedTbCore: entity.isolatedTbCore}); - this.entityForm.patchValue({isolatedTbRuleEngine: entity.isolatedTbRuleEngine}); - this.entityForm.patchValue({profileData: !this.isAdd ? entity.profileData : { - configuration: createTenantProfileConfiguration(TenantProfileType.DEFAULT) - } as TenantProfileData}); - this.entityForm.patchValue({description: entity.description}); - } - - showQueueParams(): boolean { - let isolatedTbRuleEngine: boolean = this.entityForm.get('isolatedTbRuleEngine').value; - if (isolatedTbRuleEngine) { -//enable - } else { -//disable - } - return isolatedTbRuleEngine; + this.entityForm.patchValue({name: entity.name}, {emitEvent: false}); + this.entityForm.patchValue({isolatedTbCore: entity.isolatedTbCore}, {emitEvent: false}); + this.entityForm.patchValue({isolatedTbRuleEngine: entity.isolatedTbRuleEngine}, {emitEvent: false}); + this.entityForm.get('profileData').patchValue({configuration: entity.profileData?.configuration}, {emitEvent: false}); + this.entityForm.get('profileData').patchValue({queueConfiguration: entity.profileData?.queueConfiguration}, {emitEvent: false}); + this.entityForm.patchValue({description: entity.description}, {emitEvent: false}); } updateFormState() { diff --git a/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.html b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.html index ae8f08e348..9d7b9ead45 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.html @@ -20,12 +20,12 @@ [disabled]="(isLoading$ | async)" (click)="onEntityAction($event, 'delete')" [fxShow]="!hideDelete() && !isEdit"> - {{'queue.delete' | translate }} + {{ 'queue.delete' | translate }}
- +
admin.queue-name @@ -57,10 +57,6 @@ - - - -
{{ 'queue.consumer-per-partition' | translate }}
{{'queue.consumer-per-partition-hint' | translate}}
@@ -77,115 +73,113 @@ {{ 'queue.pack-processing-timeout-min-value' | translate }} - -
- + - - queue.submit-strategy - {{panel1.expanded ? 'keyboard_arrow_up' : 'keyboard_arrow_down' }} + + queue.submit-strategy -
- - queue.submit-strategy - - - {{ strategy }} - - - - {{ 'queue.submit-strategy-type-required' | translate }} - - - - queue.batch-size - - - {{ 'queue.batch-size-required' | translate }} - - - {{ 'queue.batch-size-min-value' | translate }} - - -
+ +
+ + queue.submit-strategy + + + {{ strategy }} + + + + {{ 'queue.submit-strategy-type-required' | translate }} + + + + queue.batch-size + + + {{ 'queue.batch-size-required' | translate }} + + + {{ 'queue.batch-size-min-value' | translate }} + + +
+
- + - - queue.processing-strategy - {{panel2.expanded ? 'keyboard_arrow_up' : 'keyboard_arrow_down' }} + + queue.processing-strategy - -
- - queue.processing-strategy - - - {{ strategy }} - - - - {{ 'queue.processing-strategy-type-required' | translate }} - - - - queue.retries - - - {{ 'queue.retries-required' | translate }} - - - {{ 'queue.retries-min-value' | translate }} - - - - queue.failure-percentage - - - {{ 'queue.failure-percentage-required' | translate }} - - - {{ 'queue.failure-percentage-min-value' | translate }} - - - {{ 'queue.failure-percentage-max-value' | translate }} - - - - queue.pause-between-retries - - - {{ 'queue.pause-between-retries-required' | translate }} - - - {{ 'queue.pause-between-retries-min-value' | translate }} - - - - queue.max-pause-between-retries - - - {{ 'queue.max-pause-between-retries-required' | translate }} - - - {{ 'queue.max-pause-between-retries-min-value' | translate }} - - -
+ +
+ + queue.processing-strategy + + + {{ strategy }} + + + + {{ 'queue.processing-strategy-type-required' | translate }} + + + + queue.retries + + + {{ 'queue.retries-required' | translate }} + + + {{ 'queue.retries-min-value' | translate }} + + + + queue.failure-percentage + + + {{ 'queue.failure-percentage-required' | translate }} + + + {{ 'queue.failure-percentage-min-value' | translate }} + + + {{ 'queue.failure-percentage-max-value' | translate }} + + + + queue.pause-between-retries + + + {{ 'queue.pause-between-retries-required' | translate }} + + + {{ 'queue.pause-between-retries-min-value' | translate }} + + + + queue.max-pause-between-retries + + + {{ 'queue.max-pause-between-retries-required' | translate }} + + + {{ 'queue.max-pause-between-retries-min-value' | translate }} + + +
+
-
diff --git a/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.scss b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.scss index 852e391b28..c87504819e 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.scss +++ b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.scss @@ -14,46 +14,9 @@ * limitations under the License. */ :host ::ng-deep { - - .mat-expansion-panel:not([class*='mat-elevation-z']) { - box-shadow: none; - } - - .mat-accordion-container { - margin-bottom: 16px; - } - - .mat-expansion-panel { - - &:hover { - box-shadow: 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 5px 5px -3px rgba(0, 0, 0, 0.2); - } - - &-header { - height: 56px; - border: 1px solid #f2f2f2; - - &-title { - align-items: center; - justify-content: space-between; - margin-right: 0; - } - } - - &.mat-expanded { - border: none; - box-shadow: 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 5px 5px -3px rgba(0, 0, 0, 0.2); - - .mat-expansion-panel { - - &-content { - margin-top: 20px; - } - - &-body { - padding-bottom: 10px; - } - } + .queue-form { + .mat-expansion-panel-body { + padding-bottom: 0 !important; } } } diff --git a/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.ts b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.ts index f9a60773df..f436264793 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.ts @@ -23,8 +23,6 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { TranslateService } from '@ngx-translate/core'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; -import set = Reflect.set; -import { distinctUntilChanged } from 'rxjs/operators'; @Component({ selector: 'tb-queue', @@ -39,7 +37,7 @@ export class QueueComponent extends EntityComponent { processingStrategies: string[] = []; QueueSubmitStrategyTypes = QueueSubmitStrategyTypes; - hideBatchSize: boolean = false; + hideBatchSize = false; constructor(protected store: Store, protected translate: TranslateService, @@ -140,7 +138,7 @@ export class QueueComponent extends EntityComponent { } submitStrategyTypeChanged() { - const form = this.entityForm.get("submitStrategy") as FormGroup; + const form = this.entityForm.get('submitStrategy') as FormGroup; const type: QueueSubmitStrategyTypes = form.get('type').value; const batchSizeField = form.get('batchSize'); if (type === QueueSubmitStrategyTypes.BATCH) { diff --git a/ui-ngx/src/app/shared/models/queue.models.ts b/ui-ngx/src/app/shared/models/queue.models.ts index 508cf2aee1..3224d7dc23 100644 --- a/ui-ngx/src/app/shared/models/queue.models.ts +++ b/ui-ngx/src/app/shared/models/queue.models.ts @@ -16,7 +16,7 @@ import { BaseData, HasId } from '@shared/models/base-data'; import { TenantId } from '@shared/models/id/tenant-id'; -import {QueueId} from "@shared/models/id/queue-id"; +import {QueueId} from '@shared/models/id/queue-id'; export enum ServiceType { TB_CORE = 'TB_CORE', @@ -43,9 +43,10 @@ export enum QueueProcessingStrategyTypes { } export interface QueueInfo extends BaseData { + name: string; packProcessingTimeout: number; partitions: number; - consumerPerPartition: boolean, + consumerPerPartition: boolean; pollInterval: number; processingStrategy: { type: QueueProcessingStrategyTypes, @@ -58,6 +59,6 @@ export interface QueueInfo extends BaseData { type: QueueSubmitStrategyTypes, batchSize: number, }; - tenantId: TenantId; + tenantId?: TenantId; topic: string; } diff --git a/ui-ngx/src/app/shared/models/tenant.model.ts b/ui-ngx/src/app/shared/models/tenant.model.ts index cceb1cc7d7..14c0d232df 100644 --- a/ui-ngx/src/app/shared/models/tenant.model.ts +++ b/ui-ngx/src/app/shared/models/tenant.model.ts @@ -18,7 +18,7 @@ import { ContactBased } from '@shared/models/contact-based.model'; import { TenantId } from './id/tenant-id'; import { TenantProfileId } from '@shared/models/id/tenant-profile-id'; import { BaseData } from '@shared/models/base-data'; -import {Validators} from "@angular/forms"; +import { QueueInfo } from '@shared/models/queue.models'; export enum TenantProfileType { DEFAULT = 'DEFAULT' @@ -98,7 +98,7 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan export interface TenantProfileData { configuration: TenantProfileConfiguration; - queueConfiguration?: any; + queueConfiguration?: QueueInfo; } export interface TenantProfile extends BaseData { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 4414e71847..880778d6c4 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2862,7 +2862,10 @@ "max-sms-range": "Maximum number of SMS sent can't be negative", "max-created-alarms": "Maximum number of alarms created (0 - unlimited)", "max-created-alarms-required": "Maximum number of alarms created is required.", - "max-created-alarms-range": "Maximum number of alarms created can't be negative" + "max-created-alarms-range": "Maximum number of alarms created can't be negative", + "no-queue": "No Queue configured", + "add-queue": "Add Queue", + "queues-with-count": "Queues ({{count}})" }, "timeinterval": { "seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }", From 7cda81ef86c9f8571ed1d270b019ca032821764a Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Sun, 1 May 2022 16:41:37 +0200 Subject: [PATCH 076/276] TbTenantService improvements --- .../service/entitiy/AbstractTbEntityService.java | 3 --- .../DefaultTbNotificationEntityService.java | 14 ++++++++++++++ .../entitiy/TbNotificationEntityService.java | 5 +++++ .../entitiy/tenant/DefaultTbTenantService.java | 8 +++----- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java index fcdf67449f..8b2a146315 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java @@ -88,9 +88,6 @@ public abstract class AbstractTbEntityService { protected AlarmService alarmService; @Autowired protected EntityActionService entityActionService; - @Autowired - protected TbClusterService tbClusterService; - @Autowired protected DeviceService deviceService; @Autowired protected AssetService assetService; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java index f247710b91..a41c5b2f87 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java @@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.action.EntityActionService; import org.thingsboard.server.service.gateway_device.GatewayNotificationsService; import org.thingsboard.server.service.security.model.SecurityUser; @@ -50,6 +51,7 @@ import java.util.List; @Slf4j @Service +@TbCoreComponent @RequiredArgsConstructor public class DefaultTbNotificationEntityService implements TbNotificationEntityService { private static final ObjectMapper json = new ObjectMapper(); @@ -97,6 +99,18 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS sendEntityAssignToEdgeNotificationMsg(tenantId, edgeId, entityId, edgeActionType); } + @Override + public void notifyCreateOruUpdateTenant(Tenant tenant, ComponentLifecycleEvent event) { + tbClusterService.onTenantChange(tenant, null); + tbClusterService.broadcastEntityStateChangeEvent(tenant.getId(), tenant.getId(), event); + } + + @Override + public void notifyDeleteTenant(Tenant tenant) { + tbClusterService.onTenantDelete(tenant, null); + tbClusterService.broadcastEntityStateChangeEvent(tenant.getId(), tenant.getId(), ComponentLifecycleEvent.DELETED); + } + @Override public void notifyCreateOrUpdateDevice(TenantId tenantId, DeviceId deviceId, CustomerId customerId, Device device, Device oldDevice, ActionType actionType, diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java index 36058908a1..e86f068203 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.service.security.model.SecurityUser; @@ -56,6 +57,10 @@ public interface TbNotificationEntityService { EdgeEventActionType edgeActionType, SecurityUser user, Object... additionalInfo); + void notifyCreateOruUpdateTenant(Tenant tenant, ComponentLifecycleEvent event); + + void notifyDeleteTenant(Tenant tenant); + void notifyCreateOrUpdateDevice(TenantId tenantId, DeviceId deviceId, CustomerId customerId, Device device, Device oldDevice, ActionType actionType, SecurityUser user, Object... additionalInfo); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java index 6e7fa9ea2e..a2230b900e 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java @@ -44,9 +44,8 @@ public class DefaultTbTenantService extends AbstractTbEntityService implements T installScripts.createDefaultEdgeRuleChains(savedTenant.getId()); } tenantProfileCache.evict(savedTenant.getId()); - tbClusterService.onTenantChange(savedTenant, null); - tbClusterService.broadcastEntityStateChangeEvent(savedTenant.getId(), savedTenant.getId(), - newTenant ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); + notificationEntityService.notifyCreateOruUpdateTenant(tenant, newTenant ? + ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); return savedTenant; } catch (Exception e) { throw handleException(e); @@ -59,8 +58,7 @@ public class DefaultTbTenantService extends AbstractTbEntityService implements T TenantId tenantId = tenant.getId(); tenantService.deleteTenant(tenantId); tenantProfileCache.evict(tenantId); - tbClusterService.onTenantDelete(tenant, null); - tbClusterService.broadcastEntityStateChangeEvent(tenantId, tenantId, ComponentLifecycleEvent.DELETED); + notificationEntityService.notifyDeleteTenant(tenant); } catch (Exception e) { throw handleException(e); } From e0d04c2805041f5520a210a06a4878280a819e4b Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Sun, 1 May 2022 21:56:59 +0200 Subject: [PATCH 077/276] fixed minor bugs --- .../server/service/entitiy/AbstractTbEntityService.java | 1 + .../server/service/entitiy/tenant/DefaultTbTenantService.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java index 8b2a146315..e2ccc1f3d2 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java @@ -88,6 +88,7 @@ public abstract class AbstractTbEntityService { protected AlarmService alarmService; @Autowired protected EntityActionService entityActionService; + @Autowired protected DeviceService deviceService; @Autowired protected AssetService assetService; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java index a2230b900e..ac3d651b6b 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java @@ -44,7 +44,7 @@ public class DefaultTbTenantService extends AbstractTbEntityService implements T installScripts.createDefaultEdgeRuleChains(savedTenant.getId()); } tenantProfileCache.evict(savedTenant.getId()); - notificationEntityService.notifyCreateOruUpdateTenant(tenant, newTenant ? + notificationEntityService.notifyCreateOruUpdateTenant(savedTenant, newTenant ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); return savedTenant; } catch (Exception e) { From f74f3a92933cdfc7497d5b2d8f6f44aee9df8bd0 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 2 May 2022 08:23:16 +0200 Subject: [PATCH 078/276] created TbQueueService and improvements --- .../server/controller/TenantController.java | 13 +- .../entity/queue/DefaultTbQueueService.java | 144 ++++++++++++++++++ .../service/entity/queue/TbQueueService.java | 29 ++++ .../entity/tenant/DefaultTbTenantService.java | 135 ++++++++++++++++ .../entity/tenant/TbTenantService.java | 23 +++ .../DefaultSystemDataLoaderService.java | 29 +++- .../DefaultTbRuleEngineConsumerService.java | 2 +- ...ice.java => TbQueueServiceDeprecated.java} | 2 +- .../server/dao/queue/QueueService.java | 4 +- .../server/dao/tenant/TenantService.java | 8 +- ...a => DefaultTbQueueServiceDeprecated.java} | 3 +- .../server/dao/queue/BaseQueueService.java | 108 +------------ .../server/dao/tenant/TenantServiceImpl.java | 85 +---------- 13 files changed, 383 insertions(+), 202 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/entity/queue/DefaultTbQueueService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/entity/queue/TbQueueService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/entity/tenant/DefaultTbTenantService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/entity/tenant/TbTenantService.java rename common/cluster-api/src/main/java/org/thingsboard/server/queue/{TbQueueService.java => TbQueueServiceDeprecated.java} (95%) rename common/queue/src/main/java/org/thingsboard/server/queue/{DefaultTbQueueService.java => DefaultTbQueueServiceDeprecated.java} (94%) diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantController.java b/application/src/main/java/org/thingsboard/server/controller/TenantController.java index 496cd60e81..e7fd8e24dc 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantController.java @@ -18,8 +18,8 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; @@ -39,6 +39,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entity.tenant.TbTenantService; import org.thingsboard.server.service.install.InstallScripts; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; @@ -63,14 +64,14 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI @TbCoreComponent @RequestMapping("/api") @Slf4j +@RequiredArgsConstructor public class TenantController extends BaseController { private static final String TENANT_INFO_DESCRIPTION = "The Tenant Info object extends regular Tenant object and includes Tenant Profile name. "; - @Autowired - private InstallScripts installScripts; - @Autowired - private TenantService tenantService; + private final InstallScripts installScripts; + private final TenantService tenantService; + private final TbTenantService tbTenantService; @ApiOperation(value = "Get Tenant (getTenantById)", notes = "Fetch the Tenant object based on the provided Tenant Id. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @@ -129,7 +130,7 @@ public class TenantController extends BaseController { checkEntity(tenant.getId(), tenant, Resource.TENANT); - tenant = checkNotNull(tenantService.saveTenant(tenant)); + tenant = checkNotNull(tbTenantService.saveTenant(tenant)); if (newTenant) { installScripts.createDefaultRuleChains(tenant.getId()); installScripts.createDefaultEdgeRuleChains(tenant.getId()); diff --git a/application/src/main/java/org/thingsboard/server/service/entity/queue/DefaultTbQueueService.java b/application/src/main/java/org/thingsboard/server/service/entity/queue/DefaultTbQueueService.java new file mode 100644 index 0000000000..006c9025ba --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entity/queue/DefaultTbQueueService.java @@ -0,0 +1,144 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.entity.queue; + +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.id.QueueId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.dao.queue.QueueService; +import org.thingsboard.server.queue.TbQueueAdmin; +import org.thingsboard.server.queue.TbQueueClusterService; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Slf4j +@Service +@TbCoreComponent +@AllArgsConstructor +public class DefaultTbQueueService implements TbQueueService { + private final QueueService queueService; + private final TbQueueClusterService queueClusterService; + private final TbQueueAdmin tbQueueAdmin; + + @Override + public Queue saveQueue(Queue queue) { + boolean create = queue.getId() == null; + Queue oldQueue; + + if (create) { + oldQueue = null; + } else { + oldQueue = queueService.findQueueById(queue.getTenantId(), queue.getId()); + } + + //TODO: add checkNotNull + Queue savedQueue = queueService.saveQueue(queue); + + if (create) { + onQueueCreated(savedQueue); + } else { + onQueueUpdated(savedQueue, oldQueue); + } + + return savedQueue; + } + + @Override + public void deleteQueue(TenantId tenantId, QueueId queueId) { + Queue queue = queueService.findQueueById(tenantId, queueId); + queueService.deleteQueue(tenantId, queueId); + onQueueDeleted(tenantId, queue); + } + + @Override + public void deleteQueueByQueueName(TenantId tenantId, String queueName) { + Queue queue = queueService.findQueueByTenantIdAndNameInternal(tenantId, queueName); + queueService.deleteQueue(tenantId, queue.getId()); + onQueueDeleted(tenantId, queue); + } + + private void onQueueCreated(Queue queue) { + if (tbQueueAdmin != null) { + for (int i = 0; i < queue.getPartitions(); i++) { + tbQueueAdmin.createTopicIfNotExists( + new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName()); + } + } + + if (queueClusterService != null) { + queueClusterService.onQueueChange(queue); + } + } + + private void onQueueUpdated(Queue queue, Queue oldQueue) { + int oldPartitions = oldQueue.getPartitions(); + int currentPartitions = queue.getPartitions(); + + if (currentPartitions != oldPartitions && tbQueueAdmin != null) { + if (currentPartitions > oldPartitions) { + log.info("Added [{}] new partitions to [{}] queue", currentPartitions - oldPartitions, queue.getName()); + for (int i = oldPartitions; i < currentPartitions; i++) { + tbQueueAdmin.createTopicIfNotExists( + new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName()); + } + if (queueClusterService != null) { + queueClusterService.onQueueChange(queue); + } + } else { + log.info("Removed [{}] partitions from [{}] queue", oldPartitions - currentPartitions, queue.getName()); + if (queueClusterService != null) { + queueClusterService.onQueueChange(queue); + } + await(); + for (int i = currentPartitions; i < oldPartitions; i++) { + tbQueueAdmin.deleteTopic( + new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName()); + } + } + } else if (!oldQueue.equals(queue) && queueClusterService != null) { + queueClusterService.onQueueChange(queue); + } + } + + private void onQueueDeleted(TenantId tenantId, Queue queue) { + if (queueClusterService != null) { + queueClusterService.onQueueDelete(queue); + await(); + } +// queueStatsService.deleteQueueStatsByQueueId(tenantId, queueId); + if (tbQueueAdmin != null) { + for (int i = 0; i < queue.getPartitions(); i++) { + String fullTopicName = new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName(); + log.debug("Deleting queue [{}]", fullTopicName); + try { + tbQueueAdmin.deleteTopic(fullTopicName); + } catch (Exception e) { + log.error("Failed to delete queue [{}]", fullTopicName); + } + } + } + } + + @SneakyThrows + private void await() { + Thread.sleep(3000); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/entity/queue/TbQueueService.java b/application/src/main/java/org/thingsboard/server/service/entity/queue/TbQueueService.java new file mode 100644 index 0000000000..1b37486ba4 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entity/queue/TbQueueService.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.entity.queue; + +import org.thingsboard.server.common.data.id.QueueId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.queue.Queue; + +public interface TbQueueService { + + Queue saveQueue(Queue queue); + + void deleteQueue(TenantId tenantId, QueueId queueId); + + void deleteQueueByQueueName(TenantId tenantId, String queueName); +} diff --git a/application/src/main/java/org/thingsboard/server/service/entity/tenant/DefaultTbTenantService.java b/application/src/main/java/org/thingsboard/server/service/entity/tenant/DefaultTbTenantService.java new file mode 100644 index 0000000000..015c88813b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entity/tenant/DefaultTbTenantService.java @@ -0,0 +1,135 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.entity.tenant; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; +import org.thingsboard.server.dao.queue.QueueService; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.dao.tenant.TenantProfileService; +import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entity.queue.TbQueueService; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +@Service +@TbCoreComponent +@AllArgsConstructor +public class DefaultTbTenantService implements TbTenantService { + + private final TenantService tenantService; + private final TbQueueService tbQueueService; + private final QueueService queueService; + private final TenantProfileService tenantProfileService; + private final TbTenantProfileCache tenantProfileCache; + + @Override + public Tenant saveTenant(Tenant tenant) { + boolean updated = tenant.getId() != null; + Tenant oldTenant = updated ? tenantService.findTenantById(tenant.getId()) : null; + List queues; + if (updated) { + + } + + Tenant savedTenant = tenantService.saveTenant(tenant); + tenantProfileCache.evict(tenant.getId()); + updateQueuesForTenant(oldTenant, savedTenant); + return savedTenant; + } + + public void updateQueuesForTenant(Tenant oldTenant, Tenant newTenant) { + TenantProfile oldTenantProfile = oldTenant != null ? tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, oldTenant.getTenantProfileId()) : null; + TenantProfile newTenantProfile = tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, newTenant.getTenantProfileId()); + + TenantId tenantId = newTenant.getId(); + + boolean oldIsolated = oldTenantProfile != null && oldTenantProfile.isIsolatedTbRuleEngine(); + boolean newIsolated = newTenantProfile.isIsolatedTbRuleEngine(); + + if (!oldIsolated && !newIsolated) { + return; + } + + if (newTenantProfile.equals(oldTenantProfile)) { + return; + } + + Map oldQueues; + Map newQueues; + + if (oldIsolated) { + oldQueues = oldTenantProfile.getProfileData().getQueueConfiguration().stream() + .collect(Collectors.toMap(TenantProfileQueueConfiguration::getName, q -> q)); + } else { + oldQueues = Collections.emptyMap(); + } + + if (newIsolated) { + newQueues = newTenantProfile.getProfileData().getQueueConfiguration().stream() + .collect(Collectors.toMap(TenantProfileQueueConfiguration::getName, q -> q)); + } else { + newQueues = Collections.emptyMap(); + } + + List toRemove = new ArrayList<>(); + List toCreate = new ArrayList<>(); + List toUpdate = new ArrayList<>(); + + for (String oldQueue : oldQueues.keySet()) { + if (!newQueues.containsKey(oldQueue)) { + toRemove.add(oldQueue); + } + } + + for (String newQueue : newQueues.keySet()) { + if (oldQueues.containsKey(newQueue)) { + toUpdate.add(newQueue); + } else { + toCreate.add(newQueue); + } + } + + toRemove.forEach(q -> tbQueueService.deleteQueueByQueueName(tenantId, q)); + + toCreate.forEach(key -> tbQueueService.saveQueue(new Queue(tenantId, newQueues.get(key)))); + + toUpdate.forEach(key -> { + Queue queueToUpdate = new Queue(tenantId, newQueues.get(key)); + Queue foundQueue = queueService.findQueueByTenantIdAndName(tenantId, key); + queueToUpdate.setId(foundQueue.getId()); + queueToUpdate.setCreatedTime(foundQueue.getCreatedTime()); + + if (queueToUpdate.equals(foundQueue)) { + //Queue not changed + } else { + tbQueueService.saveQueue(queueToUpdate); + } + }); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/entity/tenant/TbTenantService.java b/application/src/main/java/org/thingsboard/server/service/entity/tenant/TbTenantService.java new file mode 100644 index 0000000000..56b3e5e7ba --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entity/tenant/TbTenantService.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.entity.tenant; + +import org.thingsboard.server.common.data.Tenant; + +public interface TbTenantService { + + Tenant saveTenant(Tenant tenant); +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index 37982f4a6d..8b2b99b840 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -79,6 +79,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.customer.CustomerService; @@ -208,13 +209,37 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { log.warn(e.getMessage()); } + TenantProfileData isolatedRuleEngineTenantProfileData = new TenantProfileData(); + isolatedRuleEngineTenantProfileData.setConfiguration(new DefaultTenantProfileConfiguration()); + + TenantProfileQueueConfiguration mainQueueConfiguration = new TenantProfileQueueConfiguration(); + mainQueueConfiguration.setName("Main"); + mainQueueConfiguration.setTopic("tb_rule_engine.main"); + mainQueueConfiguration.setPollInterval(25); + mainQueueConfiguration.setPartitions(10); + mainQueueConfiguration.setConsumerPerPartition(true); + mainQueueConfiguration.setPackProcessingTimeout(2000); + SubmitStrategy mainQueueSubmitStrategy = new SubmitStrategy(); + mainQueueSubmitStrategy.setType(SubmitStrategyType.BURST); + mainQueueSubmitStrategy.setBatchSize(1000); + mainQueueConfiguration.setSubmitStrategy(mainQueueSubmitStrategy); + ProcessingStrategy mainQueueProcessingStrategy = new ProcessingStrategy(); + mainQueueProcessingStrategy.setType(ProcessingStrategyType.SKIP_ALL_FAILURES); + mainQueueProcessingStrategy.setRetries(3); + mainQueueProcessingStrategy.setFailurePercentage(0); + mainQueueProcessingStrategy.setPauseBetweenRetries(3); + mainQueueProcessingStrategy.setMaxPauseBetweenRetries(3); + mainQueueConfiguration.setProcessingStrategy(mainQueueProcessingStrategy); + + isolatedRuleEngineTenantProfileData.setQueueConfiguration(Collections.singletonList(mainQueueConfiguration)); + TenantProfile isolatedTbRuleEngineProfile = new TenantProfile(); isolatedTbRuleEngineProfile.setDefault(false); isolatedTbRuleEngineProfile.setName("Isolated TB Rule Engine"); isolatedTbRuleEngineProfile.setDescription("Isolated TB Rule Engine tenant profile"); isolatedTbRuleEngineProfile.setIsolatedTbCore(false); isolatedTbRuleEngineProfile.setIsolatedTbRuleEngine(true); - isolatedTbRuleEngineProfile.setProfileData(tenantProfileData); + isolatedTbRuleEngineProfile.setProfileData(isolatedRuleEngineTenantProfileData); try { tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, isolatedTbRuleEngineProfile); @@ -228,7 +253,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { isolatedTbCoreAndTbRuleEngineProfile.setDescription("Isolated TB Core and TB Rule Engine tenant profile"); isolatedTbCoreAndTbRuleEngineProfile.setIsolatedTbCore(true); isolatedTbCoreAndTbRuleEngineProfile.setIsolatedTbRuleEngine(true); - isolatedTbCoreAndTbRuleEngineProfile.setProfileData(tenantProfileData); + isolatedTbCoreAndTbRuleEngineProfile.setProfileData(isolatedRuleEngineTenantProfileData); try { tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, isolatedTbCoreAndTbRuleEngineProfile); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java index 3f8173b9ac..8e3d07316d 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java @@ -407,7 +407,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< } } - private void updateQueue(TransportProtos.QueueUpdateMsg queueUpdateMsg) { + private synchronized void updateQueue(TransportProtos.QueueUpdateMsg queueUpdateMsg) { String queueName = queueUpdateMsg.getQueueName(); TenantId tenantId = new TenantId(new UUID(queueUpdateMsg.getTenantIdMSB(), queueUpdateMsg.getTenantIdLSB())); QueueId queueId = new QueueId(new UUID(queueUpdateMsg.getQueueIdMSB(), queueUpdateMsg.getQueueIdLSB())); diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueService.java b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueServiceDeprecated.java similarity index 95% rename from common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueService.java rename to common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueServiceDeprecated.java index bc7964df7e..2328893abe 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueService.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueServiceDeprecated.java @@ -19,7 +19,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import java.util.Set; -public interface TbQueueService { +public interface TbQueueServiceDeprecated { Set getQueuesByServiceType(ServiceType serviceType); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/queue/QueueService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/queue/QueueService.java index bdea0ef09c..acc3190da9 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/queue/QueueService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/queue/QueueService.java @@ -29,8 +29,6 @@ public interface QueueService { void deleteQueue(TenantId tenantId, QueueId queueId); - void deleteQueueByQueueName(TenantId tenantId, String queueName); - List findQueuesByTenantId(TenantId tenantId); PageData findQueuesByTenantId(TenantId tenantId, PageLink pageLink); @@ -41,5 +39,7 @@ public interface QueueService { Queue findQueueByTenantIdAndName(TenantId tenantId, String name); + Queue findQueueByTenantIdAndNameInternal(TenantId tenantId, String queueName); + void deleteQueuesByTenantId(TenantId tenantId); } \ No newline at end of file diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java index 3471d383f2..b4bf769944 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java @@ -29,14 +29,14 @@ public interface TenantService { TenantInfo findTenantInfoById(TenantId tenantId); ListenableFuture findTenantByIdAsync(TenantId callerId, TenantId tenantId); - + Tenant saveTenant(Tenant tenant); - + void deleteTenant(TenantId tenantId); - + PageData findTenants(PageLink pageLink); PageData findTenantInfos(PageLink pageLink); - + void deleteTenants(); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/DefaultTbQueueService.java b/common/queue/src/main/java/org/thingsboard/server/queue/DefaultTbQueueServiceDeprecated.java similarity index 94% rename from common/queue/src/main/java/org/thingsboard/server/queue/DefaultTbQueueService.java rename to common/queue/src/main/java/org/thingsboard/server/queue/DefaultTbQueueServiceDeprecated.java index e372fb600c..8f4175dad1 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/DefaultTbQueueService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/DefaultTbQueueServiceDeprecated.java @@ -16,7 +16,6 @@ package org.thingsboard.server.queue; import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.thingsboard.server.common.msg.queue.ServiceQueue; import org.thingsboard.server.common.msg.queue.ServiceType; @@ -31,7 +30,7 @@ import java.util.stream.Collectors; //@Service @RequiredArgsConstructor -public class DefaultTbQueueService implements TbQueueService { +public class DefaultTbQueueServiceDeprecated implements TbQueueServiceDeprecated { private final TbQueueRuleEngineSettings ruleEngineSettings; private Set ruleEngineQueues; diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java index a0ad3c3a4a..c95635a439 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java @@ -16,7 +16,6 @@ package org.thingsboard.server.dao.queue; import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -30,15 +29,12 @@ import org.thingsboard.server.common.data.queue.ProcessingStrategy; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.queue.SubmitStrategy; import org.thingsboard.server.common.data.queue.SubmitStrategyType; -import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; -import org.thingsboard.server.queue.TbQueueAdmin; -import org.thingsboard.server.queue.TbQueueClusterService; import java.util.List; @@ -53,12 +49,6 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ @Autowired private TbTenantProfileCache tenantProfileCache; - @Autowired(required = false) - private TbQueueAdmin tbQueueAdmin; - - @Autowired(required = false) - private TbQueueClusterService queueClusterService; - // @Autowired // private QueueStatsService queueStatsService; @@ -66,101 +56,13 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ public Queue saveQueue(Queue queue) { log.trace("Executing createOrUpdateQueue [{}]", queue); queueValidator.validate(queue, Queue::getTenantId); - Queue savedQueue; - if (queue.getId() == null) { - savedQueue = createQueue(queue); - } else { - savedQueue = updateQueue(queue); - } - - return savedQueue; - } - - private Queue createQueue(Queue queue) { - Queue createdQueue = queueDao.save(queue.getTenantId(), queue); - if (tbQueueAdmin != null) { - for (int i = 0; i < queue.getPartitions(); i++) { - tbQueueAdmin.createTopicIfNotExists(new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName()); - } - } - - if (queueClusterService != null) { - queueClusterService.onQueueChange(createdQueue); - } - - return createdQueue; - } - - private Queue updateQueue(Queue queue) { - Queue oldQueue = queueDao.findById(queue.getTenantId(), queue.getUuidId()); - Queue updatedQueue = queueDao.save(queue.getTenantId(), queue); - - int oldPartitions = oldQueue.getPartitions(); - int currentPartitions = queue.getPartitions(); - - if (currentPartitions != oldPartitions && tbQueueAdmin != null) { - if (currentPartitions > oldPartitions) { - log.info("Added [{}] new partitions to [{}] queue", currentPartitions - oldPartitions, queue.getName()); - for (int i = oldPartitions; i < currentPartitions; i++) { - tbQueueAdmin.createTopicIfNotExists(new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName()); - } - if (queueClusterService != null) { - queueClusterService.onQueueChange(updatedQueue); - } - } else { - log.info("Removed [{}] partitions from [{}] queue", oldPartitions - currentPartitions, queue.getName()); - if (queueClusterService != null) { - queueClusterService.onQueueChange(updatedQueue); - } - await(); - for (int i = currentPartitions; i < oldPartitions; i++) { - tbQueueAdmin.deleteTopic(new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName()); - } - } - } else if (!oldQueue.equals(queue) && queueClusterService != null) { - queueClusterService.onQueueChange(updatedQueue); - } - - return updatedQueue; + return queueDao.save(queue.getTenantId(), queue); } @Override public void deleteQueue(TenantId tenantId, QueueId queueId) { log.trace("Executing deleteQueue, queueId: [{}]", queueId); - Queue queue = findQueueById(tenantId, queueId); - doDelete(tenantId, queue); - } - - @Override - public void deleteQueueByQueueName(TenantId tenantId, String queueName) { - log.trace("Executing deleteQueueByQueueName, name: [{}]", queueName); - Queue queue = findQueueByTenantIdAndName(tenantId, queueName); - doDelete(tenantId, queue); - } - - private void doDelete(TenantId tenantId, Queue queue) { - if (queueClusterService != null) { - queueClusterService.onQueueDelete(queue); - await(); - } -// queueStatsService.deleteQueueStatsByQueueId(tenantId, queueId); - boolean result = queueDao.removeById(tenantId, queue.getUuidId()); - if (result && tbQueueAdmin != null) { - for (int i = 0; i < queue.getPartitions(); i++) { - String fullTopicName = new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName(); - log.debug("Deleting queue [{}]", fullTopicName); - try { - tbQueueAdmin.deleteTopic(fullTopicName); - } catch (Exception e) { - log.error("Failed to delete queue [{}]", fullTopicName); - } - } - } - } - - @SneakyThrows - private void await() { - Thread.sleep(3000); + queueDao.removeById(tenantId, queueId.getId()); } @Override @@ -194,6 +96,12 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ return queueDao.findQueueByTenantIdAndName(getSystemOrIsolatedTenantId(tenantId), queueName); } + @Override + public Queue findQueueByTenantIdAndNameInternal(TenantId tenantId, String queueName) { + log.trace("Executing findQueueByTenantIdAndNameInternal, tenantId: [{}] queueName: [{}]", tenantId, queueName); + return queueDao.findQueueByTenantIdAndName(tenantId, queueName); + } + @Override public void deleteQueuesByTenantId(TenantId tenantId) { Validator.validateId(tenantId, "Incorrect tenant id for delete queues request."); diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 4b935ca700..dcacaba478 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -20,6 +20,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantInfo; @@ -27,8 +28,6 @@ import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.queue.Queue; -import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; @@ -49,12 +48,6 @@ import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetsBundleService; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - import static org.thingsboard.server.dao.service.Validator.validateId; @Service @@ -145,90 +138,14 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe tenant.setTenantProfileId(tenantProfile.getId()); } tenantValidator.validate(tenant, Tenant::getId); - - Tenant oldTenant = tenant.getId() != null ? tenantDao.findById(tenant.getId(), tenant.getUuidId()) : null; - Tenant savedTenant = tenantDao.save(tenant.getId(), tenant); if (tenant.getId() == null) { deviceProfileService.createDefaultDeviceProfile(savedTenant.getId()); apiUsageStateService.createDefaultApiUsageState(savedTenant.getId(), null); } - - updateQueuesForTenant(oldTenant, savedTenant); - return savedTenant; } - private void updateQueuesForTenant(Tenant oldTenant, Tenant newTenant) { - TenantProfile oldTenantProfile = oldTenant != null ? tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, oldTenant.getTenantProfileId()) : null; - TenantProfile newTenantProfile = tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, newTenant.getTenantProfileId()); - - TenantId tenantId = newTenant.getId(); - - boolean oldIsolated = oldTenantProfile != null && oldTenantProfile.isIsolatedTbRuleEngine(); - boolean newIsolated = newTenantProfile.isIsolatedTbRuleEngine(); - - if (!oldIsolated && !newIsolated) { - return; - } - - if (newTenantProfile.equals(oldTenantProfile)) { - return; - } - - Map oldQueues; - Map newQueues; - - if (oldIsolated) { - oldQueues = oldTenantProfile.getProfileData().getQueueConfiguration().stream() - .collect(Collectors.toMap(TenantProfileQueueConfiguration::getName, q -> q)); - } else { - oldQueues = Collections.emptyMap(); - } - - if (newIsolated) { - newQueues = newTenantProfile.getProfileData().getQueueConfiguration().stream() - .collect(Collectors.toMap(TenantProfileQueueConfiguration::getName, q -> q)); - } else { - newQueues = Collections.emptyMap(); - } - - List toRemove = new ArrayList<>(); - List toCreate = new ArrayList<>(); - List toUpdate = new ArrayList<>(); - - for (String oldQueue : oldQueues.keySet()) { - if (!newQueues.containsKey(oldQueue)) { - toRemove.add(oldQueue); - } - } - - for (String newQueue : newQueues.keySet()) { - if (oldQueues.containsKey(newQueue)) { - toUpdate.add(newQueue); - } else { - toCreate.add(newQueue); - } - } - - toRemove.forEach(q -> queueService.deleteQueueByQueueName(tenantId, q)); - - toCreate.forEach(key -> queueService.saveQueue(new Queue(tenantId, newQueues.get(key)))); - - toUpdate.forEach(key -> { - Queue queueToUpdate = new Queue(tenantId, newQueues.get(key)); - Queue foundQueue = queueService.findQueueByTenantIdAndName(tenantId, key); - queueToUpdate.setId(foundQueue.getId()); - queueToUpdate.setCreatedTime(foundQueue.getCreatedTime()); - - if (queueToUpdate.equals(foundQueue)) { - //Queue not changed - } else { - queueService.saveQueue(queueToUpdate); - } - }); - } - @Override public void deleteTenant(TenantId tenantId) { log.trace("Executing deleteTenant [{}]", tenantId); From f4353e2db53ddbb00619436894104286b64245e6 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 2 May 2022 10:04:20 +0200 Subject: [PATCH 079/276] added TbTenantProfileService --- .../server/controller/QueueController.java | 7 +- .../controller/TenantProfileController.java | 11 ++- .../entity/queue/DefaultTbQueueService.java | 77 +++++++++++++++++ .../service/entity/queue/TbQueueService.java | 5 ++ .../entity/tenant/DefaultTbTenantService.java | 86 +------------------ .../DefaultTbTenantProfileService.java | 50 +++++++++++ .../TbTenantProfileService.java | 23 +++++ .../server/dao/tenant/TenantService.java | 5 ++ .../server/dao/sql/tenant/JpaTenantDao.java | 9 ++ .../dao/sql/tenant/TenantRepository.java | 4 + .../server/dao/tenant/TenantDao.java | 3 + .../server/dao/tenant/TenantServiceImpl.java | 10 ++- 12 files changed, 203 insertions(+), 87 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/entity/tenant_profile/DefaultTbTenantProfileService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/entity/tenant_profile/TbTenantProfileService.java diff --git a/application/src/main/java/org/thingsboard/server/controller/QueueController.java b/application/src/main/java/org/thingsboard/server/controller/QueueController.java index dd0e023152..2d07ea8caa 100644 --- a/application/src/main/java/org/thingsboard/server/controller/QueueController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QueueController.java @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entity.queue.TbQueueService; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; @@ -52,6 +53,8 @@ import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHO @RequiredArgsConstructor public class QueueController extends BaseController { + private final TbQueueService tbQueueService; + @ApiOperation(value = "Get queue names (getTenantQueuesByServiceType)", notes = "Returns a set of unique queue names based on service type. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @@ -126,7 +129,7 @@ public class QueueController extends BaseController { switch (type) { case TB_RULE_ENGINE: queue.setTenantId(getTenantId()); - Queue savedQueue = queueService.saveQueue(queue); + Queue savedQueue = tbQueueService.saveQueue(queue); checkNotNull(savedQueue); return savedQueue; default: @@ -145,7 +148,7 @@ public class QueueController extends BaseController { try { QueueId queueId = new QueueId(toUUID(queueIdStr)); checkQueueId(queueId, Operation.DELETE); - queueService.deleteQueue(getTenantId(), queueId); + tbQueueService.deleteQueue(getTenantId(), queueId); } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java index 2512a46c1d..f8e821e489 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java @@ -17,6 +17,7 @@ package org.thingsboard.server.controller; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; @@ -37,6 +38,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entity.tenant_profile.TbTenantProfileService; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; @@ -59,10 +61,13 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI @TbCoreComponent @RequestMapping("/api") @Slf4j +@RequiredArgsConstructor public class TenantProfileController extends BaseController { private static final String TENANT_PROFILE_INFO_DESCRIPTION = "Tenant Profile Info is a lightweight object that contains only id and name of the profile. "; + private final TbTenantProfileService tbTenantProfileService; + @ApiOperation(value = "Get Tenant Profile (getTenantProfileById)", notes = "Fetch the Tenant Profile object based on the provided Tenant Profile Id. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @@ -171,14 +176,16 @@ public class TenantProfileController extends BaseController { @RequestBody TenantProfile tenantProfile) throws ThingsboardException { try { boolean newTenantProfile = tenantProfile.getId() == null; + TenantProfile oldProfile; if (newTenantProfile) { accessControlService .checkPermission(getCurrentUser(), Resource.TENANT_PROFILE, Operation.CREATE); + oldProfile = null; } else { - checkEntityId(tenantProfile.getId(), Operation.WRITE); + oldProfile = checkTenantProfileId(tenantProfile.getId(), Operation.WRITE); } - tenantProfile = checkNotNull(tenantProfileService.saveTenantProfile(getTenantId(), tenantProfile)); + tenantProfile = checkNotNull(tbTenantProfileService.saveTenantProfile(getTenantId(), tenantProfile, oldProfile)); tenantProfileCache.put(tenantProfile); tbClusterService.onTenantProfileChange(tenantProfile, null); tbClusterService.broadcastEntityStateChangeEvent(TenantId.SYS_TENANT_ID, tenantProfile.getId(), diff --git a/application/src/main/java/org/thingsboard/server/service/entity/queue/DefaultTbQueueService.java b/application/src/main/java/org/thingsboard/server/service/entity/queue/DefaultTbQueueService.java index 006c9025ba..37556d8b95 100644 --- a/application/src/main/java/org/thingsboard/server/service/entity/queue/DefaultTbQueueService.java +++ b/application/src/main/java/org/thingsboard/server/service/entity/queue/DefaultTbQueueService.java @@ -19,15 +19,23 @@ import lombok.AllArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueClusterService; import org.thingsboard.server.queue.util.TbCoreComponent; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + @Slf4j @Service @TbCoreComponent @@ -141,4 +149,73 @@ public class DefaultTbQueueService implements TbQueueService { Thread.sleep(3000); } + @Override + public void updateQueuesByTenants(List tenantIds, TenantProfile newTenantProfile, TenantProfile oldTenantProfile) { + + boolean oldIsolated = oldTenantProfile != null && oldTenantProfile.isIsolatedTbRuleEngine(); + boolean newIsolated = newTenantProfile.isIsolatedTbRuleEngine(); + + if (!oldIsolated && !newIsolated) { + return; + } + + if (newTenantProfile.equals(oldTenantProfile)) { + return; + } + + Map oldQueues; + Map newQueues; + + if (oldIsolated) { + oldQueues = oldTenantProfile.getProfileData().getQueueConfiguration().stream() + .collect(Collectors.toMap(TenantProfileQueueConfiguration::getName, q -> q)); + } else { + oldQueues = Collections.emptyMap(); + } + + if (newIsolated) { + newQueues = newTenantProfile.getProfileData().getQueueConfiguration().stream() + .collect(Collectors.toMap(TenantProfileQueueConfiguration::getName, q -> q)); + } else { + newQueues = Collections.emptyMap(); + } + + List toRemove = new ArrayList<>(); + List toCreate = new ArrayList<>(); + List toUpdate = new ArrayList<>(); + + for (String oldQueue : oldQueues.keySet()) { + if (!newQueues.containsKey(oldQueue)) { + toRemove.add(oldQueue); + } + } + + for (String newQueue : newQueues.keySet()) { + if (oldQueues.containsKey(newQueue)) { + toUpdate.add(newQueue); + } else { + toCreate.add(newQueue); + } + } + + tenantIds.forEach(tenantId -> { + toRemove.forEach(q -> deleteQueueByQueueName(tenantId, q)); + + toCreate.forEach(key -> saveQueue(new Queue(tenantId, newQueues.get(key)))); + + toUpdate.forEach(key -> { + Queue queueToUpdate = new Queue(tenantId, newQueues.get(key)); + Queue foundQueue = queueService.findQueueByTenantIdAndName(tenantId, key); + queueToUpdate.setId(foundQueue.getId()); + queueToUpdate.setCreatedTime(foundQueue.getCreatedTime()); + + if (queueToUpdate.equals(foundQueue)) { + //Queue not changed + } else { + saveQueue(queueToUpdate); + } + }); + }); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/entity/queue/TbQueueService.java b/application/src/main/java/org/thingsboard/server/service/entity/queue/TbQueueService.java index 1b37486ba4..c431d242cf 100644 --- a/application/src/main/java/org/thingsboard/server/service/entity/queue/TbQueueService.java +++ b/application/src/main/java/org/thingsboard/server/service/entity/queue/TbQueueService.java @@ -15,10 +15,13 @@ */ package org.thingsboard.server.service.entity.queue; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.queue.Queue; +import java.util.List; + public interface TbQueueService { Queue saveQueue(Queue queue); @@ -26,4 +29,6 @@ public interface TbQueueService { void deleteQueue(TenantId tenantId, QueueId queueId); void deleteQueueByQueueName(TenantId tenantId, String queueName); + + void updateQueuesByTenants(List tenantIds, TenantProfile newTenantProfile, TenantProfile oldTenantProfile); } diff --git a/application/src/main/java/org/thingsboard/server/service/entity/tenant/DefaultTbTenantService.java b/application/src/main/java/org/thingsboard/server/service/entity/tenant/DefaultTbTenantService.java index 015c88813b..0d6f838372 100644 --- a/application/src/main/java/org/thingsboard/server/service/entity/tenant/DefaultTbTenantService.java +++ b/application/src/main/java/org/thingsboard/server/service/entity/tenant/DefaultTbTenantService.java @@ -21,20 +21,13 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.queue.Queue; -import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; -import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entity.queue.TbQueueService; -import java.util.ArrayList; import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; @Slf4j @Service @@ -44,7 +37,6 @@ public class DefaultTbTenantService implements TbTenantService { private final TenantService tenantService; private final TbQueueService tbQueueService; - private final QueueService queueService; private final TenantProfileService tenantProfileService; private final TbTenantProfileCache tenantProfileCache; @@ -52,84 +44,14 @@ public class DefaultTbTenantService implements TbTenantService { public Tenant saveTenant(Tenant tenant) { boolean updated = tenant.getId() != null; Tenant oldTenant = updated ? tenantService.findTenantById(tenant.getId()) : null; - List queues; - if (updated) { - - } Tenant savedTenant = tenantService.saveTenant(tenant); tenantProfileCache.evict(tenant.getId()); - updateQueuesForTenant(oldTenant, savedTenant); - return savedTenant; - } - public void updateQueuesForTenant(Tenant oldTenant, Tenant newTenant) { TenantProfile oldTenantProfile = oldTenant != null ? tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, oldTenant.getTenantProfileId()) : null; - TenantProfile newTenantProfile = tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, newTenant.getTenantProfileId()); - - TenantId tenantId = newTenant.getId(); - - boolean oldIsolated = oldTenantProfile != null && oldTenantProfile.isIsolatedTbRuleEngine(); - boolean newIsolated = newTenantProfile.isIsolatedTbRuleEngine(); - - if (!oldIsolated && !newIsolated) { - return; - } - - if (newTenantProfile.equals(oldTenantProfile)) { - return; - } - - Map oldQueues; - Map newQueues; - - if (oldIsolated) { - oldQueues = oldTenantProfile.getProfileData().getQueueConfiguration().stream() - .collect(Collectors.toMap(TenantProfileQueueConfiguration::getName, q -> q)); - } else { - oldQueues = Collections.emptyMap(); - } - - if (newIsolated) { - newQueues = newTenantProfile.getProfileData().getQueueConfiguration().stream() - .collect(Collectors.toMap(TenantProfileQueueConfiguration::getName, q -> q)); - } else { - newQueues = Collections.emptyMap(); - } - - List toRemove = new ArrayList<>(); - List toCreate = new ArrayList<>(); - List toUpdate = new ArrayList<>(); - - for (String oldQueue : oldQueues.keySet()) { - if (!newQueues.containsKey(oldQueue)) { - toRemove.add(oldQueue); - } - } - - for (String newQueue : newQueues.keySet()) { - if (oldQueues.containsKey(newQueue)) { - toUpdate.add(newQueue); - } else { - toCreate.add(newQueue); - } - } - - toRemove.forEach(q -> tbQueueService.deleteQueueByQueueName(tenantId, q)); - - toCreate.forEach(key -> tbQueueService.saveQueue(new Queue(tenantId, newQueues.get(key)))); - - toUpdate.forEach(key -> { - Queue queueToUpdate = new Queue(tenantId, newQueues.get(key)); - Queue foundQueue = queueService.findQueueByTenantIdAndName(tenantId, key); - queueToUpdate.setId(foundQueue.getId()); - queueToUpdate.setCreatedTime(foundQueue.getCreatedTime()); - - if (queueToUpdate.equals(foundQueue)) { - //Queue not changed - } else { - tbQueueService.saveQueue(queueToUpdate); - } - }); + TenantProfile newTenantProfile = tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, savedTenant.getTenantProfileId()); + tbQueueService.updateQueuesByTenants(Collections.singletonList(savedTenant.getTenantId()), newTenantProfile, oldTenantProfile); + return savedTenant; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/entity/tenant_profile/DefaultTbTenantProfileService.java b/application/src/main/java/org/thingsboard/server/service/entity/tenant_profile/DefaultTbTenantProfileService.java new file mode 100644 index 0000000000..2a01ddfed6 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entity/tenant_profile/DefaultTbTenantProfileService.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.entity.tenant_profile; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.tenant.TenantProfileService; +import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entity.queue.TbQueueService; + +import java.util.List; + +@Slf4j +@Service +@TbCoreComponent +@AllArgsConstructor +public class DefaultTbTenantProfileService implements TbTenantProfileService { + private final TbQueueService queueService; + private final TenantProfileService tenantProfileService; + private final TenantService tenantService; + + @Override + public TenantProfile saveTenantProfile(TenantId tenantId, TenantProfile tenantProfile, TenantProfile oldTenantProfile) { + TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(tenantId, tenantProfile); + + if (oldTenantProfile != null && savedTenantProfile.isIsolatedTbRuleEngine()) { + List tenantIds = tenantService.findTenantIdsByTenantProfileId(savedTenantProfile.getId()); + queueService.updateQueuesByTenants(tenantIds, savedTenantProfile, oldTenantProfile); + } + + return savedTenantProfile; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/entity/tenant_profile/TbTenantProfileService.java b/application/src/main/java/org/thingsboard/server/service/entity/tenant_profile/TbTenantProfileService.java new file mode 100644 index 0000000000..0764319d03 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entity/tenant_profile/TbTenantProfileService.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.entity.tenant_profile; + +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.TenantId; + +public interface TbTenantProfileService { + TenantProfile saveTenantProfile(TenantId tenantId, TenantProfile tenantProfile, TenantProfile oldTenantProfile); +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java index b4bf769944..fbb9dfa0e1 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java @@ -19,9 +19,12 @@ import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import java.util.List; + public interface TenantService { Tenant findTenantById(TenantId tenantId); @@ -38,5 +41,7 @@ public interface TenantService { PageData findTenantInfos(PageLink pageLink); + List findTenantIdsByTenantProfileId(TenantProfileId tenantProfileId); + void deleteTenants(); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java index e7a1961fab..ea4060aa44 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java @@ -21,6 +21,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; @@ -29,8 +30,10 @@ import org.thingsboard.server.dao.model.sql.TenantInfoEntity; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; import org.thingsboard.server.dao.tenant.TenantDao; +import java.util.List; import java.util.Objects; import java.util.UUID; +import java.util.stream.Collectors; /** @@ -80,4 +83,10 @@ public class JpaTenantDao extends JpaAbstractSearchTextDao return DaoUtil.pageToPageData(tenantRepository.findTenantsIds(DaoUtil.toPageable(pageLink))).mapData(TenantId::fromUUID); } + @Override + public List findTenantIdsByTenantProfileId(TenantProfileId tenantProfileId) { + return tenantRepository.findTenantIdsByTenantProfileId(tenantProfileId.getId()).stream() + .map(TenantId::fromUUID) + .collect(Collectors.toList()); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java index 4ae1278bec..8aa86a535a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java @@ -23,6 +23,7 @@ import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.model.sql.TenantEntity; import org.thingsboard.server.dao.model.sql.TenantInfoEntity; +import java.util.List; import java.util.UUID; /** @@ -54,4 +55,7 @@ public interface TenantRepository extends PagingAndSortingRepository findTenantsIds(Pageable pageable); + @Query("SELECT t.id FROM TenantEntity t where t.tenantProfileId = :tenantProfileId") + List findTenantIdsByTenantProfileId(@Param("tenantProfileId") UUID tenantProfileId); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDao.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDao.java index 94ca9c8c1e..e08ed4745f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDao.java @@ -18,10 +18,12 @@ package org.thingsboard.server.dao.tenant; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; +import java.util.List; import java.util.UUID; public interface TenantDao extends Dao { @@ -49,4 +51,5 @@ public interface TenantDao extends Dao { PageData findTenantsIds(PageLink pageLink); + List findTenantIdsByTenantProfileId(TenantProfileId tenantProfileId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index dcacaba478..187c87f1d3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -20,12 +20,12 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.asset.AssetService; @@ -48,6 +48,8 @@ import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetsBundleService; +import java.util.List; + import static org.thingsboard.server.dao.service.Validator.validateId; @Service @@ -183,6 +185,12 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe return tenantDao.findTenantInfosByRegion(TenantId.SYS_TENANT_ID, DEFAULT_TENANT_REGION, pageLink); } + @Override + public List findTenantIdsByTenantProfileId(TenantProfileId tenantProfileId) { + log.trace("Executing findTenantsByTenantProfileId [{}]", tenantProfileId); + return tenantDao.findTenantIdsByTenantProfileId(tenantProfileId); + } + @Override public void deleteTenants() { log.trace("Executing deleteTenants"); From 812e3490a6a2c29fdc8f034df2baea376447ceb0 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Mon, 2 May 2022 12:27:49 +0300 Subject: [PATCH 080/276] API to get the list of available 2FA providers for user --- .../TwoFactorAuthConfigController.java | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java index d4cc690604..f35f7a231a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java @@ -33,6 +33,7 @@ import org.springframework.web.bind.annotation.RestController; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderConfig; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager; @@ -46,6 +47,10 @@ import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; @RestController @@ -63,15 +68,15 @@ public class TwoFactorAuthConfigController extends BaseController { "or if a provider for previously set up account config is not now configured." + NEW_LINE + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER + NEW_LINE + "Response example for TOTP 2FA: " + NEW_LINE + - "{\n" + + "```\n{\n" + " \"providerType\": \"TOTP\",\n" + " \"authUrl\": \"otpauth://totp/ThingsBoard:tenant@thingsboard.org?issuer=ThingsBoard&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII\"\n" + - "}" + NEW_LINE + + "}\n```" + NEW_LINE + "Response example for SMS 2FA: " + NEW_LINE + - "{\n" + + "```\n{\n" + " \"providerType\": \"SMS\",\n" + " \"phoneNumber\": \"+380505005050\"\n" + - "}") + "}\n```") @GetMapping("/account/config") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") public TwoFactorAuthAccountConfig getTwoFaAccountConfig() throws ThingsboardException { @@ -79,6 +84,17 @@ public class TwoFactorAuthConfigController extends BaseController { return twoFactorAuthConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId()).orElse(null); } + + @GetMapping("/providers") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + public List getAvailableTwoFaProviders() throws ThingsboardException { + return twoFactorAuthConfigManager.getTwoFaSettings(getTenantId(), true) + .map(TwoFactorAuthSettings::getProviders).orElse(Collections.emptyList()).stream() + .map(TwoFactorAuthProviderConfig::getProviderType) + .collect(Collectors.toList()); + } + + @ApiOperation(value = "Generate 2FA account config (generateTwoFaAccountConfig)", notes = "Generate new 2FA account config for specified provider type. " + "This method is only useful for TOTP 2FA, as there is nothing to generate for other provider types. " + @@ -89,15 +105,15 @@ public class TwoFactorAuthConfigController extends BaseController { "Will throw an error (Bad Request) if the provider is not configured for usage. " + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER + NEW_LINE + "Example of a generated account config for TOTP 2FA: " + NEW_LINE + - "{\n" + + "```\n{\n" + " \"providerType\": \"TOTP\",\n" + " \"authUrl\": \"otpauth://totp/ThingsBoard:tenant@thingsboard.org?issuer=ThingsBoard&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII\"\n" + - "}" + NEW_LINE + + "}\n```" + NEW_LINE + "For SMS provider type it will return something like: " + NEW_LINE + - "{\n" + + "```\n{\n" + " \"providerType\": \"SMS\",\n" + " \"phoneNumber\": null\n" + - "}") + "}\n```") @PostMapping("/account/config/generate") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") public TwoFactorAuthAccountConfig generateTwoFaAccountConfig(@ApiParam(value = "2FA provider type to generate new account config for", defaultValue = "TOTP", required = true) From 38b4d6b6593e23bee91190fe579f543849b2510a Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 2 May 2022 19:20:01 +0300 Subject: [PATCH 081/276] UI: Implement update multiple attributes widget settings forms --- .../system/widget_bundles/input_widgets.json | 6 +- .../datakey-select-option.component.html | 53 ++++ .../datakey-select-option.component.scss | 40 +++ .../input/datakey-select-option.component.ts | 128 +++++++++ ...ple-attributes-key-settings.component.html | 246 +++++++++++++++++ ...tiple-attributes-key-settings.component.ts | 250 ++++++++++++++++++ ...-attributes-widget-settings.component.html | 94 +++++++ ...le-attributes-widget-settings.component.ts | 117 ++++++++ .../lib/settings/widget-settings.module.ts | 23 +- .../material-icon-select.component.html | 2 +- .../material-icon-select.component.ts | 5 + .../assets/locale/locale.constant-en_US.json | 60 ++++- 12 files changed, 1017 insertions(+), 7 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/datakey-select-option.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/datakey-select-option.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/datakey-select-option.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/input_widgets.json b/application/src/main/data/json/system/widget_bundles/input_widgets.json index a398c2fc43..c5c3773e56 100644 --- a/application/src/main/data/json/system/widget_bundles/input_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/input_widgets.json @@ -37,8 +37,10 @@ "templateHtml": "\n", "templateCss": ".tb-toast {\n min-width: 0;\n font-size: 14px !important;\n}", "controllerScript": "self.onInit = function() {\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n self.ctx.$scope.multipleInputWidget.onDataUpdated();\r\n}\r\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"MultipleInput\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showActionButtons\":{\n \"title\":\"Show action buttons\",\n \"type\":\"boolean\",\n \"default\": true\n },\n \"updateAllValues\": {\n \"title\":\"Update all values, not only modified\",\n \"type\":\"boolean\",\n \"default\": false\n },\n \"saveButtonLabel\": {\n \"title\": \"'SAVE' button label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"resetButtonLabel\": {\n \"title\": \"'UNDO' button label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\": true\n },\n \"showGroupTitle\": {\n \"title\":\"Show title for group of fields, related to different entities\",\n \"type\":\"boolean\",\n \"default\": false\n },\n \"groupTitle\": {\n \"title\": \"Group title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"fieldsAlignment\": {\n \"title\": \"Fields alignment\",\n \"type\": \"string\",\n \"default\": \"row\"\n },\n \"fieldsInRow\": {\n \"title\": \"Number of fields in the row\",\n \"type\": \"number\",\n \"default\": \"2\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showActionButtons\",\n {\n \"key\": \"updateAllValues\",\n \"condition\": \"model.showActionButtons === true\"\n },\n {\n \"key\": \"saveButtonLabel\",\n \"condition\": \"model.showActionButtons === true\"\n },\n {\n \"key\": \"resetButtonLabel\",\n \"condition\": \"model.showActionButtons === true\"\n },\n \"showResultMessage\",\n \"showGroupTitle\",\n \"groupTitle\",\n {\n \"key\": \"fieldsAlignment\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"row\",\n \"label\": \"Row (default)\"\n },\n {\n \"value\": \"column\",\n \"label\": \"Column\"\n }\n ]\n },\n {\n \"key\": \"fieldsInRow\",\n \"condition\": \"model.fieldsAlignment === 'row'\"\n }\n ]\n}", - "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"dataKeyType\": {\n \"title\": \"Datakey type\",\n \"type\": \"string\",\n \"default\": \"server\"\n },\n \"dataKeyValueType\": {\n \"title\": \"Datakey value type\",\n \"type\": \"string\",\n \"default\": \"string\"\n },\n \"slideToggleLabelPosition\": {\n \"title\": \"Label appears after or before the slide-toggle\",\n \"type\": \"string\",\n \"default\": \"after\"\n },\n \"selectOptions\": {\n \"title\": \"Select options\",\n \"type\": \"array\",\n \"default\": [],\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"value\": {\n \"title\": \"Value (write 'null' for create empty option)\",\n \"type\": \"string\"\n },\n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n }\n },\n \"required\": [\"value\"]\n }\n },\n \"step\": {\n \"title\": \"Step interval between values\",\n \"type\": \"number\",\n \"default\": \"1\"\n },\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\"\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\"\n },\n \"required\": {\n \"title\": \"Value is required\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"minValueErrorMessage\": {\n \"title\": \"'Min Value' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValueErrorMessage\": {\n \"title\": \"'Max Value' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"invalidDateErrorMessage\": {\n \"title\": \"'Invalid Date' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"isEditable\": {\n \"title\": \"Ability to edit attribute\",\n \"type\": \"string\",\n \"default\": \"editable\"\n },\n \"disabledOnDataKey\": {\n \"title\": \"Disable on false value of another datakey (specify datakey name)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"dataKeyHidden\": {\n \"title\": \"Hide input field\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"useCustomIcon\": {\n \"title\": \"Use custom icon\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"icon\": {\n \"title\": \"Icon to show before input cell\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"customIcon\": {\n \"title\": \"Icon to show before input cell\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useGetValueFunction\": {\n \"title\": \"use getValue function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"getValueFunctionBody\": {\n \"title\": \"getValue function: f(value, ctx)\",\n \"type\": \"string\"\n },\n \"useSetValueFunction\": {\n \"title\": \"use setValue function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"setValueFunctionBody\": {\n \"title\": \"setValue function: f(value, originValue, ctx)\",\n \"type\": \"string\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"dataKeyType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"server\",\n \"label\": \"Server attribute (default)\"\n },\n {\n \"value\": \"shared\",\n \"label\": \"Shared attribute\"\n },\n {\n \"value\": \"timeseries\",\n \"label\": \"Timeseries\"\n }\n ]\n },\n {\n \"key\": \"dataKeyValueType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"string\",\n \"label\": \"String\"\n },\n {\n \"value\": \"double\",\n \"label\": \"Double\"\n },\n {\n \"value\": \"integer\",\n \"label\": \"Integer\"\n },\n {\n \"value\": \"booleanCheckbox\",\n \"label\": \"Boolean (Checkbox)\"\n },\n {\n \"value\": \"booleanSwitch\",\n \"label\": \"Boolean (Switch)\"\n },\n {\n \"value\": \"dateTime\",\n \"label\": \"Date & Time\"\n },\n {\n \"value\": \"date\",\n \"label\": \"Date\"\n },\n {\n \"value\": \"time\",\n \"label\": \"Time\"\n },\n {\n \"value\": \"select\",\n \"label\": \"Select\"\n }\n ]\n },\n {\n \"key\": \"slideToggleLabelPosition\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"condition\": \"model.dataKeyValueType === 'booleanSwitch'\",\n \"items\": [\n {\n \"value\": \"after\",\n \"label\": \"After\"\n },\n {\n \"value\": \"before\",\n \"label\": \"Before\"\n }\n ]\n },\n {\n \"key\": \"selectOptions\",\n \"condition\": \"model.dataKeyValueType === 'select'\",\n \"items\": [\"selectOptions[].value\", \"selectOptions[].label\"]\n },\n {\n \"key\": \"step\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n {\n \"key\": \"minValue\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n {\n \"key\": \"maxValue\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n \"required\",\n {\n \"key\": \"requiredErrorMessage\",\n \"condition\": \"model.required === true\"\n },\n {\n \"key\": \"invalidDateErrorMessage\",\n \"condition\": \"model.dataKeyValueType === 'dateTime' || model.dataKeyValueType === 'date' || model.dataKeyValueType === 'time'\"\n },\n {\n \"key\": \"minValueErrorMessage\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n {\n \"key\": \"maxValueErrorMessage\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n {\n \"key\": \"isEditable\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"editable\",\n \"label\": \"Editable (default)\"\n },\n {\n \"value\": \"disabled\",\n \"label\": \"Disabled\"\n },\n {\n \"value\": \"readonly\",\n \"label\": \"Read-only\"\n }\n ]\n },\n \"disabledOnDataKey\",\n \"dataKeyHidden\",\n \"useCustomIcon\",\n\t\t{\n \t\t\"key\": \"icon\",\n\t\t\t\"type\": \"icon\",\n\t\t\t\"condition\": \"model.useCustomIcon !== true\"\n\t\t},\n\t\t{\n \t\t\"key\": \"customIcon\",\n\t\t\t\"type\": \"image\",\n\t\t\t\"condition\": \"model.useCustomIcon === true\"\n\t\t},\n\t\t\"useGetValueFunction\",\n\t\t{\n\t\t \"key\": \"getValueFunctionBody\",\n\t\t \"type\": \"javascript\",\n\t\t \"condition\": \"model.useGetValueFunction === true\"\n\t\t},\n\t\t\"useSetValueFunction\",\n\t\t{\n\t\t \"key\": \"setValueFunctionBody\",\n\t\t \"type\": \"javascript\",\n\t\t \"condition\": \"model.useSetValueFunction === true\"\n\t\t}\n ]\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-update-multiple-attributes-widget-settings", + "dataKeySettingsDirective": "tb-update-multiple-attributes-key-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update Multiple Attributes\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/datakey-select-option.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/datakey-select-option.component.html new file mode 100644 index 0000000000..68a1a6c5dc --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/datakey-select-option.component.html @@ -0,0 +1,53 @@ + + + +
+ +
+
+
+
+ + +
+
+ +
+ +
+
+ + widgets.input-widgets.option-value + + + + widgets.input-widgets.option-label + + +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/datakey-select-option.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/datakey-select-option.component.scss new file mode 100644 index 0000000000..36c8d7c154 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/datakey-select-option.component.scss @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2022 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. + */ +:host { + display: block; + .mat-expansion-panel { + box-shadow: none; + &.datakey-select-option { + border: 1px groove rgba(0, 0, 0, .25); + .mat-expansion-panel-header { + padding: 0 24px 0 8px; + &.mat-expanded { + height: 48px; + } + } + } + } +} + +:host ::ng-deep { + .mat-expansion-panel { + &.datakey-select-option { + .mat-expansion-panel-body { + padding: 0 8px 8px; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/datakey-select-option.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/datakey-select-option.component.ts new file mode 100644 index 0000000000..6a1033a4e2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/datakey-select-option.component.ts @@ -0,0 +1,128 @@ +/// +/// Copyright © 2016-2022 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 { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormBuilder, + FormGroup, + NG_VALUE_ACCESSOR, + Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; + +export interface DataKeySelectOption { + value: string; + label?: string; +} + +export function dataKeySelectOptionValidator(control: AbstractControl) { + const selectOption: DataKeySelectOption = control.value; + if (!selectOption || !selectOption.value) { + return { + dataKeySelectOption: true + }; + } + return null; +} + +@Component({ + selector: 'tb-datakey-select-option', + templateUrl: './datakey-select-option.component.html', + styleUrls: ['./datakey-select-option.component.scss', './../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DataKeySelectOptionComponent), + multi: true + } + ] +}) +export class DataKeySelectOptionComponent extends PageComponent implements OnInit, ControlValueAccessor { + + @Input() + disabled: boolean; + + @Input() + expanded = false; + + @Output() + removeSelectOption = new EventEmitter(); + + private modelValue: DataKeySelectOption; + + private propagateChange = null; + + public selectOptionFormGroup: FormGroup; + + constructor(protected store: Store, + private translate: TranslateService, + private domSanitizer: DomSanitizer, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.selectOptionFormGroup = this.fb.group({ + value: [null, [Validators.required]], + label: [null, []] + }); + this.selectOptionFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.selectOptionFormGroup.disable({emitEvent: false}); + } else { + this.selectOptionFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DataKeySelectOption): void { + this.modelValue = value; + this.selectOptionFormGroup.patchValue( + value, {emitEvent: false} + ); + } + + selectOptionHtml(): SafeHtml { + const selectOption: DataKeySelectOption = this.selectOptionFormGroup.value; + const value = selectOption?.value || 'Undefined'; + const label = selectOption?.label || ''; + return this.domSanitizer.bypassSecurityTrustHtml(`${value} ${label ? '(' + label + ')' : ''}`); + } + + private updateModel() { + const value: DataKeySelectOption = this.selectOptionFormGroup.value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html new file mode 100644 index 0000000000..2f47998fba --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html @@ -0,0 +1,246 @@ + +
+ + {{ 'widgets.input-widgets.hide-input-field' | translate }} + +
+ widgets.input-widgets.general-settings +
+ + widgets.input-widgets.datakey-type + + + {{ 'widgets.input-widgets.datakey-type-server' | translate }} + + + {{ 'widgets.input-widgets.datakey-type-shared' | translate }} + + + {{ 'widgets.input-widgets.datakey-type-timeseries' | translate }} + + + + + widgets.input-widgets.datakey-value-type + + + {{ 'widgets.input-widgets.datakey-value-type-string' | translate }} + + + {{ 'widgets.input-widgets.datakey-value-type-double' | translate }} + + + {{ 'widgets.input-widgets.datakey-value-type-integer' | translate }} + + + {{ 'widgets.input-widgets.datakey-value-type-boolean-checkbox' | translate }} + + + {{ 'widgets.input-widgets.datakey-value-type-boolean-switch' | translate }} + + + {{ 'widgets.input-widgets.datakey-value-type-date-time' | translate }} + + + {{ 'widgets.input-widgets.datakey-value-type-date' | translate }} + + + {{ 'widgets.input-widgets.datakey-value-type-time' | translate }} + + + {{ 'widgets.input-widgets.datakey-value-type-select' | translate }} + + + +
+ + {{ 'widgets.input-widgets.value-is-required' | translate }} + +
+ + widgets.input-widgets.ability-to-edit-attribute + + + {{ 'widgets.input-widgets.ability-to-edit-attribute-editable' | translate }} + + + {{ 'widgets.input-widgets.ability-to-edit-attribute-disabled' | translate }} + + + {{ 'widgets.input-widgets.ability-to-edit-attribute-readonly' | translate }} + + + + + widgets.input-widgets.disable-on-datakey-name + + +
+
+
+ widgets.input-widgets.slide-toggle-settings + + widgets.input-widgets.slide-toggle-label-position + + + {{ 'widgets.input-widgets.slide-toggle-label-position-after' | translate }} + + + {{ 'widgets.input-widgets.slide-toggle-label-position-before' | translate }} + + + +
+
+ widgets.input-widgets.select-options +
+
+
+ + +
+
+
+ widgets.input-widgets.no-select-options +
+
+ +
+
+
+
+ widgets.input-widgets.numeric-field-settings +
+ + widgets.input-widgets.step-interval + + + + widgets.input-widgets.min-value + + + + widgets.input-widgets.max-value + + +
+
+
+ widgets.input-widgets.error-messages + + widgets.input-widgets.required-error-message + + +
+ + widgets.input-widgets.min-value-error-message + + + + widgets.input-widgets.max-value-error-message + + +
+ + widgets.input-widgets.invalid-date-error-message + + +
+
+ widgets.input-widgets.icon-settings + + {{ 'widgets.input-widgets.use-custom-icon' | translate }} + + + + + +
+
+ widgets.input-widgets.value-conversion-settings +
+ widgets.input-widgets.get-value-settings + + + + + {{ 'widgets.input-widgets.use-get-value-function' | translate }} + + + + widget-config.advanced-settings + + + + + + + +
+
+ widgets.input-widgets.set-value-settings + + + + + {{ 'widgets.input-widgets.use-set-value-function' | translate }} + + + + widget-config.advanced-settings + + + + + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts new file mode 100644 index 0000000000..4884166be5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts @@ -0,0 +1,250 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { AbstractControl, FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + DataKeySelectOption, + dataKeySelectOptionValidator +} from '@home/components/widget/lib/settings/input/datakey-select-option.component'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; + +@Component({ + selector: 'tb-update-multiple-attributes-key-settings', + templateUrl: './update-multiple-attributes-key-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class UpdateMultipleAttributesKeySettingsComponent extends WidgetSettingsComponent { + + updateMultipleAttributesKeySettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.updateMultipleAttributesKeySettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + dataKeyHidden: false, + dataKeyType: 'server', + dataKeyValueType: 'string', + required: false, + isEditable: 'editable', + disabledOnDataKey: '', + + slideToggleLabelPosition: 'after', + selectOptions: [], + step: 1, + minValue: null, + maxValue: null, + + requiredErrorMessage: '', + minValueErrorMessage: '', + maxValueErrorMessage: '', + invalidDateErrorMessage: '', + + useCustomIcon: false, + icon: '', + customIcon: '', + + useGetValueFunction: false, + getValueFunctionBody: '', + useSetValueFunction: false, + setValueFunctionBody: '' + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.updateMultipleAttributesKeySettingsForm = this.fb.group({ + + dataKeyHidden: [settings.dataKeyHidden, []], + + // General settings + + dataKeyType: [settings.dataKeyType, []], + dataKeyValueType: [settings.dataKeyValueType, []], + required: [settings.required, []], + isEditable: [settings.isEditable, []], + disabledOnDataKey: [settings.disabledOnDataKey, []], + + // Slide toggle settings + + slideToggleLabelPosition: [settings.slideToggleLabelPosition, []], + + // Select options + + selectOptions: this.prepareSelectOptionsFormArray(settings.selectOptions), + + // Numeric field settings + + step: [settings.step, [Validators.min(0)]], + minValue: [settings.minValue, []], + maxValue: [settings.maxValue, []], + + // Error messages + + requiredErrorMessage: [settings.requiredErrorMessage, []], + minValueErrorMessage: [settings.minValueErrorMessage, []], + maxValueErrorMessage: [settings.maxValueErrorMessage, []], + invalidDateErrorMessage: [settings.invalidDateErrorMessage, []], + + // Icon settings + + useCustomIcon: [settings.useCustomIcon, []], + icon: [settings.icon, []], + customIcon: [settings.customIcon, []], + + // Value conversion settings + + useGetValueFunction: [settings.useGetValueFunction, []], + getValueFunctionBody: [settings.getValueFunctionBody, []], + useSetValueFunction: [settings.useSetValueFunction, []], + setValueFunctionBody: [settings.setValueFunctionBody, []] + + }); + } + + protected validatorTriggers(): string[] { + return ['dataKeyHidden', 'dataKeyValueType', 'required', 'isEditable', 'useCustomIcon', 'useGetValueFunction', 'useSetValueFunction']; + } + + protected updateValidators(emitEvent: boolean) { + const dataKeyHidden: boolean = this.updateMultipleAttributesKeySettingsForm.get('dataKeyHidden').value; + const dataKeyValueType: string = this.updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value; + const required: boolean = this.updateMultipleAttributesKeySettingsForm.get('required').value; + const isEditable: string = this.updateMultipleAttributesKeySettingsForm.get('isEditable').value; + const useCustomIcon: boolean = this.updateMultipleAttributesKeySettingsForm.get('useCustomIcon').value; + const useGetValueFunction: boolean = this.updateMultipleAttributesKeySettingsForm.get('useGetValueFunction').value; + const useSetValueFunction: boolean = this.updateMultipleAttributesKeySettingsForm.get('useSetValueFunction').value; + + this.updateMultipleAttributesKeySettingsForm.disable({emitEvent: false}); + this.updateMultipleAttributesKeySettingsForm.get('dataKeyHidden').enable({emitEvent: false}); + + if (!dataKeyHidden) { + this.updateMultipleAttributesKeySettingsForm.get('dataKeyType').enable({emitEvent: false}); + this.updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').enable({emitEvent: false}); + this.updateMultipleAttributesKeySettingsForm.get('required').enable({emitEvent: false}); + this.updateMultipleAttributesKeySettingsForm.get('isEditable').enable({emitEvent: false}); + this.updateMultipleAttributesKeySettingsForm.get('useCustomIcon').enable({emitEvent: false}); + this.updateMultipleAttributesKeySettingsForm.get('useGetValueFunction').enable({emitEvent: false}); + this.updateMultipleAttributesKeySettingsForm.get('useSetValueFunction').enable({emitEvent: false}); + + if (isEditable !== 'disabled') { + this.updateMultipleAttributesKeySettingsForm.get('disabledOnDataKey').enable({emitEvent: false}); + } + + if (dataKeyValueType === 'booleanSwitch') { + this.updateMultipleAttributesKeySettingsForm.get('slideToggleLabelPosition').enable({emitEvent: false}); + } else if (dataKeyValueType === 'select') { + this.updateMultipleAttributesKeySettingsForm.get('selectOptions').enable({emitEvent: false}); + } else if (dataKeyValueType === 'integer' || dataKeyValueType === 'double') { + this.updateMultipleAttributesKeySettingsForm.get('step').enable({emitEvent: false}); + this.updateMultipleAttributesKeySettingsForm.get('minValue').enable({emitEvent: false}); + this.updateMultipleAttributesKeySettingsForm.get('maxValue').enable({emitEvent: false}); + this.updateMultipleAttributesKeySettingsForm.get('minValueErrorMessage').enable({emitEvent: false}); + this.updateMultipleAttributesKeySettingsForm.get('maxValueErrorMessage').enable({emitEvent: false}); + } else if (dataKeyValueType === 'dateTime' || dataKeyValueType === 'date' || dataKeyValueType === 'time') { + this.updateMultipleAttributesKeySettingsForm.get('invalidDateErrorMessage').enable({emitEvent: false}); + } + if (required) { + this.updateMultipleAttributesKeySettingsForm.get('requiredErrorMessage').enable({emitEvent: false}); + } + if (useCustomIcon) { + this.updateMultipleAttributesKeySettingsForm.get('customIcon').enable({emitEvent: false}); + } else { + this.updateMultipleAttributesKeySettingsForm.get('icon').enable({emitEvent: false}); + } + if (useGetValueFunction) { + this.updateMultipleAttributesKeySettingsForm.get('getValueFunctionBody').enable({emitEvent: false}); + } + if (useSetValueFunction) { + this.updateMultipleAttributesKeySettingsForm.get('setValueFunctionBody').enable({emitEvent: false}); + } + } + this.updateMultipleAttributesKeySettingsForm.updateValueAndValidity({emitEvent: false}); + } + + protected doUpdateSettings(settingsForm: FormGroup, settings: WidgetSettings) { + settingsForm.setControl('selectOptions', this.prepareSelectOptionsFormArray(settings.selectOptions), {emitEvent: false}); + } + + private prepareSelectOptionsFormArray(selectOptions: DataKeySelectOption[] | undefined): FormArray { + const selectOptionsControls: Array = []; + if (selectOptions) { + selectOptions.forEach((selectOption) => { + selectOptionsControls.push(this.fb.control(selectOption, [dataKeySelectOptionValidator])); + }); + } + return this.fb.array(selectOptionsControls, [(control: AbstractControl) => { + const selectOptionItems = control.value; + if (!selectOptionItems || !selectOptionItems.length) { + return { + selectOptionItems: true + }; + } + return null; + }]); + } + + selectOptionsFormArray(): FormArray { + return this.updateMultipleAttributesKeySettingsForm.get('selectOptions') as FormArray; + } + + public trackBySelectOption(index: number, selectOptionControl: AbstractControl): any { + return selectOptionControl; + } + + public removeSelectOption(index: number) { + (this.updateMultipleAttributesKeySettingsForm.get('selectOptions') as FormArray).removeAt(index); + } + + public addSelectOption() { + const selectOption: DataKeySelectOption = { + value: null, + label: null + }; + const selectOptionsArray = this.updateMultipleAttributesKeySettingsForm.get('selectOptions') as FormArray; + const selectOptionControl = this.fb.control(selectOption, [dataKeySelectOptionValidator]); + (selectOptionControl as any).new = true; + selectOptionsArray.push(selectOptionControl); + this.updateMultipleAttributesKeySettingsForm.updateValueAndValidity(); + if (!this.updateMultipleAttributesKeySettingsForm.valid) { + this.onSettingsChanged(this.updateMultipleAttributesKeySettingsForm.value); + } + } + + selectOptionDrop(event: CdkDragDrop) { + const selectOptionsArray = this.updateMultipleAttributesKeySettingsForm.get('selectOptions') as FormArray; + const selectOption = selectOptionsArray.at(event.previousIndex); + selectOptionsArray.removeAt(event.previousIndex); + selectOptionsArray.insert(event.currentIndex, selectOption); + } + + displayErrorMessagesSection(): boolean { + const dataKeyHidden: boolean = this.updateMultipleAttributesKeySettingsForm.get('dataKeyHidden').value; + const required: boolean = this.updateMultipleAttributesKeySettingsForm.get('required').value; + const dataKeyValueType: string = this.updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value; + return !dataKeyHidden && (required || (['integer', 'double', 'dateTime', 'date', 'time'].includes(dataKeyValueType))); + } +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.html new file mode 100644 index 0000000000..327879ed03 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.html @@ -0,0 +1,94 @@ + +
+
+ widgets.input-widgets.general-settings + + widgets.input-widgets.widget-title + + + + {{ 'widgets.input-widgets.show-result-message' | translate }} + +
+
+ widgets.input-widgets.action-buttons + + + + + {{ 'widgets.input-widgets.show-action-buttons' | translate }} + + + + widget-config.advanced-settings + + + +
+ + {{ 'widgets.input-widgets.update-all-values' | translate }} + +
+ + widgets.input-widgets.save-button-label + + + + widgets.input-widgets.reset-button-label + + +
+
+
+
+
+
+ widgets.input-widgets.group-settings +
+ + {{ 'widgets.input-widgets.show-group-title' | translate }} + + + widgets.input-widgets.group-title + + +
+
+
+ widgets.input-widgets.fields-alignment +
+ + widgets.input-widgets.fields-alignment + + + {{ 'widgets.input-widgets.fields-alignment-row' | translate }} + + + {{ 'widgets.input-widgets.fields-alignment-column' | translate }} + + + + + widgets.input-widgets.fields-in-row + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.ts new file mode 100644 index 0000000000..167b6c5f49 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.ts @@ -0,0 +1,117 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-update-multiple-attributes-widget-settings', + templateUrl: './update-multiple-attributes-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class UpdateMultipleAttributesWidgetSettingsComponent extends WidgetSettingsComponent { + + updateMultipleAttributesWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.updateMultipleAttributesWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + widgetTitle: '', + showResultMessage: true, + showActionButtons: true, + updateAllValues: false, + saveButtonLabel: '', + resetButtonLabel: '', + showGroupTitle: false, + groupTitle: '', + fieldsAlignment: 'row', + fieldsInRow: 2 + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.updateMultipleAttributesWidgetSettingsForm = this.fb.group({ + + // General settings + + widgetTitle: [settings.widgetTitle, []], + showResultMessage: [settings.showResultMessage, []], + + // Action button settings + + showActionButtons: [settings.showActionButtons, []], + updateAllValues: [settings.updateAllValues, []], + saveButtonLabel: [settings.saveButtonLabel, []], + resetButtonLabel: [settings.resetButtonLabel, []], + + // Group settings + + showGroupTitle: [settings.showGroupTitle, []], + groupTitle: [settings.groupTitle, []], + + // Fields alignment + + fieldsAlignment: [settings.fieldsAlignment, []], + fieldsInRow: [settings.fieldsInRow, [Validators.min(1)]], + }); + } + + protected validatorTriggers(): string[] { + return ['showActionButtons', 'showGroupTitle', 'fieldsAlignment']; + } + + protected updateValidators(emitEvent: boolean) { + const showActionButtons: boolean = this.updateMultipleAttributesWidgetSettingsForm.get('showActionButtons').value; + const showGroupTitle: boolean = this.updateMultipleAttributesWidgetSettingsForm.get('showGroupTitle').value; + const fieldsAlignment: string = this.updateMultipleAttributesWidgetSettingsForm.get('fieldsAlignment').value; + + if (showActionButtons) { + this.updateMultipleAttributesWidgetSettingsForm.get('updateAllValues').enable(); + this.updateMultipleAttributesWidgetSettingsForm.get('saveButtonLabel').enable(); + this.updateMultipleAttributesWidgetSettingsForm.get('resetButtonLabel').enable(); + } else { + this.updateMultipleAttributesWidgetSettingsForm.get('updateAllValues').disable(); + this.updateMultipleAttributesWidgetSettingsForm.get('saveButtonLabel').disable(); + this.updateMultipleAttributesWidgetSettingsForm.get('resetButtonLabel').disable(); + } + if (showGroupTitle) { + this.updateMultipleAttributesWidgetSettingsForm.get('groupTitle').enable(); + } else { + this.updateMultipleAttributesWidgetSettingsForm.get('groupTitle').disable(); + } + if (fieldsAlignment === 'row') { + this.updateMultipleAttributesWidgetSettingsForm.get('fieldsInRow').enable(); + } else { + this.updateMultipleAttributesWidgetSettingsForm.get('fieldsInRow').disable(); + } + this.updateMultipleAttributesWidgetSettingsForm.get('updateAllValues').updateValueAndValidity({emitEvent}); + this.updateMultipleAttributesWidgetSettingsForm.get('saveButtonLabel').updateValueAndValidity({emitEvent}); + this.updateMultipleAttributesWidgetSettingsForm.get('resetButtonLabel').updateValueAndValidity({emitEvent}); + this.updateMultipleAttributesWidgetSettingsForm.get('groupTitle').updateValueAndValidity({emitEvent}); + this.updateMultipleAttributesWidgetSettingsForm.get('fieldsInRow').updateValueAndValidity({emitEvent}); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index bbf7e52a10..d982dba872 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -202,6 +202,15 @@ import { import { PhotoCameraInputWidgetSettingsComponent } from '@home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component'; +import { + UpdateMultipleAttributesWidgetSettingsComponent +} from '@home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component'; +import { + DataKeySelectOptionComponent +} from '@home/components/widget/lib/settings/input/datakey-select-option.component'; +import { + UpdateMultipleAttributesKeySettingsComponent +} from '@home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component'; @NgModule({ declarations: [ @@ -275,7 +284,10 @@ import { UpdateDateAttributeWidgetSettingsComponent, UpdateLocationAttributeWidgetSettingsComponent, UpdateJsonAttributeWidgetSettingsComponent, - PhotoCameraInputWidgetSettingsComponent + PhotoCameraInputWidgetSettingsComponent, + UpdateMultipleAttributesWidgetSettingsComponent, + DataKeySelectOptionComponent, + UpdateMultipleAttributesKeySettingsComponent ], imports: [ CommonModule, @@ -353,7 +365,10 @@ import { UpdateDateAttributeWidgetSettingsComponent, UpdateLocationAttributeWidgetSettingsComponent, UpdateJsonAttributeWidgetSettingsComponent, - PhotoCameraInputWidgetSettingsComponent + PhotoCameraInputWidgetSettingsComponent, + UpdateMultipleAttributesWidgetSettingsComponent, + DataKeySelectOptionComponent, + UpdateMultipleAttributesKeySettingsComponent ] }) export class WidgetSettingsModule { @@ -415,5 +430,7 @@ export const widgetSettingsComponentsMap: {[key: string]: Type {{materialIconFormGroup.get('icon').value}} - icon.icon + {{ label }} -
- - -
- - - admin.queue-name - - - {{ 'queue.name-required' | translate }} - - - - queue.poll-interval - - - {{ 'queue.poll-interval-required' | translate }} - - - {{ 'queue.poll-interval-min-value' | translate }} - - - - queue.partitions - - - {{ 'queue.partitions-required' | translate }} - - - {{ 'queue.partitions-min-value' | translate }} - - - - -
{{ 'queue.consumer-per-partition' | translate }}
-
{{'queue.consumer-per-partition-hint' | translate}}
-
- - - queue.processing-timeout - - - {{ 'queue.pack-processing-timeout-required' | translate }} - - - {{ 'queue.pack-processing-timeout-min-value' | translate }} - - - - - - - queue.submit-strategy - - - -
- - queue.submit-strategy - - - {{ strategy }} - - - - {{ 'queue.submit-strategy-type-required' | translate }} - - - - queue.batch-size - - - {{ 'queue.batch-size-required' | translate }} - - - {{ 'queue.batch-size-min-value' | translate }} - - -
-
-
- - - - queue.processing-strategy - - - -
- - queue.processing-strategy - - - {{ strategy }} - - - - {{ 'queue.processing-strategy-type-required' | translate }} - - - - queue.retries - - - {{ 'queue.retries-required' | translate }} - - - {{ 'queue.retries-min-value' | translate }} - - - - queue.failure-percentage - - - {{ 'queue.failure-percentage-required' | translate }} - - - {{ 'queue.failure-percentage-min-value' | translate }} - - - {{ 'queue.failure-percentage-max-value' | translate }} - - - - queue.pause-between-retries - - - {{ 'queue.pause-between-retries-required' | translate }} - - - {{ 'queue.pause-between-retries-min-value' | translate }} - - - - queue.max-pause-between-retries - - - {{ 'queue.max-pause-between-retries-required' | translate }} - - - {{ 'queue.max-pause-between-retries-min-value' | translate }} - - -
-
-
-
-
-
- diff --git a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.html b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.html index 59b7e063a1..34c43f240c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.html @@ -17,18 +17,34 @@ -->
- -
- - -
+ + + +
+ + {{ getName(queuesControl.value.name) }} + + + +
+
+ + + + +
-
+
tenant-profile.no-queue
diff --git a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.scss b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.scss index 9702d807a6..189badf9fd 100644 --- a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.scss +++ b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.scss @@ -13,15 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import '../../../../../../scss/constants'; :host { .tb-tenant-profile-queues { - &.mat-padding { - padding: 8px; - @media #{$mat-gt-sm} { - padding: 16px; - } + .mat-expansion-panel-body { + padding-bottom: 0 !important; } } diff --git a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts index abcde44419..a403f8e82e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts @@ -14,16 +14,16 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { Component, forwardRef, Input, OnDestroy } from '@angular/core'; import { AbstractControl, ControlValueAccessor, FormArray, FormBuilder, - FormControl, FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, + ValidationErrors, Validator, Validators } from '@angular/forms'; @@ -32,6 +32,7 @@ import { AppState } from '@app/core/core.state'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { Subscription } from 'rxjs'; import { QueueInfo } from '@shared/models/queue.models'; +import { UtilsService } from '@core/services/utils.service'; @Component({ selector: 'tb-tenant-profile-queues', @@ -50,7 +51,7 @@ import { QueueInfo } from '@shared/models/queue.models'; } ] }) -export class TenantProfileQueuesComponent implements ControlValueAccessor, OnInit, Validator { +export class TenantProfileQueuesComponent implements ControlValueAccessor, Validator, OnDestroy { tenantProfileQueuesFormGroup: FormGroup; newQueue = false; @@ -67,11 +68,12 @@ export class TenantProfileQueuesComponent implements ControlValueAccessor, OnIni @Input() disabled: boolean; - private valueChangeSubscription: Subscription = null; + private valueChangeSubscription$: Subscription = null; private propagateChange = (v: any) => { }; constructor(private store: Store, + private utils: UtilsService, private fb: FormBuilder) { } @@ -79,6 +81,12 @@ export class TenantProfileQueuesComponent implements ControlValueAccessor, OnIni this.propagateChange = fn; } + ngOnDestroy() { + if (this.valueChangeSubscription$) { + this.valueChangeSubscription$.unsubscribe(); + } + } + registerOnTouched(fn: any): void { } @@ -88,7 +96,7 @@ export class TenantProfileQueuesComponent implements ControlValueAccessor, OnIni }); } - queuesFormArray(): FormArray { + get queuesFormArray(): FormArray { return this.tenantProfileQueuesFormGroup.get('queues') as FormArray; } @@ -102,8 +110,8 @@ export class TenantProfileQueuesComponent implements ControlValueAccessor, OnIni } writeValue(queues: Array | null): void { - if (this.valueChangeSubscription) { - this.valueChangeSubscription.unsubscribe(); + if (this.valueChangeSubscription$) { + this.valueChangeSubscription$.unsubscribe(); } const queuesControls: Array = []; if (queues) { @@ -117,7 +125,7 @@ export class TenantProfileQueuesComponent implements ControlValueAccessor, OnIni } else { this.tenantProfileQueuesFormGroup.enable({emitEvent: false}); } - this.valueChangeSubscription = this.tenantProfileQueuesFormGroup.valueChanges.subscribe(() => { + this.valueChangeSubscription$ = this.tenantProfileQueuesFormGroup.valueChanges.subscribe(value => { this.updateModel(); }); } @@ -163,14 +171,18 @@ export class TenantProfileQueuesComponent implements ControlValueAccessor, OnIni } } - public validate(c: FormControl) { - return (this.tenantProfileQueuesFormGroup.valid) ? null : { + public validate(c: AbstractControl): ValidationErrors | null { + return this.tenantProfileQueuesFormGroup.valid ? null : { queues: { valid: false, }, }; } + getName(value) { + return this.utils.customTranslation(value, value); + } + private updateModel() { const queues: Array = this.tenantProfileQueuesFormGroup.get('queues').value; this.propagateChange(queues); diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.html index 81ca768911..5cf574a0e4 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.html @@ -16,17 +16,8 @@ -->
- - - -
tenant-profile.profile-configuration
-
-
- - - - -
+ +
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html index 4b511037e7..789ba8590a 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html @@ -68,13 +68,13 @@
{{'tenant.isolated-tb-rule-engine-details' | translate}}
- + -
{{'tenant-profile.queues-with-count' | translate: + {{'tenant-profile.queues-with-count' | translate: {count: entityForm.get('profileData').get('queueConfiguration').value ? - entityForm.get('profileData').get('queueConfiguration').value.length : 0} }}
+ entityForm.get('profileData').get('queueConfiguration').value.length : 0} }}
@@ -83,10 +83,19 @@ >
- - + + + +
tenant-profile.profile-configuration
+
+
+ + + + +
tenant-profile.description diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts index 285cecc5e6..6a57ee57d0 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts @@ -81,7 +81,7 @@ export class TenantProfileComponent extends EntityComponent { profileData: this.fb.group({ configuration: [entity && !this.isAdd ? entity?.profileData.configuration : createTenantProfileConfiguration(TenantProfileType.DEFAULT), []], - queueConfiguration: [null, []] + queueConfiguration: [entity && !this.isAdd ? entity?.profileData.queueConfiguration : null, []] }), description: [entity ? entity.description : '', []], } diff --git a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html new file mode 100644 index 0000000000..9ea9720b7b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html @@ -0,0 +1,171 @@ + + +
+ + admin.queue-name + + + {{ 'queue.name-required' | translate }} + + + + queue.poll-interval + + + {{ 'queue.poll-interval-required' | translate }} + + + {{ 'queue.poll-interval-min-value' | translate }} + + + + queue.partitions + + + {{ 'queue.partitions-required' | translate }} + + + {{ 'queue.partitions-min-value' | translate }} + + + +
{{ 'queue.consumer-per-partition' | translate }}
+
{{'queue.consumer-per-partition-hint' | translate}}
+
+ + queue.processing-timeout + + + {{ 'queue.pack-processing-timeout-required' | translate }} + + + {{ 'queue.pack-processing-timeout-min-value' | translate }} + + + + + + + queue.submit-strategy + + + +
+ + queue.submit-strategy + + + {{ strategy }} + + + + {{ 'queue.submit-strategy-type-required' | translate }} + + + + queue.batch-size + + + {{ 'queue.batch-size-required' | translate }} + + + {{ 'queue.batch-size-min-value' | translate }} + + +
+
+
+ + + + queue.processing-strategy + + + +
+ + queue.processing-strategy + + + {{ strategy }} + + + + {{ 'queue.processing-strategy-type-required' | translate }} + + + + queue.retries + + + {{ 'queue.retries-required' | translate }} + + + {{ 'queue.retries-min-value' | translate }} + + + + queue.failure-percentage + + + {{ 'queue.failure-percentage-required' | translate }} + + + {{ 'queue.failure-percentage-min-value' | translate }} + + + {{ 'queue.failure-percentage-max-value' | translate }} + + + + queue.pause-between-retries + + + {{ 'queue.pause-between-retries-required' | translate }} + + + {{ 'queue.pause-between-retries-min-value' | translate }} + + + + queue.max-pause-between-retries + + + {{ 'queue.max-pause-between-retries-required' | translate }} + + + {{ 'queue.max-pause-between-retries-min-value' | translate }} + + +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queue.component.scss b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.scss similarity index 100% rename from ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queue.component.scss rename to ui-ngx/src/app/modules/home/components/queue/queue-form.component.scss diff --git a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queue.component.ts b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts similarity index 78% rename from ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queue.component.ts rename to ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts index 7eb0062293..c3d4b1273a 100644 --- a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queue.component.ts +++ b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; +import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { ControlValueAccessor, FormBuilder, @@ -25,46 +25,40 @@ import { Validator, Validators } from '@angular/forms'; -import { DeviceProfileAlarm } from '@shared/models/device.models'; import { MatDialog } from '@angular/material/dialog'; import { UtilsService } from '@core/services/utils.service'; -import { QueueProcessingStrategyTypes, QueueSubmitStrategyTypes } from '@shared/models/queue.models'; +import { QueueInfo, QueueProcessingStrategyTypes, QueueSubmitStrategyTypes } from '@shared/models/queue.models'; +import { isDefinedAndNotNull } from '@core/utils'; @Component({ - selector: 'tb-tenant-profile-queue', - templateUrl: './tenant-profile-queue.component.html', - styleUrls: ['./tenant-profile-queue.component.scss'], + selector: 'tb-queue-form', + templateUrl: './queue-form.component.html', + styleUrls: ['./queue-form.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TenantProfileQueueComponent), + useExisting: forwardRef(() => QueueFormComponent), multi: true }, { provide: NG_VALIDATORS, - useExisting: forwardRef(() => TenantProfileQueueComponent), + useExisting: forwardRef(() => QueueFormComponent), multi: true, } ] }) -export class TenantProfileQueueComponent implements ControlValueAccessor, OnInit, Validator { +export class QueueFormComponent implements ControlValueAccessor, OnInit, Validator { @Input() disabled: boolean; - @Output() - removeQueue = new EventEmitter(); - @Input() - expanded = false; + newQueue = false; @Input() - mainQueue = false; + systemQueue = false; - @Input() - newQueue = false; - - private modelValue: DeviceProfileAlarm; + private modelValue: QueueInfo; queueFormGroup: FormGroup; @@ -106,7 +100,7 @@ export class TenantProfileQueueComponent implements ControlValueAccessor, OnInit packProcessingTimeout: [2000, [Validators.min(1), Validators.required]], submitStrategy: this.fb.group({ type: [null, [Validators.required]], - batchSize: [0, [Validators.min(1), Validators.required]], + batchSize: [null], }), processingStrategy: this.fb.group({ type: [null, [Validators.required]], @@ -141,20 +135,20 @@ export class TenantProfileQueueComponent implements ControlValueAccessor, OnInit } } - writeValue(value: DeviceProfileAlarm): void { + writeValue(value: QueueInfo): void { this.propagateChangePending = false; this.modelValue = value; - if (!this.modelValue.alarmType) { - this.expanded = true; + if (isDefinedAndNotNull(this.modelValue)) { + this.queueFormGroup.patchValue(this.modelValue, {emitEvent: false}); } - this.queueFormGroup.reset(this.modelValue || undefined, {emitEvent: false}); + this.submitStrategyTypeChanged(); if (!this.disabled && !this.queueFormGroup.valid) { this.updateModel(); } } public validate(c: FormControl) { - if (c.parent) { + if (c.parent && !this.systemQueue) { const queueName = c.value.name; const profileQueues = []; c.parent.getRawValue().forEach((queue) => { @@ -174,11 +168,6 @@ export class TenantProfileQueueComponent implements ControlValueAccessor, OnInit }; } - get queueTitle(): string { - const queueName = this.queueFormGroup.get('name').value; - return this.utils.customTranslation(queueName, queueName); - } - private updateModel() { const value = this.queueFormGroup.value; this.modelValue = {...this.modelValue, ...value}; @@ -194,12 +183,14 @@ export class TenantProfileQueueComponent implements ControlValueAccessor, OnInit const type: QueueSubmitStrategyTypes = form.get('type').value; const batchSizeField = form.get('batchSize'); if (type === QueueSubmitStrategyTypes.BATCH) { - batchSizeField.enable(); - batchSizeField.patchValue(1000); + batchSizeField.enable({emitEvent: false}); + batchSizeField.patchValue(1000, {emitEvent: false}); + batchSizeField.setValidators([Validators.min(1), Validators.required]); this.hideBatchSize = true; } else { - batchSizeField.patchValue(null); - batchSizeField.disable(); + batchSizeField.patchValue(null, {emitEvent: false}); + batchSizeField.disable({emitEvent: false}); + batchSizeField.clearValidators(); this.hideBatchSize = false; } } diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts index 1f1a5266ba..b886fe5711 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts @@ -49,7 +49,7 @@ import { MediaBreakpoints } from '@shared/models/constants'; import { RuleChainId } from '@shared/models/id/rule-chain-id'; import { ServiceType } from '@shared/models/queue.models'; import { deepTrim } from '@core/utils'; -import { QueueId } from "@shared/models/id/queue-id"; +import { QueueId } from '@shared/models/id/queue-id'; @Component({ selector: 'tb-device-wizard', diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts index 3f8b26f105..e5252f915f 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts @@ -187,19 +187,41 @@ const routes: Routes = [ }, { path: 'queues', - component: EntitiesTableComponent, - canDeactivate: [ConfirmOnExitGuard], data: { - auth: [Authority.SYS_ADMIN], - title: 'admin.queues', breadcrumb: { label: 'admin.queues', icon: 'swap_calls' } }, - resolve: { - entitiesTableConfig: QueuesTableConfigResolver - } + children: [ + { + path: '', + component: EntitiesTableComponent, + data: { + auth: [Authority.SYS_ADMIN], + title: 'admin.queues' + }, + resolve: { + entitiesTableConfig: QueuesTableConfigResolver + } + }, + { + path: ':entityId', + component: EntityDetailsPageComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + breadcrumb: { + labelFunction: entityDetailsPageBreadcrumbLabelFunction, + icon: 'swap_calls' + } as BreadCrumbConfig, + auth: [Authority.SYS_ADMIN], + title: 'admin.queues' + }, + resolve: { + entitiesTableConfig: QueuesTableConfigResolver + } + } + ] } ] } diff --git a/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.html b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.html index 9d7b9ead45..0432f99966 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.html @@ -16,170 +16,33 @@ -->
+ +
+ +
- -
-
-
- - admin.queue-name - - - {{ 'queue.name-required' | translate }} - - - - queue.poll-interval - - - {{ 'queue.poll-interval-required' | translate }} - - - {{ 'queue.poll-interval-min-value' | translate }} - - - - queue.partitions - - - {{ 'queue.partitions-required' | translate }} - - - {{ 'queue.partitions-min-value' | translate }} - - - - -
{{ 'queue.consumer-per-partition' | translate }}
-
{{'queue.consumer-per-partition-hint' | translate}}
-
- - - queue.processing-timeout - - - {{ 'queue.pack-processing-timeout-required' | translate }} - - - {{ 'queue.pack-processing-timeout-min-value' | translate }} - - - - - - - queue.submit-strategy - - - -
- - queue.submit-strategy - - - {{ strategy }} - - - - {{ 'queue.submit-strategy-type-required' | translate }} - - - - queue.batch-size - - - {{ 'queue.batch-size-required' | translate }} - - - {{ 'queue.batch-size-min-value' | translate }} - - -
-
-
- - - - queue.processing-strategy - - - -
- - queue.processing-strategy - - - {{ strategy }} - - - - {{ 'queue.processing-strategy-type-required' | translate }} - - - - queue.retries - - - {{ 'queue.retries-required' | translate }} - - - {{ 'queue.retries-min-value' | translate }} - - - - queue.failure-percentage - - - {{ 'queue.failure-percentage-required' | translate }} - - - {{ 'queue.failure-percentage-min-value' | translate }} - - - {{ 'queue.failure-percentage-max-value' | translate }} - - - - queue.pause-between-retries - - - {{ 'queue.pause-between-retries-required' | translate }} - - - {{ 'queue.pause-between-retries-min-value' | translate }} - - - - queue.max-pause-between-retries - - - {{ 'queue.max-pause-between-retries-required' | translate }} - - - {{ 'queue.max-pause-between-retries-min-value' | translate }} - - -
-
-
-
-
-
+
+ +
diff --git a/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.scss b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.scss deleted file mode 100644 index c87504819e..0000000000 --- a/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.scss +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -:host ::ng-deep { - .queue-form { - .mat-expansion-panel-body { - padding-bottom: 0 !important; - } - } -} diff --git a/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.ts b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.ts index f436264793..d2fbaa1b77 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.ts @@ -16,18 +16,19 @@ import { ChangeDetectorRef, Component, Inject } from '@angular/core'; import { EntityType } from '@shared/models/entity-type.models'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { FormBuilder, FormGroup } from '@angular/forms'; import { EntityComponent } from '@home/components/entity/entity.component'; -import { QueueInfo, QueueProcessingStrategyTypes, QueueSubmitStrategyTypes } from '@shared/models/queue.models'; +import { QueueInfo } from '@shared/models/queue.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { TranslateService } from '@ngx-translate/core'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; @Component({ selector: 'tb-queue', templateUrl: './queue.component.html', - styleUrls: ['./queue.component.scss'] + styleUrls: [] }) export class QueueComponent extends EntityComponent { entityForm: FormGroup; @@ -36,9 +37,6 @@ export class QueueComponent extends EntityComponent { submitStrategies: string[] = []; processingStrategies: string[] = []; - QueueSubmitStrategyTypes = QueueSubmitStrategyTypes; - hideBatchSize = false; - constructor(protected store: Store, protected translate: TranslateService, @Inject('entity') protected entityValue: QueueInfo, @@ -46,62 +44,16 @@ export class QueueComponent extends EntityComponent { protected cd: ChangeDetectorRef, public fb: FormBuilder) { super(store, fb, entityValue, entitiesTableConfigValue, cd); - this.submitStrategies = Object.values(QueueSubmitStrategyTypes); - this.processingStrategies = Object.values(QueueProcessingStrategyTypes); } ngOnInit() { super.ngOnInit(); - this.entityForm.get('submitStrategy').get('type').valueChanges.subscribe(() => { - this.submitStrategyTypeChanged(); - }); } buildForm(entity: QueueInfo): FormGroup { - return this.fb.group( - { - name: [entity ? entity.name : '', [Validators.required]], - pollInterval: [ - entity && entity.pollInterval ? entity.pollInterval : 25, - [Validators.min(1), Validators.required] - ], - partitions: [ - entity && entity.partitions ? entity.partitions : 10, - [Validators.min(1), Validators.required] - ], - consumerPerPartition: [entity ? entity.consumerPerPartition : false, []], - packProcessingTimeout: [ - entity && entity.packProcessingTimeout ? entity.packProcessingTimeout : 2000, - [Validators.min(1), Validators.required] - ], - submitStrategy: this.fb.group({ - type: [entity ? entity.submitStrategy?.type : null, [Validators.required]], - batchSize: [ - entity && entity.submitStrategy?.batchSize ? entity.submitStrategy?.batchSize : 1000, - [Validators.min(1), Validators.required] - ], - }), - processingStrategy: this.fb.group({ - type: [entity ? entity.processingStrategy?.type : null, [Validators.required]], - retries: [ - entity && entity.processingStrategy?.retries ? entity.processingStrategy?.retries : 3, - [Validators.min(0), Validators.required] - ], - failurePercentage: [ - entity && entity.processingStrategy?.failurePercentage ? entity.processingStrategy?.failurePercentage : 0, - [Validators.min(0), Validators.required, Validators.max(100)] - ], - pauseBetweenRetries: [ - entity && entity.processingStrategy?.pauseBetweenRetries ? entity.processingStrategy?.pauseBetweenRetries : 3, - [Validators.min(1), Validators.required] - ], - maxPauseBetweenRetries: [ - entity && entity.processingStrategy?.maxPauseBetweenRetries ? entity.processingStrategy?.maxPauseBetweenRetries : 3, - [Validators.min(1), Validators.required] - ], - }) - } - ); + return this.fb.group({ + queue: [entity] + }); } hideDelete() { @@ -114,41 +66,22 @@ export class QueueComponent extends EntityComponent { updateForm(entity: QueueInfo) { this.entityForm.patchValue({ - name: entity.name, - pollInterval: entity.pollInterval, - partitions: entity.partitions, - consumerPerPartition: entity.consumerPerPartition, - packProcessingTimeout: entity.packProcessingTimeout, - submitStrategy: { - type: entity.submitStrategy?.type, - batchSize: entity.submitStrategy?.batchSize, - }, - processingStrategy: { - type: entity.processingStrategy?.type, - retries: entity.processingStrategy?.retries, - failurePercentage: entity.processingStrategy?.failurePercentage, - pauseBetweenRetries: entity.processingStrategy?.pauseBetweenRetries, - maxPauseBetweenRetries: entity.processingStrategy?.maxPauseBetweenRetries, - } - }, {emitEvent: true}); + queue: entity + }, {emitEvent: false}); + } - if (!this.isAdd) { - this.entityForm.get('name').disable({emitEvent: false}); - } + prepareFormValue(formValue: any) { + return super.prepareFormValue(formValue.queue); } - submitStrategyTypeChanged() { - const form = this.entityForm.get('submitStrategy') as FormGroup; - const type: QueueSubmitStrategyTypes = form.get('type').value; - const batchSizeField = form.get('batchSize'); - if (type === QueueSubmitStrategyTypes.BATCH) { - batchSizeField.enable(); - batchSizeField.patchValue(1000); - this.hideBatchSize = true; - } else { - batchSizeField.patchValue(null); - batchSizeField.disable(); - this.hideBatchSize = false; - } + onQueueIdCopied($event) { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('queue.idCopiedMessage'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); } } diff --git a/ui-ngx/src/app/modules/home/pages/admin/queue/queues-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/admin/queue/queues-table-config.resolver.ts index aaaac2168f..5af2735a49 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/queue/queues-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/queue/queues-table-config.resolver.ts @@ -15,11 +15,8 @@ /// import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; -import { - EntityTableColumn, - EntityTableConfig -} from '@home/models/entity/entities-table-config.models'; +import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router'; +import { EntityTableColumn, EntityTableConfig } from '@home/models/entity/entities-table-config.models'; import { QueueInfo, ServiceType } from '@shared/models/queue.models'; import { select, Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -34,6 +31,7 @@ import { TranslateService } from '@ngx-translate/core'; import { QueueComponent } from './queue.component'; import { QueueService } from '@core/http/queue.service'; import { selectAuthUser } from '@core/auth/auth.selectors'; +import { EntityAction } from '@home/models/entity/entity-component.models'; @Injectable() export class QueuesTableConfigResolver implements Resolve> { @@ -48,6 +46,7 @@ export class QueuesTableConfigResolver implements Resolve this.translate.instant('queue.delete-queue-text'); this.config.deleteEntitiesTitle = count => this.translate.instant('queue.delete-queues-title', {count}); this.config.deleteEntitiesContent = () => this.translate.instant('queue.delete-queues-text'); + + this.config.onEntityAction = action => this.onQueueAction(action); } resolve(route: ActivatedRouteSnapshot): Observable> { @@ -100,16 +101,28 @@ export class QueuesTableConfigResolver implements Resolve this.queueService.getTenantQueuesByServiceType(pageLink, this.queueType); this.config.loadEntity = id => this.queueService.getQueueById(id.id); - this.config.saveEntity = queue => this.queueService.saveQueue(this.addTopicForQueue(queue), this.queueType).pipe( + this.config.saveEntity = queue => this.queueService.saveQueue(queue, this.queueType).pipe( mergeMap((savedQueue) => this.queueService.getQueueById(savedQueue.id.id) )); this.config.deleteEntity = id => this.queueService.deleteQueue(id.id); this.config.deleteEnabled = (queue) => queue && queue.name !== 'Main'; + this.config.entitySelectionEnabled = (queue) => queue && queue.name !== 'Main'; + } + + onQueueAction(action: EntityAction): boolean { + switch (action.action) { + case 'open': + this.openQueue(action.event, action.entity); + return true; + } + return false; } - private addTopicForQueue(queue: QueueInfo): QueueInfo { - const modifiedQueue = Object.assign({}, queue); - modifiedQueue.topic = `tb_rule_engine.${queue.name}`; - return modifiedQueue; + private openQueue($event: Event, queue) { + if ($event) { + $event.stopPropagation(); + } + const url = this.router.createUrlTree(['settings', 'queues', queue.id.id]); + this.router.navigateByUrl(url); } } diff --git a/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.html b/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.html index db9e899f1b..b991556b78 100644 --- a/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.html @@ -42,7 +42,7 @@ {{ translate.get('queue.no-queues-matching', - {entity: truncate.transform(searchText, true, 6, '...')}) | async }} + {queue: truncate.transform(searchText, true, 6, '...')}) | async }}
diff --git a/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.ts b/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.ts index 28f40425ea..131e99d6e9 100644 --- a/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.ts @@ -27,11 +27,11 @@ import { EntityType } from '@shared/models/entity-type.models'; import { BaseData } from '@shared/models/base-data'; import { EntityService } from '@core/http/entity.service'; import { TruncatePipe } from '@shared/pipe/truncate.pipe'; -import {QueueInfo, ServiceType} from "@shared/models/queue.models"; -import { QueueService } from "@core/http/queue.service"; -import { PageLink } from "@shared/models/page/page-link"; -import { Direction } from "@shared/models/page/sort-order"; -import { emptyPageData } from "@shared/models/page/page-data"; +import { QueueInfo, ServiceType } from '@shared/models/queue.models'; +import { QueueService } from '@core/http/queue.service'; +import { PageLink } from '@shared/models/page/page-link'; +import { Direction } from '@shared/models/page/sort-order'; +import { emptyPageData } from '@shared/models/page/page-data'; @Component({ selector: 'tb-queue-autocomplete', diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index c90af9df6a..df28c3bcfc 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -29,7 +29,7 @@ import * as _moment from 'moment'; import { AbstractControl, ValidationErrors } from '@angular/forms'; import { OtaPackageId } from '@shared/models/id/ota-package-id'; import { DashboardId } from '@shared/models/id/dashboard-id'; -import { QueueId } from "@shared/models/id/queue-id"; +import { QueueId } from '@shared/models/id/queue-id'; import { DataType } from '@shared/models/constants'; import { getDefaultProfileClientLwM2mSettingsConfig, diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 9c97cdaa82..c92c2c43db 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -428,7 +428,8 @@ export const baseDetailsPageByEntityType = new Map([ [EntityType.EDGE, '/edgeInstances'], [EntityType.ENTITY_VIEW, '/entityViews'], [EntityType.TB_RESOURCE, '/settings/resources-library'], - [EntityType.OTA_PACKAGE, '/otaUpdates'] + [EntityType.OTA_PACKAGE, '/otaUpdates'], + [EntityType.QUEUE, '/settings/queues'] ]); export interface EntitySubtype { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 880778d6c4..5c73d7a017 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2719,13 +2719,15 @@ "partitions": "Partitions", "consumer-per-partition": "Consumer per partition", "consumer-per-partition-hint": "Enable separate consumer(s) per each partition", - "processing-timeout": "Processing timeout", + "processing-timeout": "Processing timeout, ms", "batch-size": "Batch size", "retries": "Retries (0 - unlimited)", "failure-percentage": "Failure Percentage", "pause-between-retries": "Pause between retries", "max-pause-between-retries": "Maximal pause between retries", - "delete": "Delete queue" + "delete": "Delete queue", + "copyId": "Copy queue Id", + "idCopiedMessage": "Queue Id has been copied to clipboard" }, "tenant": { "tenant": "Tenant", From acd7bd98d6466095e54498c5ab8790517f16d19b Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 5 May 2022 11:06:36 +0200 Subject: [PATCH 092/276] fixed NP after changing tenant profile --- .../service/entity/tenant/DefaultTbTenantService.java | 1 + .../server/queue/discovery/HashPartitionService.java | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/entity/tenant/DefaultTbTenantService.java b/application/src/main/java/org/thingsboard/server/service/entity/tenant/DefaultTbTenantService.java index ca5bd08354..807cf6b1b1 100644 --- a/application/src/main/java/org/thingsboard/server/service/entity/tenant/DefaultTbTenantService.java +++ b/application/src/main/java/org/thingsboard/server/service/entity/tenant/DefaultTbTenantService.java @@ -50,6 +50,7 @@ public class DefaultTbTenantService implements TbTenantService { TenantProfile oldTenantProfile = oldTenant != null ? tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, oldTenant.getTenantProfileId()) : null; TenantProfile newTenantProfile = tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, savedTenant.getTenantProfileId()); + //TODO: send notifications before queue update tbQueueService.updateQueuesByTenants(Collections.singletonList(savedTenant.getTenantId()), newTenantProfile, oldTenantProfile); return savedTenant; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index 246fac00af..c5d10d918f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -155,6 +155,8 @@ public class HashPartitionService implements PartitionService { myPartitions.remove(queueKey); partitionTopicsMap.remove(queueKey); partitionSizesMap.remove(queueKey); + //TODO: remove after merging tb entity services + removeTenant(tenantId); } @Override @@ -172,10 +174,10 @@ public class HashPartitionService implements PartitionService { //TODO: replace if we can notify CheckPoint rule nodes about queue changes if (queueRoutingInfo == null) { - log.warn("Queue was removed but still used in CheckPoint rule node. [{}][{}]", tenantId, entityId); + log.debug("Queue was removed but still used in CheckPoint rule node. [{}][{}]", tenantId, entityId); queueKey = getMainQueueKey(serviceType, tenantId); } else if (!queueRoutingInfo.getTenantId().equals(getIsolatedOrSystemTenantId(serviceType, tenantId))) { - log.warn("Tenant profile was changed but CheckPoint rule node still uses the queue from system level. [{}][{}]", tenantId, entityId); + log.debug("Tenant profile was changed but CheckPoint rule node still uses the queue from system level. [{}][{}]", tenantId, entityId); queueKey = getMainQueueKey(serviceType, tenantId); } else { queueKey = new QueueKey(serviceType, queueRoutingInfo); From 3bb52b02509f0d38493e29dd049338464f286a9e Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Thu, 5 May 2022 17:41:16 +0300 Subject: [PATCH 093/276] Advanced TransactionCacheManager --- .../cache/CaffeineCacheConfiguration.java | 5 + .../CaffeineCacheTransactionStorage.java | 118 +++++++++++ .../cache/CaffeineTbCacheTransaction.java | 61 ++++++ .../CaffeineTbTransactionalCacheManager.java | 59 ++++++ .../RedisTbTransactionalCacheManager.java | 42 ++++ .../cache/TBRedisCacheConfiguration.java | 24 +-- .../cache/TBRedisClusterConfiguration.java | 8 +- .../cache/TBRedisStandaloneConfiguration.java | 18 +- .../server/cache/TbCacheTransaction.java | 16 ++ .../server/cache/TbTransactionalCache.java | 20 ++ .../server/dao/CacheDaoConfig.java | 36 ++++ .../server/dao/JpaServiceDaoConfig.java | 38 ++++ .../attributes/AttributesCacheWrapper.java | 15 -- .../attributes/CachedAttributesService.java | 58 +++--- .../DefaultAttributesCacheWrapper.java | 63 ------ .../dao/relation/BaseRelationService.java | 4 +- .../server/dao/AbstractDaoServiceTest.java | 40 ++++ .../server/dao/RedisTestSuite.java | 50 +++++ .../CachedAttributesServiceTest.java | 6 +- .../sql/attributes/AttributeServiceTest.java | 184 ++++++++++++++++++ .../attributes/RedisAttributeServiceTest.java | 30 +++ .../resources/application-test.properties | 13 ++ 22 files changed, 776 insertions(+), 132 deletions(-) create mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/CaffeineCacheTransactionStorage.java create mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbCacheTransaction.java create mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCacheManager.java create mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCacheManager.java create mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/TbCacheTransaction.java create mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/CacheDaoConfig.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/JpaServiceDaoConfig.java delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesCacheWrapper.java delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/attributes/DefaultAttributesCacheWrapper.java create mode 100644 dao/src/test/java/org/thingsboard/server/dao/AbstractDaoServiceTest.java create mode 100644 dao/src/test/java/org/thingsboard/server/dao/RedisTestSuite.java create mode 100644 dao/src/test/java/org/thingsboard/server/dao/sql/attributes/AttributeServiceTest.java create mode 100644 dao/src/test/java/org/thingsboard/server/dao/sql/attributes/RedisAttributeServiceTest.java diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineCacheConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineCacheConfiguration.java index 373b730d41..ef8ef7d8b9 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineCacheConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineCacheConfiguration.java @@ -21,6 +21,8 @@ import com.github.benmanes.caffeine.cache.Ticker; import com.github.benmanes.caffeine.cache.Weigher; import lombok.Data; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cache.CacheManager; @@ -46,6 +48,9 @@ import java.util.stream.Collectors; @Slf4j public class CaffeineCacheConfiguration { + @Value("${cache.type}") + private String test; + private Map specs; diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineCacheTransactionStorage.java b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineCacheTransactionStorage.java new file mode 100644 index 0000000000..8a9a9850fc --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineCacheTransactionStorage.java @@ -0,0 +1,118 @@ +package org.thingsboard.server.cache; + +import lombok.RequiredArgsConstructor; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@RequiredArgsConstructor +class CaffeineCacheTransactionStorage { + + private final String cacheName; + private final CaffeineTbTransactionalCacheManager cache; + private final Lock lock = new ReentrantLock(); + private final Map> objectTransactions = new HashMap<>(); + private final Map transactions = new HashMap<>(); + + + TbCacheTransaction newTransaction(List keys) { + lock.lock(); + try { + var transaction = new CaffeineTbCacheTransaction(this, keys); + var transactionId = transaction.getId(); + for (K key : keys) { + objectTransactions.computeIfAbsent(key, k -> new HashSet<>()).add(transactionId); + } + transactions.put(transactionId, transaction); + return transaction; + } finally { + lock.unlock(); + } + } + + void putIfAbsent(K key, V value) { + lock.lock(); + try { + failAllTransactionsByKey(key); + cache.doPutIfAbsent(cacheName, key, value); + } finally { + lock.unlock(); + } + } + + public void evict(K key) { + lock.lock(); + try { + failAllTransactionsByKey(key); + cache.doEvict(cacheName, key); + } finally { + lock.unlock(); + } + } + + public boolean commit(UUID trId, Map pendingPuts) { + lock.lock(); + try { + var tr = transactions.get(trId); + var success = !tr.isFailed(); + if (success) { + for (Object key : tr.getKeys()) { + Set otherTransactions = objectTransactions.get(key); + if (otherTransactions != null) { + for (UUID otherTrId : otherTransactions) { + if (trId == null || !trId.equals(otherTrId)) { + transactions.get(otherTrId).setFailed(true); + } + } + } + } + pendingPuts.forEach((k, v) -> cache.doPutIfAbsent(cacheName, k, v)); + } + removeTransaction(trId); + return success; + } finally { + lock.unlock(); + } + } + + void rollback(UUID id) { + lock.lock(); + try { + removeTransaction(id); + } finally { + lock.unlock(); + } + } + + private void removeTransaction(UUID id) { + CaffeineTbCacheTransaction transaction = transactions.remove(id); + if (transaction != null) { + for (var key : transaction.getKeys()) { + Set transactions = objectTransactions.get(key); + if (transactions != null) { + transactions.remove(id); + if (transactions.isEmpty()) { + objectTransactions.remove(key); + } + } + } + } + } + + private void failAllTransactionsByKey(K key) { + Set transactionsIds = objectTransactions.get(key); + if (transactionsIds != null) { + for (UUID otherTrId : transactionsIds) { + transactions.get(otherTrId).setFailed(true); + } + } + } + +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbCacheTransaction.java b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbCacheTransaction.java new file mode 100644 index 0000000000..202d9c4007 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbCacheTransaction.java @@ -0,0 +1,61 @@ +package org.thingsboard.server.cache; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.Executor; + +@Slf4j +@RequiredArgsConstructor +public class CaffeineTbCacheTransaction implements TbCacheTransaction { + @Getter + private final UUID id = UUID.randomUUID(); + private final CaffeineCacheTransactionStorage cache; + @Getter + private final List keys; + @Getter @Setter + private boolean failed; + + private final Map pendingPuts = new LinkedHashMap<>(); + + @Override + public void putIfAbsent(K key, V value) { + pendingPuts.put(key, value); + } + + @Override + public boolean commit() { + return cache.commit(id, pendingPuts); + } + + @Override + public void rollback() { + cache.rollback(id); + } + + @Override + public void rollBackOnFailure(ListenableFuture future, Executor executor) { + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable T result) { + } + + @Override + public void onFailure(Throwable t) { + log.trace("[{}] Rollback transaction due to error", id, t); + rollback(); + } + }, executor); + } + +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCacheManager.java b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCacheManager.java new file mode 100644 index 0000000000..e9dba40cf1 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCacheManager.java @@ -0,0 +1,59 @@ +package org.thingsboard.server.cache; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service +@RequiredArgsConstructor +public class CaffeineTbTransactionalCacheManager implements TbTransactionalCache { + + private final CacheManager cacheManager; + private final ConcurrentMap caches = new ConcurrentHashMap<>(); + + @Override + public Cache.ValueWrapper get(String cacheName, K key) { + return cacheManager.getCache(cacheName).get(key); + } + + @Override + public void putIfAbsent(String cacheName, K key, V value) { + getCache(cacheName).putIfAbsent(key, value); + } + + @Override + public void evict(String cacheName, K key) { + getCache(cacheName).evict(key); + } + + @Override + public TbCacheTransaction newTransactionForKey(String cacheName, K key) { + return getCache(cacheName).newTransaction(Collections.singletonList(key)); + } + + @Override + public TbCacheTransaction newTransactionForKeys(String cacheName, List keys) { + return getCache(cacheName).newTransaction(keys); + } + + private CaffeineCacheTransactionStorage getCache(String cacheName) { + return caches.computeIfAbsent(cacheName, cn -> new CaffeineCacheTransactionStorage(cacheName, this)); + } + + void doPutIfAbsent(String cacheName, Object key, Object value) { + cacheManager.getCache(cacheName).putIfAbsent(key, value); + } + + void doEvict(String cacheName, K key) { + cacheManager.getCache(cacheName).evict(key); + } +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCacheManager.java b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCacheManager.java new file mode 100644 index 0000000000..a4df25aeac --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCacheManager.java @@ -0,0 +1,42 @@ +package org.thingsboard.server.cache; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; + +import java.io.Serializable; +import java.util.List; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service +@RequiredArgsConstructor +public class RedisTbTransactionalCacheManager implements TbTransactionalCache { + + private final CacheManager cacheManager; + + @Override + public Cache.ValueWrapper get(String cacheName, K key) { + return cacheManager.getCache(cacheName).get(key); + } + + @Override + public void putIfAbsent(String cacheName, K key, V value) { + } + + @Override + public void evict(String cacheName, K key) { + } + + @Override + public TbCacheTransaction newTransactionForKey(String cacheName, K key) { + return null; + } + + @Override + public TbCacheTransaction newTransactionForKeys(String cacheName, List keys) { + return null; + } + +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java index ebf031c562..32127a5a39 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java @@ -36,42 +36,42 @@ import redis.clients.jedis.JedisPoolConfig; import java.time.Duration; @Configuration -@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis", matchIfMissing = false) +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @EnableCaching @Data public abstract class TBRedisCacheConfiguration { - @Value("${redis.pool_config.maxTotal}") + @Value("${redis.pool_config.maxTotal:128}") private int maxTotal; - @Value("${redis.pool_config.maxIdle}") + @Value("${redis.pool_config.maxIdle:128}") private int maxIdle; - @Value("${redis.pool_config.minIdle}") + @Value("${redis.pool_config.minIdle:16}") private int minIdle; - @Value("${redis.pool_config.testOnBorrow}") + @Value("${redis.pool_config.testOnBorrow:true}") private boolean testOnBorrow; - @Value("${redis.pool_config.testOnReturn}") + @Value("${redis.pool_config.testOnReturn:true}") private boolean testOnReturn; - @Value("${redis.pool_config.testWhileIdle}") + @Value("${redis.pool_config.testWhileIdle:true}") private boolean testWhileIdle; - @Value("${redis.pool_config.minEvictableMs}") + @Value("${redis.pool_config.minEvictableMs:60000}") private long minEvictableMs; - @Value("${redis.pool_config.evictionRunsMs}") + @Value("${redis.pool_config.evictionRunsMs:30000}") private long evictionRunsMs; - @Value("${redis.pool_config.maxWaitMills}") + @Value("${redis.pool_config.maxWaitMills:60000}") private long maxWaitMills; - @Value("${redis.pool_config.numberTestsPerEvictionRun}") + @Value("${redis.pool_config.numberTestsPerEvictionRun:3}") private int numberTestsPerEvictionRun; - @Value("${redis.pool_config.blockWhenExhausted}") + @Value("${redis.pool_config.blockWhenExhausted:true}") private boolean blockWhenExhausted; @Bean diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java index 3538b6e0d7..4925842ad3 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java @@ -36,16 +36,16 @@ public class TBRedisClusterConfiguration extends TBRedisCacheConfiguration { private static final String COMMA = ","; private static final String COLON = ":"; - @Value("${redis.cluster.nodes}") + @Value("${redis.cluster.nodes:}") private String clusterNodes; - @Value("${redis.cluster.max-redirects}") + @Value("${redis.cluster.max-redirects:12}") private Integer maxRedirects; - @Value("${redis.cluster.useDefaultPoolConfig}") + @Value("${redis.cluster.useDefaultPoolConfig:true}") private boolean useDefaultPoolConfig; - @Value("${redis.password}") + @Value("${redis.password:}") private String password; public JedisConnectionFactory loadFactory() { diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisStandaloneConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisStandaloneConfiguration.java index 6a5b6e15e6..482c96d0da 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisStandaloneConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisStandaloneConfiguration.java @@ -30,31 +30,31 @@ import java.time.Duration; @ConditionalOnProperty(prefix = "redis.connection", value = "type", havingValue = "standalone") public class TBRedisStandaloneConfiguration extends TBRedisCacheConfiguration { - @Value("${redis.standalone.host}") + @Value("${redis.standalone.host:localhost}") private String host; - @Value("${redis.standalone.port}") + @Value("${redis.standalone.port:6379}") private Integer port; - @Value("${redis.standalone.clientName}") + @Value("${redis.standalone.clientName:standalone}") private String clientName; - @Value("${redis.standalone.connectTimeout}") + @Value("${redis.standalone.connectTimeout:30000}") private Long connectTimeout; - @Value("${redis.standalone.readTimeout}") + @Value("${redis.standalone.readTimeout:60000}") private Long readTimeout; - @Value("${redis.standalone.useDefaultClientConfig}") + @Value("${redis.standalone.useDefaultClientConfig:true}") private boolean useDefaultClientConfig; - @Value("${redis.standalone.usePoolConfig}") + @Value("${redis.standalone.usePoolConfig:false}") private boolean usePoolConfig; - @Value("${redis.db}") + @Value("${redis.db:0}") private Integer db; - @Value("${redis.password}") + @Value("${redis.password:}") private String password; public JedisConnectionFactory loadFactory() { diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbCacheTransaction.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbCacheTransaction.java new file mode 100644 index 0000000000..22bbec4489 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbCacheTransaction.java @@ -0,0 +1,16 @@ +package org.thingsboard.server.cache; + +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.concurrent.Executor; + +public interface TbCacheTransaction { + + void putIfAbsent(K key, V value); + + boolean commit(); + + void rollback(); + + void rollBackOnFailure(ListenableFuture result, Executor cacheExecutor); +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java new file mode 100644 index 0000000000..ad370f3d18 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java @@ -0,0 +1,20 @@ +package org.thingsboard.server.cache; + +import org.springframework.cache.Cache; + +import java.io.Serializable; +import java.util.List; + +public interface TbTransactionalCache { + + Cache.ValueWrapper get(String cacheName, K key); + + void putIfAbsent(String cacheName, K key, V value); + + void evict(String cacheName, K key); + + TbCacheTransaction newTransactionForKey(String cacheName, K key); + + TbCacheTransaction newTransactionForKeys(String cacheName, List keys); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/CacheDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/CacheDaoConfig.java new file mode 100644 index 0000000000..f23ae62165 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/CacheDaoConfig.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao; + +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.thingsboard.server.dao.util.TbAutoConfiguration; + +/** + * @author Valerii Sosliuk + */ +@Configuration +@TbAutoConfiguration +@ComponentScan({"org.thingsboard.server.dao.sql", "org.thingsboard.server.dao.attributes"}) +@EnableJpaRepositories("org.thingsboard.server.dao.sql") +@EntityScan("org.thingsboard.server.dao.model.sql") +@EnableTransactionManagement +public class CacheDaoConfig { + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/JpaServiceDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/JpaServiceDaoConfig.java new file mode 100644 index 0000000000..46d067fc4a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/JpaServiceDaoConfig.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao; + +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.thingsboard.server.dao.util.TbAutoConfiguration; + +/** + * @author Valerii Sosliuk + */ +@Configuration +@EnableCaching +@TbAutoConfiguration +@ComponentScan({"org.thingsboard.server.dao.sql", "org.thingsboard.server.dao.attributes", "org.thingsboard.server.dao.cache", "org.thingsboard.server.cache"}) +@EnableJpaRepositories("org.thingsboard.server.dao.sql") +@EntityScan("org.thingsboard.server.dao.model.sql") +@EnableTransactionManagement +public class JpaServiceDaoConfig { + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesCacheWrapper.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesCacheWrapper.java deleted file mode 100644 index 466cd4b0f1..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesCacheWrapper.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.thingsboard.server.dao.attributes; - -import org.springframework.cache.Cache; -import org.thingsboard.server.common.data.kv.AttributeKvEntry; - -public interface AttributesCacheWrapper { - - Cache.ValueWrapper get(AttributeCacheKey attributeCacheKey); - - void put(AttributeCacheKey attributeCacheKey, AttributeKvEntry attributeKvEntry); - - void putIfAbsent(AttributeCacheKey attributeCacheKey, AttributeKvEntry attributeKvEntry); - - void evict(AttributeCacheKey attributeCacheKey); -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index abe741d6d7..d9e172c113 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -25,6 +25,7 @@ import org.springframework.cache.Cache; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; @@ -33,6 +34,8 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.stats.DefaultCounter; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.cache.CacheExecutorService; +import org.thingsboard.server.cache.TbCacheTransaction; +import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.dao.service.Validator; import javax.annotation.PostConstruct; @@ -59,22 +62,22 @@ public class CachedAttributesService implements AttributesService { public static final String LOCAL_CACHE_TYPE = "caffeine"; private final AttributesDao attributesDao; - private final AttributesCacheWrapper cacheWrapper; private final CacheExecutorService cacheExecutorService; private final DefaultCounter hitCounter; private final DefaultCounter missCounter; + private final TbTransactionalCache cache; private Executor cacheExecutor; @Value("${cache.type}") private String cacheType; public CachedAttributesService(AttributesDao attributesDao, - AttributesCacheWrapper cacheWrapper, StatsFactory statsFactory, - CacheExecutorService cacheExecutorService) { + CacheExecutorService cacheExecutorService, + TbTransactionalCache cache) { this.attributesDao = attributesDao; - this.cacheWrapper = cacheWrapper; this.cacheExecutorService = cacheExecutorService; + this.cache = cache; this.hitCounter = statsFactory.createDefaultCounter(STATS_NAME, "result", "hit"); this.missCounter = statsFactory.createDefaultCounter(STATS_NAME, "result", "miss"); @@ -99,23 +102,26 @@ public class CachedAttributesService implements AttributesService { return cacheExecutorService; } + @Override public ListenableFuture> find(TenantId tenantId, EntityId entityId, String scope, String attributeKey) { validate(entityId, scope); Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey); AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, attributeKey); - Cache.ValueWrapper cachedAttributeValue = cacheWrapper.get(attributeCacheKey); + Cache.ValueWrapper cachedAttributeValue = cache.get(CacheConstants.ATTRIBUTES_CACHE, attributeCacheKey); if (cachedAttributeValue != null) { hitCounter.increment(); AttributeKvEntry cachedAttributeKvEntry = (AttributeKvEntry) cachedAttributeValue.get(); return Futures.immediateFuture(Optional.ofNullable(cachedAttributeKvEntry)); } else { missCounter.increment(); + TbCacheTransaction cacheTransaction = cache.newTransactionForKey(CacheConstants.ATTRIBUTES_CACHE, attributeCacheKey); ListenableFuture> result = attributesDao.find(tenantId, entityId, scope, attributeKey); + cacheTransaction.rollBackOnFailure(result, cacheExecutor); return Futures.transform(result, foundAttrKvEntry -> { - // TODO: think if it's a good idea to store 'empty' attributes - cacheWrapper.putIfAbsent(attributeCacheKey, foundAttrKvEntry.orElse(null)); + cacheTransaction.putIfAbsent(attributeCacheKey, foundAttrKvEntry.orElse(null)); + cacheTransaction.commit(); return foundAttrKvEntry; }, cacheExecutor); } @@ -139,15 +145,31 @@ public class CachedAttributesService implements AttributesService { Set notFoundAttributeKeys = new HashSet<>(attributeKeys); notFoundAttributeKeys.removeAll(wrappedCachedAttributes.keySet()); + List notFoundKeys = notFoundAttributeKeys.stream().map(k -> new AttributeCacheKey(scope, entityId, k)).collect(Collectors.toList()); + + TbCacheTransaction cacheTransaction = cache.newTransactionForKeys(CacheConstants.ATTRIBUTES_CACHE, notFoundKeys); ListenableFuture> result = attributesDao.find(tenantId, entityId, scope, notFoundAttributeKeys); - return Futures.transform(result, foundInDbAttributes -> mergeDbAndCacheAttributes(entityId, scope, cachedAttributes, notFoundAttributeKeys, foundInDbAttributes), cacheExecutor); + return Futures.transform(result, foundInDbAttributes -> { + for (AttributeKvEntry foundInDbAttribute : foundInDbAttributes) { + AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, foundInDbAttribute.getKey()); + cacheTransaction.putIfAbsent(attributeCacheKey, foundInDbAttribute); + notFoundAttributeKeys.remove(foundInDbAttribute.getKey()); + } + for (String key : notFoundAttributeKeys) { + cacheTransaction.putIfAbsent(new AttributeCacheKey(scope, entityId, key), null); + } + List mergedAttributes = new ArrayList<>(cachedAttributes); + mergedAttributes.addAll(foundInDbAttributes); + cacheTransaction.commit(); + return mergedAttributes; + }, cacheExecutor); } private Map findCachedAttributes(EntityId entityId, String scope, Collection attributeKeys) { Map cachedAttributes = new HashMap<>(); for (String attributeKey : attributeKeys) { - Cache.ValueWrapper cachedAttributeValue = cacheWrapper.get(new AttributeCacheKey(scope, entityId, attributeKey)); + Cache.ValueWrapper cachedAttributeValue = cache.get(CacheConstants.ATTRIBUTES_CACHE, new AttributeCacheKey(scope, entityId, attributeKey)); if (cachedAttributeValue != null) { hitCounter.increment(); cachedAttributes.put(attributeKey, cachedAttributeValue); @@ -158,20 +180,6 @@ public class CachedAttributesService implements AttributesService { return cachedAttributes; } - private List mergeDbAndCacheAttributes(EntityId entityId, String scope, List cachedAttributes, Set notFoundAttributeKeys, List foundInDbAttributes) { - for (AttributeKvEntry foundInDbAttribute : foundInDbAttributes) { - AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, foundInDbAttribute.getKey()); - cacheWrapper.putIfAbsent(attributeCacheKey, foundInDbAttribute); - notFoundAttributeKeys.remove(foundInDbAttribute.getKey()); - } -// for (String key : notFoundAttributeKeys) { -// cacheWrapper.putIfAbsent(new AttributeCacheKey(scope, entityId, key), null); -// } - List mergedAttributes = new ArrayList<>(cachedAttributes); - mergedAttributes.addAll(foundInDbAttributes); - return mergedAttributes; - } - @Override public ListenableFuture> findAll(TenantId tenantId, EntityId entityId, String scope) { validate(entityId, scope); @@ -197,7 +205,7 @@ public class CachedAttributesService implements AttributesService { for (var attribute : attributes) { ListenableFuture future = attributesDao.save(tenantId, entityId, scope, attribute); futures.add(Futures.transform(future, key -> { - cacheWrapper.evict(new AttributeCacheKey(scope, entityId, key)); + cache.evict(CacheConstants.ATTRIBUTES_CACHE, new AttributeCacheKey(scope, entityId, key)); return key; }, cacheExecutor)); } @@ -210,7 +218,7 @@ public class CachedAttributesService implements AttributesService { validate(entityId, scope); List> futures = attributesDao.removeAll(tenantId, entityId, scope, attributeKeys); return Futures.allAsList(futures.stream().map(future -> Futures.transform(future, key -> { - cacheWrapper.evict(new AttributeCacheKey(scope, entityId, key)); + cache.evict(CacheConstants.ATTRIBUTES_CACHE, new AttributeCacheKey(scope, entityId, key)); return key; }, cacheExecutor)).collect(Collectors.toList())); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/DefaultAttributesCacheWrapper.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/DefaultAttributesCacheWrapper.java deleted file mode 100644 index aa0222551a..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/DefaultAttributesCacheWrapper.java +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.dao.attributes; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.cache.Cache; -import org.springframework.cache.CacheManager; -import org.springframework.context.annotation.Primary; -import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.kv.AttributeKvEntry; - -import static org.thingsboard.server.common.data.CacheConstants.ATTRIBUTES_CACHE; - -@Service -@ConditionalOnProperty(prefix = "cache.attributes", value = "enabled", havingValue = "true") -@Primary -@Slf4j -public class DefaultAttributesCacheWrapper implements AttributesCacheWrapper { - private final Cache attributesCache; - - public DefaultAttributesCacheWrapper(CacheManager cacheManager) { - this.attributesCache = cacheManager.getCache(ATTRIBUTES_CACHE); - } - - @Override - public Cache.ValueWrapper get(AttributeCacheKey attributeCacheKey) { - var result = attributesCache.get(attributeCacheKey); - log.warn("[{}] Get = {}", attributeCacheKey, result); - return result; - } - - @Override - public void put(AttributeCacheKey attributeCacheKey, AttributeKvEntry attributeKvEntry) { - log.warn("[{}] Put = {}", attributeCacheKey, attributeKvEntry); - attributesCache.put(attributeCacheKey, attributeKvEntry); - } - - @Override - public void putIfAbsent(AttributeCacheKey attributeCacheKey, AttributeKvEntry attributeKvEntry) { - var result = attributesCache.putIfAbsent(attributeCacheKey, attributeKvEntry); - log.warn("[{}] Put if absent = {}, result = {}", attributeCacheKey, attributeKvEntry, result); - } - - @Override - public void evict(AttributeCacheKey attributeCacheKey) { - log.warn("[{}] Evict", attributeCacheKey); - attributesCache.evict(attributeCacheKey); - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index e62669586e..e2e1ba7d2b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -320,7 +320,7 @@ public class BaseRelationService implements RelationService { cache.evict(toTypeAndTypeGroup); } - @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup, 'FROM'}") +// @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup, 'FROM'}") @Transactional(propagation = Propagation.SUPPORTS) @Override public List findByFrom(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup) { @@ -381,7 +381,7 @@ public class BaseRelationService implements RelationService { }, MoreExecutors.directExecutor()); } - @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType, #typeGroup, 'FROM'}") +// @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType, #typeGroup, 'FROM'}") @Override public List findByFromAndType(TenantId tenantId, EntityId from, String relationType, RelationTypeGroup typeGroup) { try { diff --git a/dao/src/test/java/org/thingsboard/server/dao/AbstractDaoServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/AbstractDaoServiceTest.java new file mode 100644 index 0000000000..25c423eddb --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/AbstractDaoServiceTest.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao; + +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.support.DirtiesContextTestExecutionListener; +import org.thingsboard.server.common.stats.StatsFactory; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = {JpaServiceDaoConfig.class, PsqlTsDaoConfig.class, PsqlTsLatestDaoConfig.class, SqlTimeseriesDaoConfig.class}) +@DaoSqlTest +@TestExecutionListeners({ + DependencyInjectionTestExecutionListener.class, + DirtiesContextTestExecutionListener.class}) +public abstract class AbstractDaoServiceTest { + + @MockBean(answer = Answers.RETURNS_MOCKS) + StatsFactory statsFactory; + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/RedisTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/RedisTestSuite.java new file mode 100644 index 0000000000..21838ebc2c --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/RedisTestSuite.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao; + +import org.junit.ClassRule; +import org.junit.extensions.cpsuite.ClasspathSuite; +import org.junit.extensions.cpsuite.ClasspathSuite.ClassnameFilters; +import org.junit.runner.RunWith; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.support.TestPropertySourceUtils; +import org.testcontainers.containers.GenericContainer; + +@ContextConfiguration(initializers = RedisTestSuite.class) +@RunWith(ClasspathSuite.class) +@ClassnameFilters({ + "org.thingsboard.server.dao.sql.attributes.*ServiceTest", +}) +public class RedisTestSuite implements ApplicationContextInitializer { + + @ClassRule + public static GenericContainer redis = new GenericContainer("redis:4.0").withExposedPorts(6379); + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + TestPropertySourceUtils.addInlinedPropertiesToEnvironment( + applicationContext, "cache.type=redis"); + TestPropertySourceUtils.addInlinedPropertiesToEnvironment( + applicationContext, "redis.connection.type=standalone"); + TestPropertySourceUtils.addInlinedPropertiesToEnvironment( + applicationContext, "redis.standalone.host=localhost"); + TestPropertySourceUtils.addInlinedPropertiesToEnvironment( + applicationContext, "redis.standalone.port=" + redis.getMappedPort(6379)); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/attributes/CachedAttributesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/attributes/CachedAttributesServiceTest.java index a45eb0b711..4f699912e3 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/attributes/CachedAttributesServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/attributes/CachedAttributesServiceTest.java @@ -17,6 +17,9 @@ package org.thingsboard.server.dao.attributes; import com.google.common.util.concurrent.MoreExecutors; import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.dao.AbstractJpaDaoTest; +import org.thingsboard.server.dao.alarm.AlarmDao; import org.thingsboard.server.dao.cache.CacheExecutorService; import static org.hamcrest.CoreMatchers.is; @@ -25,7 +28,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.willCallRealMethod; import static org.mockito.Mockito.mock; -public class CachedAttributesServiceTest { +public class CachedAttributesServiceTest extends AbstractJpaDaoTest { public static final String REDIS = "redis"; @@ -57,7 +60,6 @@ public class CachedAttributesServiceTest { assertThat(cachedAttributesService.getExecutor(REDIS, cacheExecutorService), is(cacheExecutorService)); assertThat(cachedAttributesService.getExecutor("unknownCacheType", cacheExecutorService), is(cacheExecutorService)); - } } \ No newline at end of file diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/AttributeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/AttributeServiceTest.java new file mode 100644 index 0000000000..02b9ef54d6 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/AttributeServiceTest.java @@ -0,0 +1,184 @@ +package org.thingsboard.server.dao.sql.attributes; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.dao.AbstractDaoServiceTest; +import org.thingsboard.server.dao.attributes.CachedAttributesService; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +public class AttributeServiceTest extends AbstractDaoServiceTest { + + private static final String OLD_VALUE = "OLD VALUE"; + private static final String NEW_VALUE = "NEW VALUE"; + + @Autowired + private CachedAttributesService attributesService; + + @Test + public void testDummyRequestWithEmptyResult() throws Exception { + var future = attributesService.find(new TenantId(UUID.randomUUID()), new DeviceId(UUID.randomUUID()), DataConstants.SERVER_SCOPE, "TEST"); + Assert.assertNotNull(future); + var result = future.get(10, TimeUnit.SECONDS); + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void testConcurrentFetchAndUpdate() throws Exception { + var tenantId = new TenantId(UUID.randomUUID()); + ListeningExecutorService pool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(2)); + try { + for (int i = 0; i < 100; i++) { + var deviceId = new DeviceId(UUID.randomUUID()); + testConcurrentFetchAndUpdate(tenantId, deviceId, pool); + } + } finally { + pool.shutdownNow(); + } + } + + @Test + public void testConcurrentFetchAndUpdateMulti() throws Exception { + var tenantId = new TenantId(UUID.randomUUID()); + ListeningExecutorService pool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(2)); + try { + for (int i = 0; i < 100; i++) { + var deviceId = new DeviceId(UUID.randomUUID()); + testConcurrentFetchAndUpdateMulti(tenantId, deviceId, pool); + } + } finally { + pool.shutdownNow(); + } + } + + @Test + public void testFetchAndUpdateEmpty() throws Exception { + var tenantId = new TenantId(UUID.randomUUID()); + var deviceId = new DeviceId(UUID.randomUUID()); + var scope = DataConstants.SERVER_SCOPE; + var key = "TEST"; + + Optional emptyValue = attributesService.find(tenantId, deviceId, scope, key).get(10, TimeUnit.SECONDS); + Assert.assertTrue(emptyValue.isEmpty()); + + saveAttribute(tenantId, deviceId, scope, key, NEW_VALUE); + Assert.assertEquals(NEW_VALUE, getAttributeValue(tenantId, deviceId, scope, key)); + } + + @Test + public void testFetchAndUpdateMulti() throws Exception { + var tenantId = new TenantId(UUID.randomUUID()); + var deviceId = new DeviceId(UUID.randomUUID()); + var scope = DataConstants.SERVER_SCOPE; + var key1 = "TEST1"; + var key2 = "TEST2"; + + var value = getAttributeValues(tenantId, deviceId, scope, Arrays.asList(key1, key2)); + Assert.assertTrue(value.isEmpty()); + + saveAttribute(tenantId, deviceId, scope, key1, OLD_VALUE); + + value = getAttributeValues(tenantId, deviceId, scope, Arrays.asList(key1, key2)); + Assert.assertEquals(1, value.size()); + Assert.assertEquals(OLD_VALUE, value.get(0)); + + saveAttribute(tenantId, deviceId, scope, key2, NEW_VALUE); + + value = getAttributeValues(tenantId, deviceId, scope, Arrays.asList(key1, key2)); + Assert.assertEquals(2, value.size()); + Assert.assertTrue(value.contains(OLD_VALUE)); + Assert.assertTrue(value.contains(NEW_VALUE)); + + saveAttribute(tenantId, deviceId, scope, key1, NEW_VALUE); + + value = getAttributeValues(tenantId, deviceId, scope, Arrays.asList(key1, key2)); + Assert.assertEquals(2, value.size()); + Assert.assertEquals(NEW_VALUE, value.get(0)); + Assert.assertEquals(NEW_VALUE, value.get(1)); + } + + private void testConcurrentFetchAndUpdate(TenantId tenantId, DeviceId deviceId, ListeningExecutorService pool) throws Exception { + var scope = DataConstants.SERVER_SCOPE; + var key = "TEST"; + saveAttribute(tenantId, deviceId, scope, key, OLD_VALUE); + List> futures = new ArrayList<>(); + futures.add(pool.submit(() -> { + var value = getAttributeValue(tenantId, deviceId, scope, key); + Assert.assertTrue(value.equals(OLD_VALUE) || value.equals(NEW_VALUE)); + })); + futures.add(pool.submit(() -> saveAttribute(tenantId, deviceId, scope, key, NEW_VALUE))); + Futures.allAsList(futures).get(10, TimeUnit.SECONDS); + Assert.assertEquals(NEW_VALUE, getAttributeValue(tenantId, deviceId, scope, key)); + } + + private void testConcurrentFetchAndUpdateMulti(TenantId tenantId, DeviceId deviceId, ListeningExecutorService pool) throws Exception { + var scope = DataConstants.SERVER_SCOPE; + var key1 = "TEST1"; + var key2 = "TEST2"; + saveAttribute(tenantId, deviceId, scope, key1, OLD_VALUE); + saveAttribute(tenantId, deviceId, scope, key2, OLD_VALUE); + List> futures = new ArrayList<>(); + futures.add(pool.submit(() -> { + var value = getAttributeValues(tenantId, deviceId, scope, Arrays.asList(key1, key2)); + Assert.assertEquals(2, value.size()); + Assert.assertTrue(value.contains(OLD_VALUE) || value.contains(NEW_VALUE)); + })); + futures.add(pool.submit(() -> { + saveAttribute(tenantId, deviceId, scope, key1, NEW_VALUE); + saveAttribute(tenantId, deviceId, scope, key2, NEW_VALUE); + })); + Futures.allAsList(futures).get(10, TimeUnit.SECONDS); + var newResult = getAttributeValues(tenantId, deviceId, scope, Arrays.asList(key1, key2)); + Assert.assertEquals(2, newResult.size()); + Assert.assertEquals(NEW_VALUE, newResult.get(0)); + Assert.assertEquals(NEW_VALUE, newResult.get(1)); + } + + private String getAttributeValue(TenantId tenantId, DeviceId deviceId, String scope, String key) { + try { + Optional entry = attributesService.find(tenantId, deviceId, scope, key).get(10, TimeUnit.SECONDS); + return entry.orElseThrow(RuntimeException::new).getStrValue().orElse("Unknown"); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private List getAttributeValues(TenantId tenantId, DeviceId deviceId, String scope, List keys) { + try { + List entry = attributesService.find(tenantId, deviceId, scope, keys).get(10, TimeUnit.SECONDS); + return entry.stream().map(e -> e.getStrValue().orElse(null)).collect(Collectors.toList()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void saveAttribute(TenantId tenantId, DeviceId deviceId, String scope, String key, String s) { + try { + AttributeKvEntry newEntry = new BaseAttributeKvEntry(System.currentTimeMillis(), new StringDataEntry(key, s)); + attributesService.save(tenantId, deviceId, scope, Collections.singletonList(newEntry)).get(10, TimeUnit.SECONDS); + } catch (Exception e) { + Assert.assertNull(e); + } + } + + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/RedisAttributeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/RedisAttributeServiceTest.java new file mode 100644 index 0000000000..09a0ccf9d4 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/RedisAttributeServiceTest.java @@ -0,0 +1,30 @@ +package org.thingsboard.server.dao.sql.attributes; + +import lombok.extern.slf4j.Slf4j; +import org.junit.ClassRule; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.support.TestPropertySourceUtils; +import org.testcontainers.containers.GenericContainer; + +@TestPropertySource(properties = { + "cache.type=redis", "redis.connection.type=standalone" +}) +@ContextConfiguration(initializers = RedisAttributeServiceTest.class) +@Slf4j +public class RedisAttributeServiceTest extends AttributeServiceTest implements ApplicationContextInitializer { + + @ClassRule + public static GenericContainer redis = new GenericContainer("redis:4.0").withExposedPorts(6379); + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + TestPropertySourceUtils.addInlinedPropertiesToEnvironment( + applicationContext, "redis.standalone.host=localhost"); + TestPropertySourceUtils.addInlinedPropertiesToEnvironment( + applicationContext, "redis.standalone.port=" + redis.getMappedPort(6379)); + } + +} diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index 5d02ae84d5..d61bd4eb65 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -11,6 +11,7 @@ audit-log.sink.type=none cache.type=caffeine cache.maximumPoolSize=16 +cache.attributes.enabled=true #cache.type=redis caffeine.specs.relations.timeToLiveInMinutes=1440 @@ -22,6 +23,9 @@ caffeine.specs.deviceCredentials.maxSize=100000 caffeine.specs.devices.timeToLiveInMinutes=1440 caffeine.specs.devices.maxSize=100000 +caffeine.specs.sessions.timeToLiveInMinutes=1440 +caffeine.specs.sessions.maxSize=100000 + caffeine.specs.assets.timeToLiveInMinutes=1440 caffeine.specs.assets.maxSize=100000 @@ -31,12 +35,21 @@ caffeine.specs.entityViews.maxSize=100000 caffeine.specs.claimDevices.timeToLiveInMinutes=1440 caffeine.specs.claimDevices.maxSize=100000 +caffeine.specs.securitySettings.timeToLiveInMinutes=1440 +caffeine.specs.securitySettings.maxSize=100000 + caffeine.specs.tenantProfiles.timeToLiveInMinutes=1440 caffeine.specs.tenantProfiles.maxSize=100000 caffeine.specs.deviceProfiles.timeToLiveInMinutes=1440 caffeine.specs.deviceProfiles.maxSize=100000 +caffeine.specs.attributes.timeToLiveInMinutes=1440 +caffeine.specs.attributes.maxSize=100000 + +caffeine.specs.tokensOutdatageTime.timeToLiveInMinutes=1440 +caffeine.specs.tokensOutdatageTime.maxSize=100000 + caffeine.specs.otaPackages.timeToLiveInMinutes=1440 caffeine.specs.otaPackages.maxSize=100000 From 4480badd7862b546b471e29e5542ffb75fcc05d2 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Thu, 5 May 2022 18:08:54 +0300 Subject: [PATCH 094/276] Email 2FA provider; 2FA API improvements --- .../controller/TwoFaConfigController.java | 168 +++++++++++++----- .../controller/TwoFactorAuthController.java | 11 +- .../service/mail/DefaultMailService.java | 9 +- .../mfa/config/DefaultTwoFaConfigManager.java | 46 ++--- .../auth/mfa/config/TwoFaConfigManager.java | 3 +- .../auth/mfa/provider/TwoFaProvider.java | 4 +- .../mfa/provider/impl/EmailTwoFaProvider.java | 57 ++++++ .../provider/impl/OtpBasedTwoFaProvider.java | 14 +- .../mfa/provider/impl/TotpTwoFaProvider.java | 2 +- .../model/mfa/PlatformTwoFaSettings.java | 15 +- .../mfa/account/EmailTwoFaAccountConfig.java | 38 ++++ .../mfa/account/SmsTwoFaAccountConfig.java | 4 - .../mfa/account/TotpTwoFaAccountConfig.java | 5 - .../model/mfa/account/TwoFaAccountConfig.java | 3 +- .../provider/EmailTwoFaProviderConfig.java | 30 ++++ .../provider/OtpBasedTwoFaProviderConfig.java | 5 +- .../mfa/provider/SmsTwoFaProviderConfig.java | 6 - .../mfa/provider/TotpTwoFaProviderConfig.java | 5 - .../mfa/provider/TwoFaProviderConfig.java | 3 +- .../model/mfa/provider/TwoFaProviderType.java | 3 +- .../rule/engine/api/MailService.java | 4 +- 21 files changed, 313 insertions(+), 122 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/EmailTwoFaProvider.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/EmailTwoFaAccountConfig.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/EmailTwoFaProviderConfig.java diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFaConfigController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFaConfigController.java index 6eb192b7e2..60541b1ce4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFaConfigController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFaConfigController.java @@ -33,7 +33,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings; @@ -51,6 +50,7 @@ import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; @@ -65,20 +65,15 @@ public class TwoFaConfigController extends BaseController { private final TwoFactorAuthService twoFactorAuthService; - @ApiOperation(value = "Get 2FA account config (getTwoFaAccountConfig)", - notes = "Get user's account 2FA configuration. Returns empty result if user did not configured 2FA, " + - "or if a provider for previously set up account config is not now configured." + NEW_LINE + - ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER + NEW_LINE + - "Response example for TOTP 2FA: " + NEW_LINE + - "```\n{\n" + - " \"providerType\": \"TOTP\",\n" + - " \"authUrl\": \"otpauth://totp/ThingsBoard:tenant@thingsboard.org?issuer=ThingsBoard&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII\"\n" + - "}\n```" + NEW_LINE + - "Response example for SMS 2FA: " + NEW_LINE + - "```\n{\n" + - " \"providerType\": \"SMS\",\n" + - " \"phoneNumber\": \"+380505005050\"\n" + - "}\n```") + @ApiOperation(value = "Get account 2FA settings (getAccountTwoFaSettings)", + notes = "Get user's account 2FA configuration. Configuration contains configs for different 2FA providers." + NEW_LINE + + "Example:\n" + + "```\n{\n \"configs\": {\n" + + " \"EMAIL\": {\n \"providerType\": \"EMAIL\",\n \"useByDefault\": true,\n \"email\": \"tenant@thingsboard.org\"\n },\n" + + " \"TOTP\": {\n \"providerType\": \"TOTP\",\n \"useByDefault\": false,\n \"authUrl\": \"otpauth://totp/TB%202FA:tenant@thingsboard.org?issuer=TB+2FA&secret=P6Z2TLYTASOGP6LCJZAD24ETT5DACNNX\"\n },\n" + + " \"SMS\": {\n \"providerType\": \"SMS\",\n \"useByDefault\": false,\n \"phoneNumber\": \"+380501253652\"\n }\n" + + " }\n}\n```" + + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) @GetMapping("/account/settings") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") public AccountTwoFaSettings getAccountTwoFaSettings() throws ThingsboardException { @@ -88,24 +83,29 @@ public class TwoFaConfigController extends BaseController { @ApiOperation(value = "Generate 2FA account config (generateTwoFaAccountConfig)", - notes = "Generate new 2FA account config for specified provider type. " + - "This method is only useful for TOTP 2FA, as there is nothing to generate for other provider types. " + + notes = "Generate new 2FA account config template for specified provider type. " + NEW_LINE + "For TOTP, this will return a corresponding account config template " + "with a generated OTP auth URL (with new random secret key for each API call) that can be then " + - "converted to a QR code to scan with an authenticator app. " + - "For other provider types, this method will return an empty config. " + NEW_LINE + - "Will throw an error (Bad Request) if the provider is not configured for usage. " + - ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER + NEW_LINE + - "Example of a generated account config for TOTP 2FA: " + NEW_LINE + + "converted to a QR code to scan with an authenticator app. Example:\n" + "```\n{\n" + " \"providerType\": \"TOTP\",\n" + - " \"authUrl\": \"otpauth://totp/ThingsBoard:tenant@thingsboard.org?issuer=ThingsBoard&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII\"\n" + + " \"useByDefault\": false,\n" + + " \"authUrl\": \"otpauth://totp/TB%202FA:tenant@thingsboard.org?issuer=TB+2FA&secret=PNJDNWJVAK4ZTUYT7RFGPQLXA7XGU7PX\"\n" + "}\n```" + NEW_LINE + - "For SMS provider type it will return something like: " + NEW_LINE + + "For EMAIL, the generated config will contain email from user's account:\n" + + "```\n{\n" + + " \"providerType\": \"EMAIL\",\n" + + " \"useByDefault\": false,\n" + + " \"email\": \"tenant@thingsboard.org\"\n" + + "}\n```" + NEW_LINE + + "For SMS 2FA this method will just return a config with empty/default values as there is nothing to generate/preset:\n" + "```\n{\n" + " \"providerType\": \"SMS\",\n" + + " \"useByDefault\": false,\n" + " \"phoneNumber\": null\n" + - "}\n```") + "}\n```" + NEW_LINE + + "Will throw an error (Bad Request) if the provider is not configured for usage. " + + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PostMapping("/account/config/generate") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") public TwoFaAccountConfig generateTwoFaAccountConfig(@ApiParam(value = "2FA provider type to generate new account config for", defaultValue = "TOTP", required = true) @@ -133,51 +133,84 @@ public class TwoFaConfigController extends BaseController { notes = "Submit 2FA account config to prepare for a future verification. " + "Basically, this method will send a verification code for a given account config, if this has " + "sense for a chosen 2FA provider. This code is needed to then verify and save the account config." + NEW_LINE + + "Example of EMAIL 2FA account config:\n" + + "```\n{\n" + + " \"providerType\": \"EMAIL\",\n" + + " \"useByDefault\": true,\n" + + " \"email\": \"separate-email-for-2fa@thingsboard.org\"\n" + + "}\n```" + NEW_LINE + + "Example of SMS 2FA account config:\n" + + "```\n{\n" + + " \"providerType\": \"SMS\",\n" + + " \"useByDefault\": false,\n" + + " \"phoneNumber\": \"+38012312321\"\n" + + "}\n```" + NEW_LINE + + "For TOTP this method does nothing." + NEW_LINE + "Will throw an error (Bad Request) if submitted account config is not valid, " + "or if the provider is not configured for usage. " + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PostMapping("/account/config/submit") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - public void submitTwoFaAccountConfig(@ApiParam(value = "2FA account config value. For TOTP 2FA config, authUrl value must not be blank and must match specific pattern. " + - "For SMS 2FA, phoneNumber property must not be blank and must be of E.164 phone number format.", required = true) - @Valid @RequestBody TwoFaAccountConfig accountConfig) throws Exception { + public void submitTwoFaAccountConfig(@Valid @RequestBody TwoFaAccountConfig accountConfig) throws Exception { SecurityUser user = getCurrentUser(); twoFactorAuthService.prepareVerificationCode(user, accountConfig, false); } @ApiOperation(value = "Verify and save 2FA account config (verifyAndSaveTwoFaAccountConfig)", - notes = "Checks the verification code for submitted config, and if it is correct, saves the provided account config. " + - "The config is stored in the user's additionalInfo. " + NEW_LINE + + notes = "Checks the verification code for submitted config, and if it is correct, saves the provided account config. " + NEW_LINE + + "Returns whole account's 2FA settings object.\n" + "Will throw an error (Bad Request) if the provider is not configured for usage. " + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PostMapping("/account/config") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - public AccountTwoFaSettings verifyAndSaveTwoFaAccountConfig(@ApiParam(value = "2FA account config to save. Validation rules are the same as in submitTwoFaAccountConfig API method", required = true) - @Valid @RequestBody TwoFaAccountConfig accountConfig, - @ApiParam(value = "6-digit code from an authenticator app in case of TOTP 2FA, or the one sent via an SMS message in case of SMS 2FA", required = true) + public AccountTwoFaSettings verifyAndSaveTwoFaAccountConfig(@Valid @RequestBody TwoFaAccountConfig accountConfig, + @ApiParam(value = "6-digit code from an authenticator app in case of TOTP 2FA, or the one sent via an SMS or email message in case of SMS or EMAIL 2FA", required = true) @RequestParam String verificationCode) throws Exception { SecurityUser user = getCurrentUser(); + if (twoFaConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig.getProviderType()).isPresent()) { + throw new IllegalArgumentException("2FA provider is already configured"); + } + boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, verificationCode, accountConfig, false); if (verificationSuccess) { return twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig); } else { - throw new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.INVALID_ARGUMENTS); + throw new IllegalArgumentException("Verification code is incorrect"); } } + @ApiOperation(value = "Update 2FA account config (updateTwoFaAccountConfig)", notes = + "Update config for a given provider type. \n" + + "Update request example:\n" + + "```\n{\n \"useByDefault\": true\n}\n```\n" + + "Returns whole account's 2FA settings object.\n" + + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PutMapping("/account/config") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") public AccountTwoFaSettings updateTwoFaAccountConfig(@RequestParam TwoFaProviderType providerType, @RequestBody TwoFaAccountConfigUpdateRequest updateRequest) throws ThingsboardException { SecurityUser user = getCurrentUser(); - TwoFaAccountConfig accountConfig = twoFaConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType) - .orElseThrow(() -> new IllegalArgumentException("No 2FA config for provider " + providerType)); + AccountTwoFaSettings settings = twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user.getId()) + .orElseThrow(() -> new IllegalArgumentException("No 2FA config found")); + Map configs = settings.getConfigs(); + + TwoFaAccountConfig accountConfig; + if ((accountConfig = configs.get(providerType)) == null) { + throw new IllegalArgumentException("Config for " + providerType + " 2FA provider not found"); + } + if (updateRequest.isUseByDefault()) { + configs.values().forEach(config -> config.setUseByDefault(false)); + } accountConfig.setUseByDefault(updateRequest.isUseByDefault()); - return twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig); + + return twoFaConfigManager.saveAccountTwoFaSettings(user.getTenantId(), user.getId(), settings); } - @ApiOperation(value = "Delete 2FA account config (deleteTwoFaAccountConfig)", - notes = "Delete user's 2FA config. " + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @ApiOperation(value = "Delete 2FA account config (deleteTwoFaAccountConfig)", notes = + "Delete 2FA config for a given 2FA provider type. \n" + + "Returns whole account's 2FA settings object.\n" + + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) @DeleteMapping("/account/config") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") public AccountTwoFaSettings deleteTwoFaAccountConfig(@RequestParam TwoFaProviderType providerType) throws ThingsboardException { @@ -186,6 +219,12 @@ public class TwoFaConfigController extends BaseController { } + @ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviders)", notes = + "Get the list of provider types available for user to use (the ones configured by tenant or sysadmin).\n" + + "Example of response:\n" + + "```\n[\n \"TOTP\",\n \"EMAIL\",\n \"SMS\"\n]\n```" + + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER + ) @GetMapping("/providers") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") public List getAvailableTwoFaProviders() throws ThingsboardException { @@ -196,8 +235,9 @@ public class TwoFaConfigController extends BaseController { } - @ApiOperation(value = "Get 2FA settings (getTwoFaSettings)", // FIXME [viacheslav] - notes = "Get settings for 2FA. If 2FA is not configured, then an empty response will be returned." + + @ApiOperation(value = "Get platform 2FA settings (getPlatformTwoFaSettings)", + notes = "Get platform settings for 2FA. The settings are described for savePlatformTwoFaSettings API method. " + + "If 2FA is not configured, then an empty response will be returned." + ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @GetMapping("/settings") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @@ -205,9 +245,49 @@ public class TwoFaConfigController extends BaseController { return twoFaConfigManager.getPlatformTwoFaSettings(getTenantId(), false).orElse(null); } - @ApiOperation(value = "Save 2FA settings (saveTwoFaSettings)", - notes = "Save settings for 2FA. If a user is sysadmin - the settings are saved as AdminSettings; " + - "if it is a tenant admin - as a tenant attribute." + + @ApiOperation(value = "Save platform 2FA settings (savePlatformTwoFaSettings)", + notes = "Save 2FA settings for platform. The settings have following properties:\n" + + "- `useSystemTwoFactorAuthSettings` - option for tenant admins to use 2FA settings configured by sysadmin. " + + "If this param is set to true, then the settings will not be validated for constraints (if it is a tenant admin; for sysadmin this param is ignored).\n" + + "- `providers` - the list of 2FA providers' configs. Users will only be allowed to use 2FA providers from this list. \n\n" + + "- `verificationCodeSendRateLimit` - rate limit configuration for verification code sending. " + + "The format is standard: 'amountOfRequests:periodInSeconds'. The value of '1:60' would limit verification " + + "code sending requests to one per minute.\n" + + "- `verificationCodeCheckRateLimit` - rate limit configuration for verification code checking.\n" + + "- `maxVerificationFailuresBeforeUserLockout` - maximum number of verification failures before a user gets disabled.\n" + + "- `totalAllowedTimeForVerification` - total amount of time in seconds allotted for verification. " + + "Basically, this property sets a lifetime for pre-verification token. If not set, default value of 30 minutes is used.\n" + NEW_LINE + + "TOTP 2FA provider config has following settings:\n" + + "- `issuerName` - issuer name that will be displayed in an authenticator app near a username. Must not be blank.\n\n" + + "For SMS 2FA provider:\n" + + "- `smsVerificationMessageTemplate` - verification message template. Available template variables " + + "are ${verificationCode} and ${userEmail}. It must not be blank and must contain verification code variable.\n" + + "- `verificationCodeLifetime` - verification code lifetime in seconds. Required to be positive.\n\n" + + "For EMAIL provider type:\n" + + "- `verificationCodeLifetime` - the same as for SMS." + NEW_LINE + + "Example of the settings:\n" + + "```\n{\n" + + " \"useSystemTwoFactorAuthSettings\": false,\n" + + " \"providers\": [\n" + + " {\n" + + " \"providerType\": \"TOTP\",\n" + + " \"issuerName\": \"TB\"\n" + + " },\n" + + " {\n" + + " \"providerType\": \"EMAIL\",\n" + + " \"verificationCodeLifetime\": 60\n" + + " },\n" + + " {\n" + + " \"providerType\": \"SMS\",\n" + + " \"verificationCodeLifetime\": 60,\n" + + " \"smsVerificationMessageTemplate\": \"Here is your verification code: ${verificationCode}\"\n" + + " }\n" + + " ],\n" + + " \"verificationCodeSendRateLimit\": \"1:60\",\n" + + " \"verificationCodeCheckRateLimit\": \"3:900\",\n" + + " \"maxVerificationFailuresBeforeUserLockout\": 10,\n" + + " \"totalAllowedTimeForVerification\": 600\n" + + "}\n```" + ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PostMapping("/settings") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java index a1fe5a7cfc..a43a73896e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -84,8 +84,8 @@ public class TwoFactorAuthController extends BaseController { "and Too Many Requests error if rate limits are exceeded.") @PostMapping("/verification/check") @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") - public JwtTokenPair checkTwoFaVerificationCode(@ApiParam(value = "6-digit verification code", required = true) - @RequestParam TwoFaProviderType providerType, + public JwtTokenPair checkTwoFaVerificationCode(@RequestParam TwoFaProviderType providerType, + @ApiParam(value = "6-digit verification code", required = true) @RequestParam String verificationCode, HttpServletRequest servletRequest) throws Exception { SecurityUser user = getCurrentUser(); boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, providerType, verificationCode, true); @@ -101,6 +101,13 @@ public class TwoFactorAuthController extends BaseController { } + @ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviders)", notes = + "Get the list of 2FA provider infos available for user to use. Example:\n" + + "```\n[\n" + + " {\n \"type\": \"EMAIL\",\n \"default\": true\n },\n" + + " {\n \"type\": \"TOTP\",\n \"default\": false\n },\n" + + " {\n \"type\": \"SMS\",\n \"default\": false\n }\n" + + "]\n```") @GetMapping("/providers") @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") public List getAvailableTwoFaProviders() throws ThingsboardException { diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index fa7cb2b7ff..c6ae0c70e4 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -40,7 +40,6 @@ import org.thingsboard.server.common.data.ApiUsageStateValue; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.settings.AdminSettingsService; @@ -311,6 +310,14 @@ public class DefaultMailService implements MailService { sendMail(mailSender, mailFrom, email, subject, message); } + @Override + public void sendTwoFaVerificationEmail(String email, String verificationCode) throws ThingsboardException { // TODO [viacheslav]: mail template + String subject = "ThingsBoard two-factor authentication"; + String message = "Your 2FA verification code: " + verificationCode; + + sendMail(mailSender, mailFrom, email, subject, message); + } + @Override public void sendApiFeatureStateEmail(ApiFeature apiFeature, ApiUsageStateValue stateValue, String email, ApiUsageStateMailMessage msg) throws ThingsboardException { String subject = messages.getMessage("api.usage.state", null, Locale.US); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java index 9f9b4e4b0a..75ca7b1e45 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java @@ -68,6 +68,20 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { }); } + @Override + public AccountTwoFaSettings saveAccountTwoFaSettings(TenantId tenantId, UserId userId, AccountTwoFaSettings settings) { + UserAuthSettings userAuthSettings = Optional.ofNullable(userAuthSettingsDao.findByUserId(userId)) + .orElseGet(() -> { + UserAuthSettings newUserAuthSettings = new UserAuthSettings(); + newUserAuthSettings.setUserId(userId); + return newUserAuthSettings; + }); + userAuthSettings.setTwoFaSettings(settings); + userAuthSettingsDao.save(tenantId, userAuthSettings); + return settings; + } + + @Override public Optional getTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType) { return getAccountTwoFaSettings(tenantId, userId) @@ -80,33 +94,21 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { getTwoFaProviderConfig(tenantId, accountConfig.getProviderType()) .orElseThrow(() -> new IllegalArgumentException("2FA provider is not configured")); - return createOrUpdateAccountTwoFaSettings(tenantId, userId, accountTwoFaSettings -> { - Map configs = accountTwoFaSettings.getConfigs(); - configs.put(accountConfig.getProviderType(), accountConfig); + AccountTwoFaSettings settings = getAccountTwoFaSettings(tenantId, userId).orElseGet(() -> { + AccountTwoFaSettings newSettings = new AccountTwoFaSettings(); + newSettings.setConfigs(new LinkedHashMap<>()); + return newSettings; }); + settings.getConfigs().put(accountConfig.getProviderType(), accountConfig); + return saveAccountTwoFaSettings(tenantId, userId, settings); } @Override public AccountTwoFaSettings deleteTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType) { - return createOrUpdateAccountTwoFaSettings(tenantId, userId, accountTwoFaSettings -> { - accountTwoFaSettings.getConfigs().keySet().removeIf(providerType::equals); - }); - } - - private AccountTwoFaSettings createOrUpdateAccountTwoFaSettings(TenantId tenantId, UserId userId, Consumer updater) { - UserAuthSettings userAuthSettings = Optional.ofNullable(userAuthSettingsDao.findByUserId(userId)) - .orElseGet(() -> { - UserAuthSettings newUserAuthSettings = new UserAuthSettings(); - newUserAuthSettings.setUserId(userId); - - AccountTwoFaSettings newAccountTwoFaSettings = new AccountTwoFaSettings(); - newAccountTwoFaSettings.setConfigs(new LinkedHashMap<>()); - newUserAuthSettings.setTwoFaSettings(newAccountTwoFaSettings); - return newUserAuthSettings; - }); - updater.accept(userAuthSettings.getTwoFaSettings()); - userAuthSettingsDao.save(tenantId, userAuthSettings); - return userAuthSettings.getTwoFaSettings(); + AccountTwoFaSettings settings = getAccountTwoFaSettings(tenantId, userId) + .orElseThrow(() -> new IllegalArgumentException("2FA not configured")); + settings.getConfigs().remove(providerType); + return saveAccountTwoFaSettings(tenantId, userId, settings); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java index b0073338b7..f1a4590572 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java @@ -26,9 +26,10 @@ import java.util.Optional; public interface TwoFaConfigManager { - Optional getAccountTwoFaSettings(TenantId tenantId, UserId userId); + AccountTwoFaSettings saveAccountTwoFaSettings(TenantId tenantId, UserId userId, AccountTwoFaSettings settings); + Optional getTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType); AccountTwoFaSettings saveTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaAccountConfig accountConfig); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFaProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFaProvider.java index 4d4db92af1..e5c278942b 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFaProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFaProvider.java @@ -26,9 +26,9 @@ public interface TwoFaProvider { + + private final MailService mailService; + + protected EmailTwoFaProvider(CacheManager cacheManager, MailService mailService) { + super(cacheManager); + this.mailService = mailService; + } + + @Override + public EmailTwoFaAccountConfig generateNewAccountConfig(User user, EmailTwoFaProviderConfig providerConfig) { + EmailTwoFaAccountConfig config = new EmailTwoFaAccountConfig(); + config.setEmail(user.getEmail()); + return config; + } + + @Override + protected void sendVerificationCode(SecurityUser user, String verificationCode, EmailTwoFaProviderConfig providerConfig, EmailTwoFaAccountConfig accountConfig) throws ThingsboardException { + mailService.sendTwoFaVerificationEmail(accountConfig.getEmail(), verificationCode); + } + + @Override + public TwoFaProviderType getType() { + return TwoFaProviderType.EMAIL; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFaProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFaProvider.java index 30f6fe4f50..f270316d2d 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFaProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFaProvider.java @@ -38,27 +38,27 @@ public abstract class OtpBasedTwoFaProvider TimeUnit.SECONDS.toMillis(providerConfig.getVerificationCodeLifetime())) { - verificationCodesCache.evict(securityUser.getId()); + verificationCodesCache.evict(user.getId()); return false; } if (verificationCode.equals(correctVerificationCode.getValue()) && accountConfig.equals(correctVerificationCode.getAccountConfig())) { - verificationCodesCache.evict(securityUser.getId()); + verificationCodesCache.evict(user.getId()); return true; } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFaProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFaProvider.java index 9b251e5b4a..289ddb7c9d 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFaProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFaProvider.java @@ -45,7 +45,7 @@ public class TotpTwoFaProvider implements TwoFaProvider providers; - @ApiModelProperty(value = "Rate limit configuration for verification code sending. The format is standard: 'amountOfRequests:periodInSeconds'. " + - "The value of '1:60' would limit verification code sending requests to one per minute.", example = "1:60", required = false) @Pattern(regexp = "[1-9]\\d*:[1-9]\\d*", message = "verification code send rate limit configuration is invalid") private String verificationCodeSendRateLimit; - @ApiModelProperty(value = "Rate limit configuration for verification code checking.", example = "3:900", required = false) @Pattern(regexp = "[1-9]\\d*:[1-9]\\d*", message = "verification code check rate limit configuration is invalid") private String verificationCodeCheckRateLimit; - @ApiModelProperty(value = "Maximum number of verification failures before a user gets disabled.", example = "10", required = false) @Min(value = 0, message = "maximum number of verification failure before user lockout must be positive") private int maxVerificationFailuresBeforeUserLockout; - @ApiModelProperty(value = "Total amount of time in seconds allotted for verification. " + - "Basically, this property sets a lifetime for pre-verification token. If not set, default value of 30 minutes is used.", example = "3600", required = false) @Min(value = 1, message = "total amount of time allotted for verification must be greater than 0") private Integer totalAllowedTimeForVerification; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/EmailTwoFaAccountConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/EmailTwoFaAccountConfig.java new file mode 100644 index 0000000000..a2170d7a54 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/EmailTwoFaAccountConfig.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.common.data.security.model.mfa.account; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; + +@Data +@EqualsAndHashCode(callSuper = true) +public class EmailTwoFaAccountConfig extends OtpBasedTwoFaAccountConfig { + + @NotBlank + @Email + private String email; + + @Override + public TwoFaProviderType getProviderType() { + return TwoFaProviderType.EMAIL; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/SmsTwoFaAccountConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/SmsTwoFaAccountConfig.java index 67d9d499ae..84a7b6924e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/SmsTwoFaAccountConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/SmsTwoFaAccountConfig.java @@ -15,8 +15,6 @@ */ package org.thingsboard.server.common.data.security.model.mfa.account; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; @@ -24,12 +22,10 @@ import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProvi import javax.validation.constraints.NotBlank; import javax.validation.constraints.Pattern; -@ApiModel @EqualsAndHashCode(callSuper = true) @Data public class SmsTwoFaAccountConfig extends OtpBasedTwoFaAccountConfig { - @ApiModelProperty(value = "Phone number to use for 2FA. Must no be blank and must be of E.164 number format.", required = true) @NotBlank(message = "phone number cannot be blank") @Pattern(regexp = "^\\+[1-9]\\d{1,14}$", message = "phone number is not of E.164 format") private String phoneNumber; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TotpTwoFaAccountConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TotpTwoFaAccountConfig.java index ecafde86ce..5cdec02e90 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TotpTwoFaAccountConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TotpTwoFaAccountConfig.java @@ -15,8 +15,6 @@ */ package org.thingsboard.server.common.data.security.model.mfa.account; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; @@ -24,13 +22,10 @@ import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProvi import javax.validation.constraints.NotBlank; import javax.validation.constraints.Pattern; -@ApiModel // FIXME [viacheslav] @Data @EqualsAndHashCode(callSuper = true) public class TotpTwoFaAccountConfig extends TwoFaAccountConfig { - @ApiModelProperty(value = "OTP auth URL used to generate a QR code to scan with an authenticator app. Must not be blank and must follow specific pattern.", - example = "otpauth://totp/ThingsBoard:tenant@thingsboard.org?issuer=ThingsBoard&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII", required = true) @NotBlank(message = "OTP auth URL cannot be blank") @Pattern(regexp = "otpauth://totp/(\\S+?):(\\S+?)\\?issuer=(\\S+?)&secret=(\\w+?)", message = "OTP auth url is invalid") private String authUrl; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFaAccountConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFaAccountConfig.java index 5d366205cd..6bf2ee8fd3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFaAccountConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFaAccountConfig.java @@ -29,7 +29,8 @@ import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProvi property = "providerType") @JsonSubTypes({ @Type(name = "TOTP", value = TotpTwoFaAccountConfig.class), - @Type(name = "SMS", value = SmsTwoFaAccountConfig.class) + @Type(name = "SMS", value = SmsTwoFaAccountConfig.class), + @Type(name = "EMAIL", value = EmailTwoFaAccountConfig.class) }) @Data public abstract class TwoFaAccountConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/EmailTwoFaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/EmailTwoFaProviderConfig.java new file mode 100644 index 0000000000..787f5b561d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/EmailTwoFaProviderConfig.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.common.data.security.model.mfa.provider; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class EmailTwoFaProviderConfig extends OtpBasedTwoFaProviderConfig { + + @Override + public TwoFaProviderType getProviderType() { + return TwoFaProviderType.EMAIL; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/OtpBasedTwoFaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/OtpBasedTwoFaProviderConfig.java index 23d64c79aa..f1595e733f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/OtpBasedTwoFaProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/OtpBasedTwoFaProviderConfig.java @@ -15,15 +15,14 @@ */ package org.thingsboard.server.common.data.security.model.mfa.provider; -import io.swagger.annotations.ApiModelProperty; import lombok.Data; import javax.validation.constraints.Min; @Data public abstract class OtpBasedTwoFaProviderConfig implements TwoFaProviderConfig { - @ApiModelProperty(value = "Verification code lifetime in seconds. Verification codes with a lifetime bigger than this param " + - "will be considered incorrect", example = "60", required = true) + @Min(value = 1, message = "verification code lifetime is required") private int verificationCodeLifetime; + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/SmsTwoFaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/SmsTwoFaProviderConfig.java index 81efb7058e..e0ed0587d9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/SmsTwoFaProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/SmsTwoFaProviderConfig.java @@ -15,22 +15,16 @@ */ package org.thingsboard.server.common.data.security.model.mfa.provider; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Pattern; -@ApiModel(parent = OtpBasedTwoFaProviderConfig.class) @EqualsAndHashCode(callSuper = true) @Data public class SmsTwoFaProviderConfig extends OtpBasedTwoFaProviderConfig { - @ApiModelProperty(value = "SMS verification message template. Available template variables are ${verificationCode} and ${userEmail}. " + - "It must not be blank and must contain verification code variable.", - example = "Here is your verification code: ${verificationCode}", required = true) @NotBlank(message = "verification message template is required") @Pattern(regexp = ".*\\$\\{verificationCode}.*", message = "template must contain verification code") private String smsVerificationMessageTemplate; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TotpTwoFaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TotpTwoFaProviderConfig.java index b631d367e5..32f443f785 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TotpTwoFaProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TotpTwoFaProviderConfig.java @@ -15,18 +15,13 @@ */ package org.thingsboard.server.common.data.security.model.mfa.provider; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; import lombok.Data; import javax.validation.constraints.NotBlank; -@ApiModel @Data public class TotpTwoFaProviderConfig implements TwoFaProviderConfig { - @ApiModelProperty(value = "Issuer name that will be displayed in an authenticator app near a username. " + - "Must not be blank.", example = "ThingsBoard", required = true) @NotBlank(message = "issuer name must not be blank") private String issuerName; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderConfig.java index 69d9989af4..65d9242900 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderConfig.java @@ -27,7 +27,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; property = "providerType") @JsonSubTypes({ @Type(name = "TOTP", value = TotpTwoFaProviderConfig.class), - @Type(name = "SMS", value = SmsTwoFaProviderConfig.class) + @Type(name = "SMS", value = SmsTwoFaProviderConfig.class), + @Type(name = "EMAIL", value = EmailTwoFaProviderConfig.class) }) public interface TwoFaProviderConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderType.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderType.java index 7e6f25195e..dd36f24394 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderType.java @@ -17,5 +17,6 @@ package org.thingsboard.server.common.data.security.model.mfa.provider; public enum TwoFaProviderType { TOTP, - SMS + SMS, + EMAIL } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/MailService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/MailService.java index 86da82187d..10c37da564 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/MailService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/MailService.java @@ -24,8 +24,6 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; -import java.util.Map; - public interface MailService { void updateMailConfiguration(); @@ -46,6 +44,8 @@ public interface MailService { void sendAccountLockoutEmail(String lockoutEmail, String email, Integer maxFailedLoginAttempts) throws ThingsboardException; + void sendTwoFaVerificationEmail(String email, String verificationCode) throws ThingsboardException; + void send(TenantId tenantId, CustomerId customerId, TbEmail tbEmail) throws ThingsboardException; void send(TenantId tenantId, CustomerId customerId, TbEmail tbEmail, JavaMailSender javaMailSender) throws ThingsboardException; From 7eaa70e4723feae7364ac3612a9bc8f7ce138a3a Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Thu, 5 May 2022 19:16:45 +0300 Subject: [PATCH 095/276] Transactional Redis Cache interface --- common/cache/pom.xml | 4 + .../CaffeineTbTransactionalCacheManager.java | 59 ------------ .../RedisTbTransactionalCacheManager.java | 42 --------- .../server/cache/TbCacheTransaction.java | 19 +++- .../server/cache/TbCacheValueWrapper.java | 22 +++++ .../server/cache/TbTransactionalCache.java | 31 +++++-- .../cache/ota/RedisOtaPackageDataCache.java | 1 - .../attributes/AttributeCaffeineCache.java | 33 +++++++ .../dao/attributes/AttributeRedisCache.java | 33 +++++++ .../dao/attributes/BaseAttributesService.java | 2 +- .../attributes/CachedAttributesService.java | 30 +++--- .../CaffeineCacheTransactionStorage.java | 39 ++++++++ .../cache/CaffeineTbCacheTransaction.java | 32 +++++-- .../cache/CaffeineTbTransactionalCache.java | 91 ++++++++++++++----- .../dao/cache/RedisTbTransactionalCache.java | 61 +++++++++++++ .../dao/cache/SimpleTbCacheValueWrapper.java | 37 ++++++++ .../dao/relation/BaseRelationService.java | 2 +- .../server/dao/sql/TbSqlBlockingQueue.java | 2 +- .../dao/sql/attributes/JpaAttributeDao.java | 2 +- .../sql/attributes/AttributeServiceTest.java | 15 +++ .../attributes/RedisAttributeServiceTest.java | 15 +++ 21 files changed, 412 insertions(+), 160 deletions(-) delete mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCacheManager.java delete mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCacheManager.java create mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/TbCacheValueWrapper.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCaffeineCache.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineCacheTransactionStorage.java rename {common/cache/src/main/java/org/thingsboard/server => dao/src/main/java/org/thingsboard/server/dao}/cache/CaffeineTbCacheTransaction.java (55%) rename common/cache/src/main/java/org/thingsboard/server/cache/CaffeineCacheTransactionStorage.java => dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbTransactionalCache.java (52%) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/cache/RedisTbTransactionalCache.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/cache/SimpleTbCacheValueWrapper.java diff --git a/common/cache/pom.xml b/common/cache/pom.xml index 47834cb6bb..b52984e0fc 100644 --- a/common/cache/pom.xml +++ b/common/cache/pom.xml @@ -56,6 +56,10 @@ com.github.ben-manes.caffeine caffeine + + javax.annotation + javax.annotation-api + org.apache.commons commons-lang3 diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCacheManager.java b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCacheManager.java deleted file mode 100644 index e9dba40cf1..0000000000 --- a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCacheManager.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.thingsboard.server.cache; - -import lombok.RequiredArgsConstructor; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.cache.Cache; -import org.springframework.cache.CacheManager; -import org.springframework.stereotype.Service; - -import java.io.Serializable; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) -@Service -@RequiredArgsConstructor -public class CaffeineTbTransactionalCacheManager implements TbTransactionalCache { - - private final CacheManager cacheManager; - private final ConcurrentMap caches = new ConcurrentHashMap<>(); - - @Override - public Cache.ValueWrapper get(String cacheName, K key) { - return cacheManager.getCache(cacheName).get(key); - } - - @Override - public void putIfAbsent(String cacheName, K key, V value) { - getCache(cacheName).putIfAbsent(key, value); - } - - @Override - public void evict(String cacheName, K key) { - getCache(cacheName).evict(key); - } - - @Override - public TbCacheTransaction newTransactionForKey(String cacheName, K key) { - return getCache(cacheName).newTransaction(Collections.singletonList(key)); - } - - @Override - public TbCacheTransaction newTransactionForKeys(String cacheName, List keys) { - return getCache(cacheName).newTransaction(keys); - } - - private CaffeineCacheTransactionStorage getCache(String cacheName) { - return caches.computeIfAbsent(cacheName, cn -> new CaffeineCacheTransactionStorage(cacheName, this)); - } - - void doPutIfAbsent(String cacheName, Object key, Object value) { - cacheManager.getCache(cacheName).putIfAbsent(key, value); - } - - void doEvict(String cacheName, K key) { - cacheManager.getCache(cacheName).evict(key); - } -} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCacheManager.java b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCacheManager.java deleted file mode 100644 index a4df25aeac..0000000000 --- a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCacheManager.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.thingsboard.server.cache; - -import lombok.RequiredArgsConstructor; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.cache.Cache; -import org.springframework.cache.CacheManager; -import org.springframework.stereotype.Service; - -import java.io.Serializable; -import java.util.List; - -@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") -@Service -@RequiredArgsConstructor -public class RedisTbTransactionalCacheManager implements TbTransactionalCache { - - private final CacheManager cacheManager; - - @Override - public Cache.ValueWrapper get(String cacheName, K key) { - return cacheManager.getCache(cacheName).get(key); - } - - @Override - public void putIfAbsent(String cacheName, K key, V value) { - } - - @Override - public void evict(String cacheName, K key) { - } - - @Override - public TbCacheTransaction newTransactionForKey(String cacheName, K key) { - return null; - } - - @Override - public TbCacheTransaction newTransactionForKeys(String cacheName, List keys) { - return null; - } - -} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbCacheTransaction.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbCacheTransaction.java index 22bbec4489..47779c251d 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TbCacheTransaction.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbCacheTransaction.java @@ -1,12 +1,27 @@ +/** + * Copyright © 2016-2022 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. + */ package org.thingsboard.server.cache; import com.google.common.util.concurrent.ListenableFuture; import java.util.concurrent.Executor; -public interface TbCacheTransaction { +public interface TbCacheTransaction { - void putIfAbsent(K key, V value); + void putIfAbsent(K key, V value); boolean commit(); diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbCacheValueWrapper.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbCacheValueWrapper.java new file mode 100644 index 0000000000..66d7a64f15 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbCacheValueWrapper.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.cache; + +public interface TbCacheValueWrapper { + + T get(); + +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java index ad370f3d18..208e5856ab 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java @@ -1,20 +1,35 @@ +/** + * Copyright © 2016-2022 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. + */ package org.thingsboard.server.cache; -import org.springframework.cache.Cache; - import java.io.Serializable; import java.util.List; -public interface TbTransactionalCache { +public interface TbTransactionalCache { + + String getCacheName(); - Cache.ValueWrapper get(String cacheName, K key); + TbCacheValueWrapper get(K key); - void putIfAbsent(String cacheName, K key, V value); + void putIfAbsent(K key, V value); - void evict(String cacheName, K key); + void evict(K key); - TbCacheTransaction newTransactionForKey(String cacheName, K key); + TbCacheTransaction newTransactionForKey(K key); - TbCacheTransaction newTransactionForKeys(String cacheName, List keys); + TbCacheTransaction newTransactionForKeys(List keys); } diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/ota/RedisOtaPackageDataCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/ota/RedisOtaPackageDataCache.java index 3117e1fa43..f6ec28d170 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/ota/RedisOtaPackageDataCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/ota/RedisOtaPackageDataCache.java @@ -21,7 +21,6 @@ import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.stereotype.Service; -import static org.thingsboard.server.common.data.CacheConstants.OTA_PACKAGE_CACHE; import static org.thingsboard.server.common.data.CacheConstants.OTA_PACKAGE_DATA_CACHE; @Service diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCaffeineCache.java new file mode 100644 index 0000000000..01dbde9998 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCaffeineCache.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.attributes; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("AttributeCache") +public class AttributeCaffeineCache extends CaffeineTbTransactionalCache { + + public AttributeCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.ATTRIBUTES_CACHE); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java new file mode 100644 index 0000000000..940c96eb00 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.attributes; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("AttributeCache") +public class AttributeRedisCache extends RedisTbTransactionalCache { + + public AttributeRedisCache(CacheManager cacheManager, RedisConnectionFactory connectionFactory) { + super(cacheManager, CacheConstants.ATTRIBUTES_CACHE, connectionFactory); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java index 508cf4c16a..a147159158 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index d9e172c113..919ff1f4cd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -25,6 +25,7 @@ import org.springframework.cache.Cache; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; +import org.thingsboard.server.cache.TbCacheValueWrapper; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -34,7 +35,6 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.stats.DefaultCounter; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.cache.CacheExecutorService; -import org.thingsboard.server.cache.TbCacheTransaction; import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.dao.service.Validator; @@ -65,7 +65,7 @@ public class CachedAttributesService implements AttributesService { private final CacheExecutorService cacheExecutorService; private final DefaultCounter hitCounter; private final DefaultCounter missCounter; - private final TbTransactionalCache cache; + private final TbTransactionalCache cache; private Executor cacheExecutor; @Value("${cache.type}") @@ -74,7 +74,7 @@ public class CachedAttributesService implements AttributesService { public CachedAttributesService(AttributesDao attributesDao, StatsFactory statsFactory, CacheExecutorService cacheExecutorService, - TbTransactionalCache cache) { + TbTransactionalCache cache) { this.attributesDao = attributesDao; this.cacheExecutorService = cacheExecutorService; this.cache = cache; @@ -109,14 +109,14 @@ public class CachedAttributesService implements AttributesService { Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey); AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, attributeKey); - Cache.ValueWrapper cachedAttributeValue = cache.get(CacheConstants.ATTRIBUTES_CACHE, attributeCacheKey); + TbCacheValueWrapper cachedAttributeValue = cache.get(attributeCacheKey); if (cachedAttributeValue != null) { hitCounter.increment(); - AttributeKvEntry cachedAttributeKvEntry = (AttributeKvEntry) cachedAttributeValue.get(); + AttributeKvEntry cachedAttributeKvEntry = cachedAttributeValue.get(); return Futures.immediateFuture(Optional.ofNullable(cachedAttributeKvEntry)); } else { missCounter.increment(); - TbCacheTransaction cacheTransaction = cache.newTransactionForKey(CacheConstants.ATTRIBUTES_CACHE, attributeCacheKey); + var cacheTransaction = cache.newTransactionForKey(attributeCacheKey); ListenableFuture> result = attributesDao.find(tenantId, entityId, scope, attributeKey); cacheTransaction.rollBackOnFailure(result, cacheExecutor); return Futures.transform(result, foundAttrKvEntry -> { @@ -132,10 +132,10 @@ public class CachedAttributesService implements AttributesService { validate(entityId, scope); attributeKeys.forEach(attributeKey -> Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey)); - Map wrappedCachedAttributes = findCachedAttributes(entityId, scope, attributeKeys); + Map> wrappedCachedAttributes = findCachedAttributes(entityId, scope, attributeKeys); List cachedAttributes = wrappedCachedAttributes.values().stream() - .map(wrappedCachedAttribute -> (AttributeKvEntry) wrappedCachedAttribute.get()) + .map(TbCacheValueWrapper::get) .filter(Objects::nonNull) .collect(Collectors.toList()); if (wrappedCachedAttributes.size() == attributeKeys.size()) { @@ -147,7 +147,7 @@ public class CachedAttributesService implements AttributesService { List notFoundKeys = notFoundAttributeKeys.stream().map(k -> new AttributeCacheKey(scope, entityId, k)).collect(Collectors.toList()); - TbCacheTransaction cacheTransaction = cache.newTransactionForKeys(CacheConstants.ATTRIBUTES_CACHE, notFoundKeys); + var cacheTransaction = cache.newTransactionForKeys(notFoundKeys); ListenableFuture> result = attributesDao.find(tenantId, entityId, scope, notFoundAttributeKeys); return Futures.transform(result, foundInDbAttributes -> { for (AttributeKvEntry foundInDbAttribute : foundInDbAttributes) { @@ -166,10 +166,10 @@ public class CachedAttributesService implements AttributesService { } - private Map findCachedAttributes(EntityId entityId, String scope, Collection attributeKeys) { - Map cachedAttributes = new HashMap<>(); + private Map> findCachedAttributes(EntityId entityId, String scope, Collection attributeKeys) { + Map> cachedAttributes = new HashMap<>(); for (String attributeKey : attributeKeys) { - Cache.ValueWrapper cachedAttributeValue = cache.get(CacheConstants.ATTRIBUTES_CACHE, new AttributeCacheKey(scope, entityId, attributeKey)); + var cachedAttributeValue = cache.get(new AttributeCacheKey(scope, entityId, attributeKey)); if (cachedAttributeValue != null) { hitCounter.increment(); cachedAttributes.put(attributeKey, cachedAttributeValue); @@ -205,7 +205,7 @@ public class CachedAttributesService implements AttributesService { for (var attribute : attributes) { ListenableFuture future = attributesDao.save(tenantId, entityId, scope, attribute); futures.add(Futures.transform(future, key -> { - cache.evict(CacheConstants.ATTRIBUTES_CACHE, new AttributeCacheKey(scope, entityId, key)); + cache.evict(new AttributeCacheKey(scope, entityId, key)); return key; }, cacheExecutor)); } @@ -218,7 +218,7 @@ public class CachedAttributesService implements AttributesService { validate(entityId, scope); List> futures = attributesDao.removeAll(tenantId, entityId, scope, attributeKeys); return Futures.allAsList(futures.stream().map(future -> Futures.transform(future, key -> { - cache.evict(CacheConstants.ATTRIBUTES_CACHE, new AttributeCacheKey(scope, entityId, key)); + cache.evict(new AttributeCacheKey(scope, entityId, key)); return key; }, cacheExecutor)).collect(Collectors.toList())); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineCacheTransactionStorage.java b/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineCacheTransactionStorage.java new file mode 100644 index 0000000000..af3c40b333 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineCacheTransactionStorage.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.cache; + +import lombok.RequiredArgsConstructor; +import org.thingsboard.server.cache.TbCacheTransaction; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@RequiredArgsConstructor +class CaffeineCacheTransactionStorage { + + + + + + +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbCacheTransaction.java b/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbCacheTransaction.java similarity index 55% rename from common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbCacheTransaction.java rename to dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbCacheTransaction.java index 202d9c4007..dc3ad0cf08 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbCacheTransaction.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbCacheTransaction.java @@ -1,4 +1,19 @@ -package org.thingsboard.server.cache; +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.cache; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; @@ -8,7 +23,9 @@ import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; +import org.thingsboard.server.cache.TbCacheTransaction; +import java.io.Serializable; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -17,19 +34,20 @@ import java.util.concurrent.Executor; @Slf4j @RequiredArgsConstructor -public class CaffeineTbCacheTransaction implements TbCacheTransaction { +public class CaffeineTbCacheTransaction implements TbCacheTransaction { @Getter private final UUID id = UUID.randomUUID(); - private final CaffeineCacheTransactionStorage cache; + private final CaffeineTbTransactionalCache cache; @Getter - private final List keys; - @Getter @Setter + private final List keys; + @Getter + @Setter private boolean failed; private final Map pendingPuts = new LinkedHashMap<>(); @Override - public void putIfAbsent(K key, V value) { + public void putIfAbsent(K key, V value) { pendingPuts.put(key, value); } @@ -45,7 +63,7 @@ public class CaffeineTbCacheTransaction implements TbCacheTransaction { @Override public void rollBackOnFailure(ListenableFuture future, Executor executor) { - Futures.addCallback(future, new FutureCallback() { + Futures.addCallback(future, new FutureCallback<>() { @Override public void onSuccess(@Nullable T result) { } diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineCacheTransactionStorage.java b/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbTransactionalCache.java similarity index 52% rename from common/cache/src/main/java/org/thingsboard/server/cache/CaffeineCacheTransactionStorage.java rename to dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbTransactionalCache.java index 8a9a9850fc..563c41ef8a 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineCacheTransactionStorage.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbTransactionalCache.java @@ -1,8 +1,29 @@ -package org.thingsboard.server.cache; +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.cache; +import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.springframework.cache.CacheManager; +import org.thingsboard.server.cache.TbCacheTransaction; +import org.thingsboard.server.cache.TbCacheValueWrapper; +import org.thingsboard.server.cache.TbTransactionalCache; import java.io.Serializable; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -13,45 +34,71 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @RequiredArgsConstructor -class CaffeineCacheTransactionStorage { +public abstract class CaffeineTbTransactionalCache implements TbTransactionalCache { + private final CacheManager cacheManager; + @Getter private final String cacheName; - private final CaffeineTbTransactionalCacheManager cache; + private final Lock lock = new ReentrantLock(); - private final Map> objectTransactions = new HashMap<>(); - private final Map transactions = new HashMap<>(); + private final Map> objectTransactions = new HashMap<>(); + private final Map> transactions = new HashMap<>(); + @Override + public TbCacheValueWrapper get(K key) { + return SimpleTbCacheValueWrapper.wrap(cacheManager.getCache(cacheName).get(key)); + } - TbCacheTransaction newTransaction(List keys) { + @Override + public void putIfAbsent(K key, V value) { lock.lock(); try { - var transaction = new CaffeineTbCacheTransaction(this, keys); - var transactionId = transaction.getId(); - for (K key : keys) { - objectTransactions.computeIfAbsent(key, k -> new HashSet<>()).add(transactionId); - } - transactions.put(transactionId, transaction); - return transaction; + failAllTransactionsByKey(key); + doPutIfAbsent(key, value); } finally { lock.unlock(); } } - void putIfAbsent(K key, V value) { + @Override + public void evict(K key) { lock.lock(); try { failAllTransactionsByKey(key); - cache.doPutIfAbsent(cacheName, key, value); + doEvict(key); } finally { lock.unlock(); } } - public void evict(K key) { + @Override + public TbCacheTransaction newTransactionForKey(K key) { + return newTransaction(Collections.singletonList(key)); + } + + @Override + public TbCacheTransaction newTransactionForKeys(List keys) { + return newTransaction(keys); + } + + void doPutIfAbsent(Object key, Object value) { + cacheManager.getCache(cacheName).putIfAbsent(key, value); + } + + void doEvict(K key) { + cacheManager.getCache(cacheName).evict(key); + } + + TbCacheTransaction newTransaction(List keys) { lock.lock(); try { - failAllTransactionsByKey(key); - cache.doEvict(cacheName, key); + var transaction = new CaffeineTbCacheTransaction<>(this, keys); + var transactionId = transaction.getId(); + for (K key : keys) { + objectTransactions.computeIfAbsent(key, k -> new HashSet<>()).add(transactionId); + } + transactions.put(transactionId, transaction); + return transaction; } finally { lock.unlock(); } @@ -63,7 +110,7 @@ class CaffeineCacheTransactionStorage { var tr = transactions.get(trId); var success = !tr.isFailed(); if (success) { - for (Object key : tr.getKeys()) { + for (K key : tr.getKeys()) { Set otherTransactions = objectTransactions.get(key); if (otherTransactions != null) { for (UUID otherTrId : otherTransactions) { @@ -73,7 +120,7 @@ class CaffeineCacheTransactionStorage { } } } - pendingPuts.forEach((k, v) -> cache.doPutIfAbsent(cacheName, k, v)); + pendingPuts.forEach(this::doPutIfAbsent); } removeTransaction(trId); return success; @@ -92,7 +139,7 @@ class CaffeineCacheTransactionStorage { } private void removeTransaction(UUID id) { - CaffeineTbCacheTransaction transaction = transactions.remove(id); + CaffeineTbCacheTransaction transaction = transactions.remove(id); if (transaction != null) { for (var key : transaction.getKeys()) { Set transactions = objectTransactions.get(key); @@ -106,7 +153,7 @@ class CaffeineCacheTransactionStorage { } } - private void failAllTransactionsByKey(K key) { + private void failAllTransactionsByKey(K key) { Set transactionsIds = objectTransactions.get(key); if (transactionsIds != null) { for (UUID otherTrId : transactionsIds) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/RedisTbTransactionalCache.java b/dao/src/main/java/org/thingsboard/server/dao/cache/RedisTbTransactionalCache.java new file mode 100644 index 0000000000..dcb16b42e8 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/cache/RedisTbTransactionalCache.java @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.cache; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.cache.CacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.thingsboard.server.cache.TbCacheTransaction; +import org.thingsboard.server.cache.TbCacheValueWrapper; +import org.thingsboard.server.cache.TbTransactionalCache; + +import java.io.Serializable; +import java.util.List; + +@RequiredArgsConstructor +public abstract class RedisTbTransactionalCache implements TbTransactionalCache { + + private final CacheManager cacheManager; + @Getter + private final String cacheName; + private final RedisConnectionFactory connectionFactory; + + @Override + public TbCacheValueWrapper get(K key) { + return null; + } + + @Override + public void putIfAbsent(K key, V value) { + + } + + @Override + public void evict(K key) { + + } + + @Override + public TbCacheTransaction newTransactionForKey(K key) { + return null; + } + + @Override + public TbCacheTransaction newTransactionForKeys(List keys) { + return null; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/SimpleTbCacheValueWrapper.java b/dao/src/main/java/org/thingsboard/server/dao/cache/SimpleTbCacheValueWrapper.java new file mode 100644 index 0000000000..8e08582477 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/cache/SimpleTbCacheValueWrapper.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.cache; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import org.springframework.cache.Cache; +import org.thingsboard.server.cache.TbCacheValueWrapper; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class SimpleTbCacheValueWrapper implements TbCacheValueWrapper { + + private final T value; + + @Override + public T get() { + return value; + } + + @SuppressWarnings("unchecked") + public static SimpleTbCacheValueWrapper wrap(Cache.ValueWrapper source) { + return source == null ? null : new SimpleTbCacheValueWrapper<>((T) source.get()); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index e2e1ba7d2b..36ab0d9f85 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java index c1ee7f3fd3..115def0492 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java index 185dae1c2f..0c5fbd5d3a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/AttributeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/AttributeServiceTest.java index 02b9ef54d6..c67122994c 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/AttributeServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/AttributeServiceTest.java @@ -1,3 +1,18 @@ +/** + * Copyright © 2016-2022 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. + */ package org.thingsboard.server.dao.sql.attributes; import com.google.common.util.concurrent.Futures; diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/RedisAttributeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/RedisAttributeServiceTest.java index 09a0ccf9d4..ce0afb8f60 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/RedisAttributeServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/RedisAttributeServiceTest.java @@ -1,3 +1,18 @@ +/** + * Copyright © 2016-2022 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. + */ package org.thingsboard.server.dao.sql.attributes; import lombok.extern.slf4j.Slf4j; From 285f35b31b3e6ba8325f7619528a70114f4cbe2b Mon Sep 17 00:00:00 2001 From: fe-dev Date: Fri, 6 May 2022 12:28:11 +0300 Subject: [PATCH 096/276] UI: Fix trackby queues and add padding --- .../queue/tenant-profile-queues.component.ts | 8 +-- .../profile/tenant-profile.component.html | 60 ++++++++++--------- .../profile/tenant-profile.component.scss | 3 + 3 files changed, 36 insertions(+), 35 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts index a403f8e82e..7f4de4c68a 100644 --- a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts @@ -130,12 +130,8 @@ export class TenantProfileQueuesComponent implements ControlValueAccessor, Valid }); } - public trackByQueue(index: number, queueControl: AbstractControl): string { - if (queueControl) { - return queueControl.value.id; - } else { - return null; - } + public trackByQueue(index: number, queueControl: AbstractControl) { + return queueControl; } public removeQueue(index: number) { diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html index 789ba8590a..fb20387608 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html @@ -68,35 +68,37 @@
{{'tenant.isolated-tb-rule-engine-details' | translate}}
- - - - - {{'tenant-profile.queues-with-count' | translate: - {count: entityForm.get('profileData').get('queueConfiguration').value ? - entityForm.get('profileData').get('queueConfiguration').value.length : 0} }} - - - - - - - - - -
tenant-profile.profile-configuration
-
-
- - - - -
-
+
+ + + + + {{'tenant-profile.queues-with-count' | translate: + {count: entityForm.get('profileData').get('queueConfiguration').value ? + entityForm.get('profileData').get('queueConfiguration').value.length : 0} }} + + + + + + + + + +
tenant-profile.profile-configuration
+
+
+ + + + +
+
+
tenant-profile.description diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.scss b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.scss index e6e5d3b9cf..c76509e9e3 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.scss +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.scss @@ -35,4 +35,7 @@ width: fit-content; } } + .expansion-panel-block { + padding-bottom: 16px; + } } From 05301efe2c6c8631aced6606b818c6de95c4124a Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Fri, 6 May 2022 12:41:10 +0300 Subject: [PATCH 097/276] Update 2FA tests --- .../controller/TwoFactorAuthConfigTest.java | 55 +- .../server/controller/TwoFactorAuthTest.java | 693 ++++++++++-------- .../common/msg/tools/RateLimitsTest.java | 2 +- 3 files changed, 408 insertions(+), 342 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java index 954d313480..3438ed19f8 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java @@ -30,6 +30,7 @@ import org.springframework.web.util.UriComponentsBuilder; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; @@ -82,15 +83,15 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { @Test - public void testSaveTwoFaSettings() throws Exception { + public void testSavePlatformTwoFaSettingsForDifferentAuthorities() throws Exception { loginSysAdmin(); - testSaveTestTwoFaSettings(); + testSavePlatformTwoFaSettings(); loginTenantAdmin(); - testSaveTestTwoFaSettings(); + testSavePlatformTwoFaSettings(); } - private void testSaveTestTwoFaSettings() throws Exception { + private void testSavePlatformTwoFaSettings() throws Exception { TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig(); totpTwoFaProviderConfig.setIssuerName("tb"); SmsTwoFaProviderConfig smsTwoFaProviderConfig = new SmsTwoFaProviderConfig(); @@ -113,7 +114,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { } @Test - public void testSaveTwoFaSettings_validationError() throws Exception { + public void testSavePlatformTwoFaSettings_validationError() throws Exception { loginTenantAdmin(); PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); @@ -147,7 +148,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { } @Test - public void testGetTwoFaSettings_useSysadminSettingsAsDefault() throws Exception { + public void testGetPlatformTwoFaSettings_useSysadminSettingsAsDefault() throws Exception { loginSysAdmin(); PlatformTwoFaSettings sysadminTwoFaSettings = new PlatformTwoFaSettings(); TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig(); @@ -204,7 +205,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { TotpTwoFaProviderConfig invalidTotpTwoFaProviderConfig = new TotpTwoFaProviderConfig(); invalidTotpTwoFaProviderConfig.setIssuerName(" "); - String errorResponse = saveTwoFaSettingsAndGetError(invalidTotpTwoFaProviderConfig); + String errorResponse = savePlatformTwoFaSettingsAndGetError(invalidTotpTwoFaProviderConfig); assertThat(errorResponse).containsIgnoringCase("issuer name must not be blank"); } @@ -214,17 +215,17 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { invalidSmsTwoFaProviderConfig.setSmsVerificationMessageTemplate("does not contain verification code"); invalidSmsTwoFaProviderConfig.setVerificationCodeLifetime(60); - String errorResponse = saveTwoFaSettingsAndGetError(invalidSmsTwoFaProviderConfig); + String errorResponse = savePlatformTwoFaSettingsAndGetError(invalidSmsTwoFaProviderConfig); assertThat(errorResponse).containsIgnoringCase("must contain verification code"); invalidSmsTwoFaProviderConfig.setSmsVerificationMessageTemplate(null); invalidSmsTwoFaProviderConfig.setVerificationCodeLifetime(0); - errorResponse = saveTwoFaSettingsAndGetError(invalidSmsTwoFaProviderConfig); + errorResponse = savePlatformTwoFaSettingsAndGetError(invalidSmsTwoFaProviderConfig); assertThat(errorResponse).containsIgnoringCase("verification message template is required"); assertThat(errorResponse).containsIgnoringCase("verification code lifetime is required"); } - private String saveTwoFaSettingsAndGetError(TwoFaProviderConfig invalidTwoFaProviderConfig) throws Exception { + private String savePlatformTwoFaSettingsAndGetError(TwoFaProviderConfig invalidTwoFaProviderConfig) throws Exception { PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); twoFaSettings.setProviders(Collections.singletonList(invalidTwoFaProviderConfig)); @@ -232,6 +233,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { .andExpect(status().isBadRequest())); } + @Test public void testSaveTwoFaAccountConfig_providerNotConfigured() throws Exception { configureSmsTwoFaProvider("${verificationCode}"); @@ -255,7 +257,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { loginTenantAdmin(); - assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), String.class)).isNullOrEmpty(); + assertThat(readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), String.class)).isNullOrEmpty(); generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig); } @@ -309,7 +311,10 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, generatedTotpTwoFaAccountConfig) .andExpect(status().isOk()); - TwoFaAccountConfig twoFaAccountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFaAccountConfig.class); + AccountTwoFaSettings accountTwoFaSettings = readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class); + assertThat(accountTwoFaSettings.getConfigs()).size().isOne(); + + TwoFaAccountConfig twoFaAccountConfig = accountTwoFaSettings.getConfigs().get(TwoFaProviderType.TOTP); assertThat(twoFaAccountConfig).isEqualTo(generatedTotpTwoFaAccountConfig); } @@ -349,15 +354,16 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { @Test public void testGetTwoFaAccountConfig_whenProviderNotConfigured() throws Exception { testVerifyAndSaveTotpTwoFaAccountConfig(); - assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), - TotpTwoFaAccountConfig.class)).isNotNull(); + assertThat(readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), + AccountTwoFaSettings.class).getConfigs()).isNotEmpty(); loginSysAdmin(); - saveProvidersConfigs(); - assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), String.class)) - .isNullOrEmpty(); + loginTenantAdmin(); + + assertThat(readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class).getConfigs()) + .isEmpty(); } @Test @@ -427,7 +433,8 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, smsTwoFaAccountConfig) .andExpect(status().isOk()); - TwoFaAccountConfig accountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFaAccountConfig.class); + AccountTwoFaSettings accountTwoFaSettings = readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class); + TwoFaAccountConfig accountConfig = accountTwoFaSettings.getConfigs().get(TwoFaProviderType.SMS); assertThat(accountConfig).isEqualTo(smsTwoFaAccountConfig); } @@ -470,7 +477,8 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, initialSmsTwoFaAccountConfig) .andExpect(status().isOk()); - TwoFaAccountConfig accountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFaAccountConfig.class); + AccountTwoFaSettings accountTwoFaSettings = readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class); + TwoFaAccountConfig accountConfig = accountTwoFaSettings.getConfigs().get(TwoFaProviderType.SMS); assertThat(accountConfig).isEqualTo(initialSmsTwoFaAccountConfig); } @@ -518,13 +526,14 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { twoFaConfigManager.saveTwoFaAccountConfig(tenantId, tenantAdminUserId, accountConfig); - TwoFaAccountConfig savedAccountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFaAccountConfig.class); + AccountTwoFaSettings accountTwoFaSettings = readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class); + TwoFaAccountConfig savedAccountConfig = accountTwoFaSettings.getConfigs().get(TwoFaProviderType.SMS); assertThat(savedAccountConfig).isEqualTo(accountConfig); - doDelete("/api/2fa/account/config").andExpect(status().isOk()); + doDelete("/api/2fa/account/config?providerType=SMS").andExpect(status().isOk()); - assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), String.class)) - .isNullOrEmpty(); + assertThat(readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class).getConfigs()) + .doesNotContainKey(TwoFaProviderType.SMS); } } diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java index 17910c4f16..1ad340d54b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; @@ -36,23 +37,26 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.security.Authority; -import org.thingsboard.server.dao.audit.AuditLogService; -import org.thingsboard.server.dao.user.UserService; -import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; -import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; +import org.thingsboard.server.common.data.security.model.mfa.account.EmailTwoFaAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.account.SmsTwoFaAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.account.TotpTwoFaAccountConfig; +import org.thingsboard.server.common.data.security.model.mfa.provider.EmailTwoFaProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.SmsTwoFaProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TotpTwoFaProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; +import org.thingsboard.server.dao.audit.AuditLogService; +import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; +import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; import org.thingsboard.server.service.security.auth.rest.LoginRequest; import org.thingsboard.server.service.security.model.JwtTokenPair; import java.time.Duration; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -68,319 +72,372 @@ import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; public abstract class TwoFactorAuthTest extends AbstractControllerTest { -// -// @Autowired -// private TwoFaConfigManager twoFaConfigManager; -// @Autowired -// private TwoFactorAuthService twoFactorAuthService; -// @MockBean -// private SmsService smsService; -// @Autowired -// private AuditLogService auditLogService; -// @Autowired -// private UserService userService; -// -// private User user; -// private String username; -// private String password; -// -// @Before -// public void beforeEach() throws Exception { -// username = "mfa@tb.io"; -// password = "psswrd"; -// -// user = new User(); -// user.setAuthority(Authority.TENANT_ADMIN); -// user.setEmail(username); -// user.setTenantId(tenantId); -// -// loginSysAdmin(); -// user = createUser(user, password); -// } -// -// @After -// public void afterEach() { -// twoFaConfigManager.deletePlatformTwoFaSettings(tenantId); -// twoFaConfigManager.deletePlatformTwoFaSettings(TenantId.SYS_TENANT_ID); -// } -// -// @Test -// public void testTwoFa_totp() throws Exception { -// TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa(); -// -// logInWithPreVerificationToken(); -// -// doPost("/api/auth/2fa/verification/send") -// .andExpect(status().isOk()); -// -// String correctVerificationCode = getCorrectTotp(totpTwoFaAccountConfig); -// -// JsonNode tokenPair = readResponse(doPost("/api/auth/2fa/verification/check?verificationCode=" + correctVerificationCode) -// .andExpect(status().isOk()), JsonNode.class); -// validateAndSetJwtToken(tokenPair, username); -// -// User currentUser = readResponse(doGet("/api/auth/user") -// .andExpect(status().isOk()), User.class); -// assertThat(currentUser.getId()).isEqualTo(user.getId()); -// } -// -// @Test -// public void testTwoFa_sms() throws Exception { -// configureSmsTwoFa(); -// -// logInWithPreVerificationToken(); -// -// doPost("/api/auth/2fa/verification/send") -// .andExpect(status().isOk()); -// -// ArgumentCaptor verificationCodeCaptor = ArgumentCaptor.forClass(String.class); -// verify(smsService).sendSms(eq(tenantId), any(), any(), verificationCodeCaptor.capture()); -// String correctVerificationCode = verificationCodeCaptor.getValue(); -// -// JsonNode tokenPair = readResponse(doPost("/api/auth/2fa/verification/check?verificationCode=" + correctVerificationCode) -// .andExpect(status().isOk()), JsonNode.class); -// validateAndSetJwtToken(tokenPair, username); -// -// User currentUser = readResponse(doGet("/api/auth/user") -// .andExpect(status().isOk()), User.class); -// assertThat(currentUser.getId()).isEqualTo(user.getId()); -// } -// -// @Test -// public void testTwoFaPreVerificationTokenLifetime() throws Exception { -// configureTotpTwoFa(twoFaSettings -> { -// twoFaSettings.setTotalAllowedTimeForVerification(5); -// }); -// -// logInWithPreVerificationToken(); -// -// await("expiration of the pre-verification token") -// .atLeast(Duration.ofSeconds(3).plusMillis(500)) -// .atMost(Duration.ofSeconds(6)) -// .untilAsserted(() -> { -// doPost("/api/auth/2fa/verification/send") -// .andExpect(status().isUnauthorized()); -// }); -// } -// -// @Test -// public void testCheckVerificationCode_userBlocked() throws Exception { -// configureTotpTwoFa(twoFaSettings -> { -// twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10); -// }); -// -// logInWithPreVerificationToken(); -// -// Stream.generate(() -> RandomStringUtils.randomNumeric(6)) -// .limit(9) -// .forEach(incorrectVerificationCode -> { -// try { -// String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=" + incorrectVerificationCode) -// .andExpect(status().isBadRequest())); -// assertThat(errorMessage).containsIgnoringCase("verification code is incorrect"); -// } catch (Exception e) { -// fail(); -// } -// }); -// -// String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=" + RandomStringUtils.randomNumeric(6)) -// .andExpect(status().isUnauthorized())); -// assertThat(errorMessage).containsIgnoringCase("account was locked due to exceeded 2fa verification attempts"); -// -// errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=" + RandomStringUtils.randomNumeric(6)) -// .andExpect(status().isUnauthorized())); -// assertThat(errorMessage).containsIgnoringCase("user is disabled"); -// } -// -// @Test -// public void testSendVerificationCode_rateLimit() throws Exception { -// configureTotpTwoFa(twoFaSettings -> { -// twoFaSettings.setVerificationCodeSendRateLimit("3:10"); -// }); -// -// logInWithPreVerificationToken(); -// -// for (int i = 0; i < 3; i++) { -// doPost("/api/auth/2fa/verification/send") -// .andExpect(status().isOk()); -// } -// -// String rateLimitExceededError = getErrorMessage(doPost("/api/auth/2fa/verification/send") -// .andExpect(status().isTooManyRequests())); -// assertThat(rateLimitExceededError).containsIgnoringCase("too many verification code sending requests"); -// -// await("verification code sending rate limit resetting") -// .atLeast(Duration.ofSeconds(8)) -// .atMost(Duration.ofSeconds(12)) -// .untilAsserted(() -> { -// doPost("/api/auth/2fa/verification/send") -// .andExpect(status().isOk()); -// }); -// } -// -// @Test -// public void testCheckVerificationCode_rateLimit() throws Exception { -// TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa(twoFaSettings -> { -// twoFaSettings.setVerificationCodeCheckRateLimit("3:10"); -// }); -// -// logInWithPreVerificationToken(); -// -// for (int i = 0; i < 3; i++) { -// String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=incorrect") -// .andExpect(status().isBadRequest())); -// assertThat(incorrectVerificationCodeError).containsIgnoringCase("verification code is incorrect"); -// } -// -// String rateLimitExceededError = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=incorrect") -// .andExpect(status().isTooManyRequests())); -// assertThat(rateLimitExceededError).containsIgnoringCase("too many verification code checking requests"); -// -// await("verification code checking rate limit resetting") -// .atLeast(Duration.ofSeconds(8)) -// .atMost(Duration.ofSeconds(12)) -// .untilAsserted(() -> { -// String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=incorrect") -// .andExpect(status().isBadRequest())); -// assertThat(incorrectVerificationCodeError).containsIgnoringCase("verification code is incorrect"); -// }); -// -// doPost("/api/auth/2fa/verification/check?verificationCode=" + getCorrectTotp(totpTwoFaAccountConfig)) -// .andExpect(status().isOk()); -// } -// -// @Test -// public void testCheckVerificationCode_invalidVerificationCode() throws Exception { -// configureTotpTwoFa(); -// logInWithPreVerificationToken(); -// -// for (String invalidVerificationCode : new String[]{"1234567", "ab1212", "12311 ", "oewkriwejqf"}) { -// String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=" + invalidVerificationCode) -// .andExpect(status().isBadRequest())); -// assertThat(errorMessage).containsIgnoringCase("verification code is incorrect"); -// } -// } -// -// @Test -// public void testCheckVerificationCode_codeExpiration() throws Exception { -// configureSmsTwoFa(smsTwoFaProviderConfig -> { -// smsTwoFaProviderConfig.setVerificationCodeLifetime(10); -// }); -// -// logInWithPreVerificationToken(); -// -// ArgumentCaptor verificationCodeCaptor = ArgumentCaptor.forClass(String.class); -// doPost("/api/auth/2fa/verification/send").andExpect(status().isOk()); -// verify(smsService).sendSms(eq(tenantId), any(), any(), verificationCodeCaptor.capture()); -// -// String correctVerificationCode = verificationCodeCaptor.getValue(); -// -// await("verification code expiration") -// .pollDelay(10, TimeUnit.SECONDS) -// .atLeast(10, TimeUnit.SECONDS) -// .atMost(12, TimeUnit.SECONDS) -// .untilAsserted(() -> { -// String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=" + correctVerificationCode) -// .andExpect(status().isBadRequest())); -// assertThat(incorrectVerificationCodeError).containsIgnoringCase("verification code is incorrect"); -// }); -// } -// -// @Test -// public void testTwoFa_logLoginAction() throws Exception { -// TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa(); -// -// logInWithPreVerificationToken(); -// await("async audit log saving").during(1, TimeUnit.SECONDS); -// assertThat(getLogInAuditLogs()).isEmpty(); -// assertThat(userService.findUserById(tenantId, user.getId()).getAdditionalInfo() -// .get("lastLoginTs")).isNull(); -// -// doPost("/api/auth/2fa/verification/check?verificationCode=incorrect") -// .andExpect(status().isBadRequest()); -// -// await("async audit log saving").atMost(1, TimeUnit.SECONDS) -// .until(() -> getLogInAuditLogs().size() == 1); -// assertThat(getLogInAuditLogs().get(0)).satisfies(failedLogInAuditLog -> { -// assertThat(failedLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.FAILURE); -// assertThat(failedLogInAuditLog.getActionFailureDetails()).containsIgnoringCase("verification code is incorrect"); -// assertThat(failedLogInAuditLog.getUserName()).isEqualTo(username); -// }); -// -// doPost("/api/auth/2fa/verification/check?verificationCode=" + getCorrectTotp(totpTwoFaAccountConfig)) -// .andExpect(status().isOk()); -// await("async audit log saving").atMost(1, TimeUnit.SECONDS) -// .until(() -> getLogInAuditLogs().size() == 2); -// assertThat(getLogInAuditLogs().get(0)).satisfies(successfulLogInAuditLog -> { -// assertThat(successfulLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.SUCCESS); -// assertThat(successfulLogInAuditLog.getUserName()).isEqualTo(username); -// }); -// assertThat(userService.findUserById(tenantId, user.getId()).getAdditionalInfo() -// .get("lastLoginTs").asLong()) -// .isGreaterThan(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(3)); -// } -// -// private List getLogInAuditLogs() { -// return auditLogService.findAuditLogsByTenantIdAndUserId(tenantId, user.getId(), List.of(ActionType.LOGIN), -// new TimePageLink(new PageLink(10, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC)), 0L, System.currentTimeMillis())).getData(); -// } -// -// @Test -// public void testAuthWithoutTwoFaAccountConfig() throws ThingsboardException { -// configureTotpTwoFa(); -// twoFaConfigManager.deleteTwoFaAccountConfig(tenantId, user.getId(), ); -// -// assertDoesNotThrow(() -> { -// login(username, password); -// }); -// } -// -// private void logInWithPreVerificationToken() throws Exception { -// LoginRequest loginRequest = new LoginRequest(username, password); -// -// JwtTokenPair response = readResponse(doPost("/api/auth/login", loginRequest).andExpect(status().isOk()), JwtTokenPair.class); -// assertThat(response.getToken()).isNotNull(); -// assertThat(response.getRefreshToken()).isNull(); -// assertThat(response.getScope()).isEqualTo(Authority.PRE_VERIFICATION_TOKEN); -// -// this.token = response.getToken(); -// } -// -// private TotpTwoFaAccountConfig configureTotpTwoFa(Consumer... customizer) throws ThingsboardException { -// TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig(); -// totpTwoFaProviderConfig.setIssuerName("tb"); -// -// PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); -// twoFaSettings.setUseSystemTwoFactorAuthSettings(false); -// twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{totpTwoFaProviderConfig}).collect(Collectors.toList())); -// Arrays.stream(customizer).forEach(c -> c.accept(twoFaSettings)); -// twoFaConfigManager.savePlatformTwoFaSettings(tenantId, twoFaSettings); -// -// TotpTwoFaAccountConfig totpTwoFaAccountConfig = (TotpTwoFaAccountConfig) twoFactorAuthService.generateNewAccountConfig(user, TwoFaProviderType.TOTP); -// twoFaConfigManager.saveTwoFaAccountConfig(tenantId, user.getId(), totpTwoFaAccountConfig); -// return totpTwoFaAccountConfig; -// } -// -// private SmsTwoFaAccountConfig configureSmsTwoFa(Consumer... customizer) throws ThingsboardException { -// SmsTwoFaProviderConfig smsTwoFaProviderConfig = new SmsTwoFaProviderConfig(); -// smsTwoFaProviderConfig.setVerificationCodeLifetime(60); -// smsTwoFaProviderConfig.setSmsVerificationMessageTemplate("${verificationCode}"); -// Arrays.stream(customizer).forEach(c -> c.accept(smsTwoFaProviderConfig)); -// -// PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); -// twoFaSettings.setUseSystemTwoFactorAuthSettings(false); -// twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{smsTwoFaProviderConfig}).collect(Collectors.toList())); -// twoFaConfigManager.savePlatformTwoFaSettings(tenantId, twoFaSettings); -// -// SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig(); -// smsTwoFaAccountConfig.setPhoneNumber("+38050505050"); -// twoFaConfigManager.saveTwoFaAccountConfig(tenantId, user.getId(), smsTwoFaAccountConfig); -// return smsTwoFaAccountConfig; -// } -// -// private String getCorrectTotp(TotpTwoFaAccountConfig totpTwoFaAccountConfig) { -// String secret = StringUtils.substringAfterLast(totpTwoFaAccountConfig.getAuthUrl(), "secret="); -// return new Totp(secret).now(); -// } + + @Autowired + private TwoFaConfigManager twoFaConfigManager; + @Autowired + private TwoFactorAuthService twoFactorAuthService; + @MockBean + private SmsService smsService; + @Autowired + private AuditLogService auditLogService; + @Autowired + private UserService userService; + + private User user; + private String username; + private String password; + + @Before + public void beforeEach() throws Exception { + username = "mfa@tb.io"; + password = "psswrd"; + + user = new User(); + user.setAuthority(Authority.TENANT_ADMIN); + user.setEmail(username); + user.setTenantId(tenantId); + + loginSysAdmin(); + user = createUser(user, password); + } + + @After + public void afterEach() { + twoFaConfigManager.deletePlatformTwoFaSettings(tenantId); + twoFaConfigManager.deletePlatformTwoFaSettings(TenantId.SYS_TENANT_ID); + } + + @Test + public void testTwoFa_totp() throws Exception { + TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa(); + + logInWithPreVerificationToken(username, password); + + doPost("/api/auth/2fa/verification/send?providerType=TOTP") + .andExpect(status().isOk()); + + String correctVerificationCode = getCorrectTotp(totpTwoFaAccountConfig); + + JsonNode tokenPair = readResponse(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + correctVerificationCode) + .andExpect(status().isOk()), JsonNode.class); + validateAndSetJwtToken(tokenPair, username); + + User currentUser = readResponse(doGet("/api/auth/user") + .andExpect(status().isOk()), User.class); + assertThat(currentUser.getId()).isEqualTo(user.getId()); + } + + @Test + public void testTwoFa_sms() throws Exception { + configureSmsTwoFa(); + + logInWithPreVerificationToken(username, password); + + doPost("/api/auth/2fa/verification/send?providerType=SMS") + .andExpect(status().isOk()); + + ArgumentCaptor verificationCodeCaptor = ArgumentCaptor.forClass(String.class); + verify(smsService).sendSms(eq(tenantId), any(), any(), verificationCodeCaptor.capture()); + String correctVerificationCode = verificationCodeCaptor.getValue(); + + JsonNode tokenPair = readResponse(doPost("/api/auth/2fa/verification/check?providerType=SMS&verificationCode=" + correctVerificationCode) + .andExpect(status().isOk()), JsonNode.class); + validateAndSetJwtToken(tokenPair, username); + + User currentUser = readResponse(doGet("/api/auth/user") + .andExpect(status().isOk()), User.class); + assertThat(currentUser.getId()).isEqualTo(user.getId()); + } + + @Test + public void testTwoFaPreVerificationTokenLifetime() throws Exception { + configureTotpTwoFa(twoFaSettings -> { + twoFaSettings.setTotalAllowedTimeForVerification(5); + }); + + logInWithPreVerificationToken(username, password); + + await("expiration of the pre-verification token") + .atLeast(Duration.ofSeconds(3).plusMillis(500)) + .atMost(Duration.ofSeconds(6)) + .untilAsserted(() -> { + doPost("/api/auth/2fa/verification/send?providerType=TOTP") + .andExpect(status().isUnauthorized()); + }); + } + + @Test + public void testCheckVerificationCode_userBlocked() throws Exception { + configureTotpTwoFa(twoFaSettings -> { + twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10); + }); + + logInWithPreVerificationToken(username, password); + + Stream.generate(() -> RandomStringUtils.randomNumeric(6)) + .limit(9) + .forEach(incorrectVerificationCode -> { + try { + String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + incorrectVerificationCode) + .andExpect(status().isBadRequest())); + assertThat(errorMessage).containsIgnoringCase("verification code is incorrect"); + } catch (Exception e) { + fail(); + } + }); + + String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + RandomStringUtils.randomNumeric(6)) + .andExpect(status().isUnauthorized())); + assertThat(errorMessage).containsIgnoringCase("account was locked due to exceeded 2fa verification attempts"); + + errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + RandomStringUtils.randomNumeric(6)) + .andExpect(status().isUnauthorized())); + assertThat(errorMessage).containsIgnoringCase("user is disabled"); + } + + @Test + public void testSendVerificationCode_rateLimit() throws Exception { + configureTotpTwoFa(twoFaSettings -> { + twoFaSettings.setVerificationCodeSendRateLimit("3:10"); + }); + + logInWithPreVerificationToken(username, password); + + for (int i = 0; i < 3; i++) { + doPost("/api/auth/2fa/verification/send?providerType=TOTP") + .andExpect(status().isOk()); + } + + String rateLimitExceededError = getErrorMessage(doPost("/api/auth/2fa/verification/send?providerType=TOTP") + .andExpect(status().isTooManyRequests())); + assertThat(rateLimitExceededError).containsIgnoringCase("too many requests"); + + await("verification code sending rate limit resetting") + .atLeast(Duration.ofSeconds(8)) + .atMost(Duration.ofSeconds(12)) + .untilAsserted(() -> { + doPost("/api/auth/2fa/verification/send?providerType=TOTP") + .andExpect(status().isOk()); + }); + } + + @Test + public void testCheckVerificationCode_rateLimit() throws Exception { + TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa(twoFaSettings -> { + twoFaSettings.setVerificationCodeCheckRateLimit("3:10"); + }); + + logInWithPreVerificationToken(username, password); + + for (int i = 0; i < 3; i++) { + String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=incorrect") + .andExpect(status().isBadRequest())); + assertThat(incorrectVerificationCodeError).containsIgnoringCase("verification code is incorrect"); + } + + String rateLimitExceededError = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=incorrect") + .andExpect(status().isTooManyRequests())); + assertThat(rateLimitExceededError).containsIgnoringCase("too many requests"); + + await("verification code checking rate limit resetting") + .atLeast(Duration.ofSeconds(8)) + .atMost(Duration.ofSeconds(12)) + .untilAsserted(() -> { + String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=incorrect") + .andExpect(status().isBadRequest())); + assertThat(incorrectVerificationCodeError).containsIgnoringCase("verification code is incorrect"); + }); + + doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + getCorrectTotp(totpTwoFaAccountConfig)) + .andExpect(status().isOk()); + } + + @Test + public void testCheckVerificationCode_invalidVerificationCode() throws Exception { + configureTotpTwoFa(); + logInWithPreVerificationToken(username, password); + + for (String invalidVerificationCode : new String[]{"1234567", "ab1212", "12311 ", "oewkriwejqf"}) { + String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + invalidVerificationCode) + .andExpect(status().isBadRequest())); + assertThat(errorMessage).containsIgnoringCase("verification code is incorrect"); + } + } + + @Test + public void testCheckVerificationCode_codeExpiration() throws Exception { + configureSmsTwoFa(smsTwoFaProviderConfig -> { + smsTwoFaProviderConfig.setVerificationCodeLifetime(10); + }); + + logInWithPreVerificationToken(username, password); + + ArgumentCaptor verificationCodeCaptor = ArgumentCaptor.forClass(String.class); + doPost("/api/auth/2fa/verification/send?providerType=SMS").andExpect(status().isOk()); + verify(smsService).sendSms(eq(tenantId), any(), any(), verificationCodeCaptor.capture()); + + String correctVerificationCode = verificationCodeCaptor.getValue(); + + await("verification code expiration") + .pollDelay(10, TimeUnit.SECONDS) + .atLeast(10, TimeUnit.SECONDS) + .atMost(12, TimeUnit.SECONDS) + .untilAsserted(() -> { + String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=SMS&verificationCode=" + correctVerificationCode) + .andExpect(status().isBadRequest())); + assertThat(incorrectVerificationCodeError).containsIgnoringCase("verification code is incorrect"); + }); + } + + @Test + public void testTwoFa_logLoginAction() throws Exception { + TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa(); + + logInWithPreVerificationToken(username, password); + await("async audit log saving").during(1, TimeUnit.SECONDS); + assertThat(getLogInAuditLogs()).isEmpty(); + assertThat(userService.findUserById(tenantId, user.getId()).getAdditionalInfo() + .get("lastLoginTs")).isNull(); + + doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=incorrect") + .andExpect(status().isBadRequest()); + + await("async audit log saving").atMost(1, TimeUnit.SECONDS) + .until(() -> getLogInAuditLogs().size() == 1); + assertThat(getLogInAuditLogs().get(0)).satisfies(failedLogInAuditLog -> { + assertThat(failedLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.FAILURE); + assertThat(failedLogInAuditLog.getActionFailureDetails()).containsIgnoringCase("verification code is incorrect"); + assertThat(failedLogInAuditLog.getUserName()).isEqualTo(username); + }); + + doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + getCorrectTotp(totpTwoFaAccountConfig)) + .andExpect(status().isOk()); + await("async audit log saving").atMost(1, TimeUnit.SECONDS) + .until(() -> getLogInAuditLogs().size() == 2); + assertThat(getLogInAuditLogs().get(0)).satisfies(successfulLogInAuditLog -> { + assertThat(successfulLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.SUCCESS); + assertThat(successfulLogInAuditLog.getUserName()).isEqualTo(username); + }); + assertThat(userService.findUserById(tenantId, user.getId()).getAdditionalInfo() + .get("lastLoginTs").asLong()) + .isGreaterThan(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(3)); + } + + private List getLogInAuditLogs() { + return auditLogService.findAuditLogsByTenantIdAndUserId(tenantId, user.getId(), List.of(ActionType.LOGIN), + new TimePageLink(new PageLink(10, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC)), 0L, System.currentTimeMillis())).getData(); + } + + @Test + public void testAuthWithoutTwoFaAccountConfig() throws ThingsboardException { + configureTotpTwoFa(); + twoFaConfigManager.deleteTwoFaAccountConfig(tenantId, user.getId(), TwoFaProviderType.TOTP); + + assertDoesNotThrow(() -> { + login(username, password); + }); + } + + @Test + public void testTwoFa_multipleProviders() throws Exception { + PlatformTwoFaSettings platformTwoFaSettings = new PlatformTwoFaSettings(); + platformTwoFaSettings.setUseSystemTwoFactorAuthSettings(true); + + TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig(); + totpTwoFaProviderConfig.setIssuerName("TB"); + + SmsTwoFaProviderConfig smsTwoFaProviderConfig = new SmsTwoFaProviderConfig(); + smsTwoFaProviderConfig.setVerificationCodeLifetime(60); + smsTwoFaProviderConfig.setSmsVerificationMessageTemplate("${verificationCode}"); + + EmailTwoFaProviderConfig emailTwoFaProviderConfig = new EmailTwoFaProviderConfig(); + emailTwoFaProviderConfig.setVerificationCodeLifetime(60); + + platformTwoFaSettings.setProviders(List.of(totpTwoFaProviderConfig, smsTwoFaProviderConfig, emailTwoFaProviderConfig)); + twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, platformTwoFaSettings); + + User twoFaUser = new User(); + twoFaUser.setAuthority(Authority.TENANT_ADMIN); + twoFaUser.setTenantId(tenantId); + twoFaUser.setEmail("2fa@thingsboard.org"); + twoFaUser = createUserAndLogin(twoFaUser, "12345678"); + + TotpTwoFaAccountConfig totpTwoFaAccountConfig = (TotpTwoFaAccountConfig) twoFactorAuthService.generateNewAccountConfig(twoFaUser, TwoFaProviderType.TOTP); + totpTwoFaAccountConfig.setUseByDefault(true); + twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser.getId(), totpTwoFaAccountConfig); + + SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig(); + smsTwoFaAccountConfig.setPhoneNumber("+38012312322"); + twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser.getId(), smsTwoFaAccountConfig); + + EmailTwoFaAccountConfig emailTwoFaAccountConfig = new EmailTwoFaAccountConfig(); + emailTwoFaAccountConfig.setEmail(twoFaUser.getEmail()); + twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser.getId(), emailTwoFaAccountConfig); + + logInWithPreVerificationToken(twoFaUser.getEmail(), "12345678"); + + Map providersInfos = readResponse(doGet("/api/auth/2fa/providers").andExpect(status().isOk()), new TypeReference>() {}).stream() + .collect(Collectors.toMap(TwoFactorAuthController.TwoFaProviderInfo::getType, v -> v)); + + assertThat(providersInfos).size().isEqualTo(3); + + assertThat(providersInfos).containsKey(TwoFaProviderType.TOTP); + assertThat(providersInfos.get(TwoFaProviderType.TOTP).isDefault()).isTrue(); + + assertThat(providersInfos).containsKey(TwoFaProviderType.SMS); + assertThat(providersInfos.get(TwoFaProviderType.SMS).isDefault()).isFalse(); + + assertThat(providersInfos).containsKey(TwoFaProviderType.EMAIL); + assertThat(providersInfos.get(TwoFaProviderType.EMAIL).isDefault()).isFalse(); + } + + private void logInWithPreVerificationToken(String username, String password) throws Exception { + LoginRequest loginRequest = new LoginRequest(username, password); + + JwtTokenPair response = readResponse(doPost("/api/auth/login", loginRequest).andExpect(status().isOk()), JwtTokenPair.class); + assertThat(response.getToken()).isNotNull(); + assertThat(response.getRefreshToken()).isNull(); + assertThat(response.getScope()).isEqualTo(Authority.PRE_VERIFICATION_TOKEN); + + this.token = response.getToken(); + } + + private TotpTwoFaAccountConfig configureTotpTwoFa(Consumer... customizer) throws ThingsboardException { + TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig(); + totpTwoFaProviderConfig.setIssuerName("tb"); + + PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); + twoFaSettings.setUseSystemTwoFactorAuthSettings(false); + twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{totpTwoFaProviderConfig}).collect(Collectors.toList())); + Arrays.stream(customizer).forEach(c -> c.accept(twoFaSettings)); + twoFaConfigManager.savePlatformTwoFaSettings(tenantId, twoFaSettings); + + TotpTwoFaAccountConfig totpTwoFaAccountConfig = (TotpTwoFaAccountConfig) twoFactorAuthService.generateNewAccountConfig(user, TwoFaProviderType.TOTP); + twoFaConfigManager.saveTwoFaAccountConfig(tenantId, user.getId(), totpTwoFaAccountConfig); + return totpTwoFaAccountConfig; + } + + private SmsTwoFaAccountConfig configureSmsTwoFa(Consumer... customizer) throws ThingsboardException { + SmsTwoFaProviderConfig smsTwoFaProviderConfig = new SmsTwoFaProviderConfig(); + smsTwoFaProviderConfig.setVerificationCodeLifetime(60); + smsTwoFaProviderConfig.setSmsVerificationMessageTemplate("${verificationCode}"); + Arrays.stream(customizer).forEach(c -> c.accept(smsTwoFaProviderConfig)); + + PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); + twoFaSettings.setUseSystemTwoFactorAuthSettings(false); + twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{smsTwoFaProviderConfig}).collect(Collectors.toList())); + twoFaConfigManager.savePlatformTwoFaSettings(tenantId, twoFaSettings); + + SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig(); + smsTwoFaAccountConfig.setPhoneNumber("+38050505050"); + twoFaConfigManager.saveTwoFaAccountConfig(tenantId, user.getId(), smsTwoFaAccountConfig); + return smsTwoFaAccountConfig; + } + + private String getCorrectTotp(TotpTwoFaAccountConfig totpTwoFaAccountConfig) { + String secret = StringUtils.substringAfterLast(totpTwoFaAccountConfig.getAuthUrl(), "secret="); + return new Totp(secret).now(); + } } diff --git a/common/message/src/test/java/org/thingsboard/server/common/msg/tools/RateLimitsTest.java b/common/message/src/test/java/org/thingsboard/server/common/msg/tools/RateLimitsTest.java index 3878d64ec9..b0bbfa3dc6 100644 --- a/common/message/src/test/java/org/thingsboard/server/common/msg/tools/RateLimitsTest.java +++ b/common/message/src/test/java/org/thingsboard/server/common/msg/tools/RateLimitsTest.java @@ -67,7 +67,7 @@ public class RateLimitsTest { assertThat(rateLimits.tryConsume()).as("new token is available").isFalse(); int expectedRefillTime = period * 1000; - int gap = 100; + int gap = 300; await("tokens refill for rate limit " + rateLimitConfig) .atLeast(expectedRefillTime - gap, TimeUnit.MILLISECONDS) From 70cbe29a5ddcdb8e766f382ed1174dba97813ad0 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 6 May 2022 17:52:22 +0300 Subject: [PATCH 098/276] UI: Add 2FA profile setting form and added support Email 2FA provider --- .../http/two-factor-authentication.service.ts | 40 ++++++- .../two-factor-auth-settings.component.html | 25 ++++- .../two-factor-auth-settings.component.ts | 26 +++-- .../email-auth-dialog.component.html | 104 +++++++++++++++++ .../email-auth-dialog.component.scss | 15 +++ .../email-auth-dialog.component.ts | 93 ++++++++++++++++ .../sms-auth-dialog.component.html | 105 ++++++++++++++++++ .../sms-auth-dialog.component.scss | 15 +++ .../sms-auth-dialog.component.ts | 91 +++++++++++++++ .../totp-auth-dialog.component.html | 97 ++++++++++++++++ .../totp-auth-dialog.component.scss | 15 +++ .../totp-auth-dialog.component.ts | 80 +++++++++++++ .../pages/profile/profile-routing.module.ts | 19 +++- .../home/pages/profile/profile.component.html | 65 ++++++++++- .../home/pages/profile/profile.component.scss | 17 +++ .../home/pages/profile/profile.component.ts | 103 ++++++++++++++++- .../home/pages/profile/profile.module.ts | 8 +- ui-ngx/src/app/shared/models/constants.ts | 1 + .../shared/models/two-factor-auth.models.ts | 35 +++++- .../assets/locale/locale.constant-en_US.json | 5 +- 20 files changed, 932 insertions(+), 27 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.ts diff --git a/ui-ngx/src/app/core/http/two-factor-authentication.service.ts b/ui-ngx/src/app/core/http/two-factor-authentication.service.ts index 230eddd208..36183db9d8 100644 --- a/ui-ngx/src/app/core/http/two-factor-authentication.service.ts +++ b/ui-ngx/src/app/core/http/two-factor-authentication.service.ts @@ -18,7 +18,12 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; import { Observable } from 'rxjs'; -import { TwoFactorAuthSettings } from '@shared/models/two-factor-auth.models'; +import { + AccountTwoFaSettings, + TwoFactorAuthAccountConfig, + TwoFactorAuthProviderType, + TwoFactorAuthSettings +} from '@shared/models/two-factor-auth.models'; @Injectable({ providedIn: 'root' @@ -38,4 +43,37 @@ export class TwoFactorAuthenticationService { return this.http.post(`/api/2fa/settings`, settings, defaultHttpOptionsFromConfig(config)); } + getAvailableTwoFaProviders(config?: RequestConfig): Observable> { + return this.http.get>(`/api/2fa/providers`, defaultHttpOptionsFromConfig(config)); + } + + generateTwoFaAccountConfig(providerType: TwoFactorAuthProviderType, config?: RequestConfig): Observable { + return this.http.post(`/api/2fa/account/config/generate?providerType=${providerType}`, + defaultHttpOptionsFromConfig(config)); + } + + getAccountTwoFaSettings(config?: RequestConfig): Observable { + return this.http.get(`/api/2fa/account/settings`, defaultHttpOptionsFromConfig(config)); + } + + updateTwoFaAccountConfig(providerType: TwoFactorAuthProviderType, useByDefault: boolean, + config?: RequestConfig): Observable { + return this.http.put(`/api/2fa/account/config?providerType=${providerType}`, {useByDefault}, + defaultHttpOptionsFromConfig(config)); + } + + submitTwoFaAccountConfig(authConfig: TwoFactorAuthAccountConfig, config?: RequestConfig): Observable { + return this.http.post(`/api/2fa/account/config/submit`, authConfig, defaultHttpOptionsFromConfig(config)); + } + + verifyAndSaveTwoFaAccountConfig(authConfig: TwoFactorAuthAccountConfig, verificationCode: number, + config?: RequestConfig): Observable { + return this.http.post(`/api/2fa/account/config?verificationCode=${verificationCode}`, authConfig, defaultHttpOptionsFromConfig(config)); + } + + deleteTwoFaAccountConfig(providerType: TwoFactorAuthProviderType, config?: RequestConfig): Observable { + return this.http.delete(`/api/2fa/account/config?providerType=${providerType}`, + defaultHttpOptionsFromConfig(config)); + } + } diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html index c92fe6de74..fccec76b54 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html @@ -81,7 +81,7 @@
- + {{ provider.value.providerType }} @@ -102,10 +102,10 @@ admin.2fa.provider - - {{ twoFactorAuthProviderType }} + + {{ provider }} @@ -144,6 +144,19 @@
+
+ + admin.2fa.verification-code-lifetime + + + {{ "admin.2fa.verification-code-lifetime-required" | translate }} + + + {{ "admin.2fa.verification-code-lifetime-pattern" | translate }} + + +
@@ -159,7 +172,7 @@ || providersForm.length == twoFactorAuthProviderTypes.length || (isLoading$ | async)" (click)="addProvider()"> add - action.add + admin.2fa.add-provider + + + +
+
+ + + done + + + Add email +
+ + user.email + + + {{ 'user.email-required' | translate }} + + + {{ 'user.invalid-email-format' | translate }} + + +
+ + +
+
+
+ + Authentication verification +
+

Enter the 6-digit code here

+

Enter the 6 digit verification code we just sent to {{ emailConfigForm.get('email').value }}.

+ + 6-digit code + + +
+ + +
+
+
+ + Two-factor authentication activated +
+

Email authentication enabled

+

If we notice an attempted login from a device or browser we don’t recognize, you will be prompted to enter the security code that will be sent to your email address.

+
+ +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.scss new file mode 100644 index 0000000000..ec6f008a80 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.scss @@ -0,0 +1,15 @@ +/** + * Copyright © 2016-2022 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. + */ diff --git a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.ts new file mode 100644 index 0000000000..be5976c78b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.ts @@ -0,0 +1,93 @@ +/// +/// Copyright © 2016-2022 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 { Component, Inject, ViewChild } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; +import { TwoFactorAuthAccountConfig, TwoFactorAuthProviderType } from '@shared/models/two-factor-auth.models'; +import { MatStepper } from '@angular/material/stepper'; + +export interface EmailAuthDialogData { + email: string; +} + +@Component({ + selector: 'tb-email-auth-dialog', + templateUrl: './email-auth-dialog.component.html', + styleUrls: ['./email-auth-dialog.component.scss'] +}) +export class EmailAuthDialogComponent extends DialogComponent { + + private authAccountConfig: TwoFactorAuthAccountConfig; + + emailConfigForm: FormGroup; + emailVerificationForm: FormGroup; + + @ViewChild('stepper', {static: false}) stepper: MatStepper; + + constructor(protected store: Store, + protected router: Router, + private twoFaService: TwoFactorAuthenticationService, + @Inject(MAT_DIALOG_DATA) public data: EmailAuthDialogData, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store, router, dialogRef); + + this.emailConfigForm = this.fb.group({ + email: [this.data.email, [Validators.required, Validators.email]] + }); + + this.emailVerificationForm = this.fb.group({ + verificationCode: ['', [ + Validators.required, + Validators.minLength(6), + Validators.maxLength(6), + Validators.pattern(/^\d*$/) + ]] + }); + } + + nextStep() { + switch (this.stepper.selectedIndex) { + case 0: + this.authAccountConfig = { + providerType: TwoFactorAuthProviderType.EMAIL, + useByDefault: true, + email: this.emailConfigForm.get('email').value as string + }; + this.twoFaService.submitTwoFaAccountConfig(this.authAccountConfig).subscribe(() => { + this.stepper.next(); + }); + break; + case 1: + this.twoFaService.verifyAndSaveTwoFaAccountConfig(this.authAccountConfig, + this.emailVerificationForm.get('verificationCode').value).subscribe(() => { + this.stepper.next(); + }); + break; + } + } + + closeDialog() { + return this.dialogRef.close(this.stepper.selectedIndex > 1); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.html b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.html new file mode 100644 index 0000000000..03c6c7a465 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.html @@ -0,0 +1,105 @@ + + +

Enable SMS authenticator

+ + +
+ + +
+
+ + + done + + + Add phone number +
+

Enter the phone number including the area code.

+ + Phone number + + + {{ 'admin.number-to-required' | translate }} + + + {{ 'admin.phone-number-pattern' | translate }} + + +
+ + +
+
+
+ + Authentication verification +
+

Enter the 6-digit code here

+

Enter the 6 digit verification code we just sent to {{ smsConfigForm.get('phone').value }}.

+ + 6-digit code + + +
+ + +
+
+
+ + Two-factor authentication activated +
+

SMS authentication enabled

+

If we notice an attempted login from a device or browser we don’t recognize, you will be prompted to enter the security code that will be sent to the phone number.

+
+ +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.scss new file mode 100644 index 0000000000..ec6f008a80 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.scss @@ -0,0 +1,15 @@ +/** + * Copyright © 2016-2022 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. + */ diff --git a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.ts new file mode 100644 index 0000000000..ed707922eb --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.ts @@ -0,0 +1,91 @@ +/// +/// Copyright © 2016-2022 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 { Component, ViewChild } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MatDialogRef } from '@angular/material/dialog'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; +import { TwoFactorAuthAccountConfig, TwoFactorAuthProviderType } from '@shared/models/two-factor-auth.models'; +import { phoneNumberPattern } from '@shared/models/settings.models'; +import { MatStepper } from '@angular/material/stepper'; + +@Component({ + selector: 'tb-sms-auth-dialog', + templateUrl: './sms-auth-dialog.component.html', + styleUrls: ['./sms-auth-dialog.component.scss'] +}) +export class SMSAuthDialogComponent extends DialogComponent { + + private authAccountConfig: TwoFactorAuthAccountConfig; + + phoneNumberPattern = phoneNumberPattern; + + smsConfigForm: FormGroup; + smsVerificationForm: FormGroup; + + @ViewChild('stepper', {static: false}) stepper: MatStepper; + + constructor(protected store: Store, + protected router: Router, + private twoFaService: TwoFactorAuthenticationService, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store, router, dialogRef); + + this.smsConfigForm = this.fb.group({ + phone: ['', [Validators.required, Validators.pattern(phoneNumberPattern)]] + }); + + this.smsVerificationForm = this.fb.group({ + verificationCode: ['', [ + Validators.required, + Validators.minLength(6), + Validators.maxLength(6), + Validators.pattern(/^\d*$/) + ]] + }); + } + + nextStep() { + switch (this.stepper.selectedIndex) { + case 0: + this.authAccountConfig = { + providerType: TwoFactorAuthProviderType.SMS, + useByDefault: true, + phoneNumber: this.smsConfigForm.get('phone').value as string + }; + this.twoFaService.submitTwoFaAccountConfig(this.authAccountConfig).subscribe(() => { + this.stepper.next(); + }); + break; + case 1: + this.twoFaService.verifyAndSaveTwoFaAccountConfig(this.authAccountConfig, + this.smsVerificationForm.get('verificationCode').value).subscribe(() => { + this.stepper.next(); + }); + break; + } + } + + closeDialog() { + return this.dialogRef.close(this.stepper.selectedIndex > 1); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.html b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.html new file mode 100644 index 0000000000..09afbb2de0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.html @@ -0,0 +1,97 @@ + + +

Enable authenticator app

+ + +
+ + +
+
+ + + done + + + Get app +
+

You'll need to use a verification app such as Google Authenticator, Authy, or Duo. Install from your app store

+
+ + +
+
+
+ + Authentication verification +
+

1. Scan this QR code with your verification app

+

Once your app reads the QR code, you'll get a 6-digit code.

+ +

2. Enter the 6-digit code here

+

Enter the code from the app below. Once connected, we'll remember your phone so you can use it each time you log in.

+ + 6-digit code + + +
+ + +
+
+
+ + Two-factor authentication activated +
+

Success

+

The next time you login from an unrecognized browser or device, you will need to provide a two-factor authentication code.

+
+ +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.scss new file mode 100644 index 0000000000..ec6f008a80 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.scss @@ -0,0 +1,15 @@ +/** + * Copyright © 2016-2022 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. + */ diff --git a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.ts new file mode 100644 index 0000000000..86b596ac2b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.ts @@ -0,0 +1,80 @@ +/// +/// Copyright © 2016-2022 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 { Component, ElementRef, ViewChild } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MatDialogRef } from '@angular/material/dialog'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; +import { TotpTwoFactorAuthAccountConfig, TwoFactorAuthProviderType } from '@shared/models/two-factor-auth.models'; +import { MatStepper } from '@angular/material/stepper'; + +@Component({ + selector: 'tb-totp-auth-dialog', + templateUrl: './totp-auth-dialog.component.html', + styleUrls: ['./totp-auth-dialog.component.scss'] +}) +export class TotpAuthDialogComponent extends DialogComponent { + + private authAccountConfig: TotpTwoFactorAuthAccountConfig; + + totpConfigForm: FormGroup; + totpAuthURL: string; + + @ViewChild('stepper', {static: false}) stepper: MatStepper; + @ViewChild('canvas', {static: false}) canvasRef: ElementRef; + + constructor(protected store: Store, + protected router: Router, + private twoFaService: TwoFactorAuthenticationService, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store, router, dialogRef); + this.twoFaService.generateTwoFaAccountConfig(TwoFactorAuthProviderType.TOTP).subscribe(accountConfig => { + this.authAccountConfig = accountConfig as TotpTwoFactorAuthAccountConfig; + this.totpAuthURL = this.authAccountConfig.authUrl; + this.authAccountConfig.useByDefault = true; + import('qrcode').then((QRCode) => { + QRCode.toCanvas(this.canvasRef.nativeElement, this.totpAuthURL); + this.canvasRef.nativeElement.style.width = 'auto'; + this.canvasRef.nativeElement.style.height = 'auto'; + }); + }); + this.totpConfigForm = this.fb.group({ + verificationCode: ['', [ + Validators.required, + Validators.minLength(6), + Validators.maxLength(6), + Validators.pattern(/^\d*$/) + ]] + }); + } + + onSaveConfig() { + this.twoFaService.verifyAndSaveTwoFaAccountConfig(this.authAccountConfig, + this.totpConfigForm.get('verificationCode').value).subscribe((res) => { + this.stepper.next(); + }); + } + + closeDialog() { + return this.dialogRef.close(this.stepper.selectedIndex > 1); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile-routing.module.ts b/ui-ngx/src/app/modules/home/pages/profile/profile-routing.module.ts index 440db606fd..e7dae87288 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/profile-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/profile/profile-routing.module.ts @@ -26,6 +26,8 @@ import { AppState } from '@core/core.state'; import { UserService } from '@core/http/user.service'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { Observable } from 'rxjs'; +import { TwoFactorAuthProviderType } from '@shared/models/two-factor-auth.models'; +import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; @Injectable() export class UserProfileResolver implements Resolve { @@ -40,6 +42,17 @@ export class UserProfileResolver implements Resolve { } } +@Injectable() +export class UserTwoFAProvidersResolver implements Resolve> { + + constructor(private twoFactorAuthService: TwoFactorAuthenticationService) { + } + + resolve(): Observable> { + return this.twoFactorAuthService.getAvailableTwoFaProviders(); + } +} + const routes: Routes = [ { path: 'profile', @@ -54,7 +67,8 @@ const routes: Routes = [ } }, resolve: { - user: UserProfileResolver + user: UserProfileResolver, + providers: UserTwoFAProvidersResolver } } ]; @@ -63,7 +77,8 @@ const routes: Routes = [ imports: [RouterModule.forChild(routes)], exports: [RouterModule], providers: [ - UserProfileResolver + UserProfileResolver, + UserTwoFAProvidersResolver ] }) export class ProfileRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile.component.html b/ui-ngx/src/app/modules/home/pages/profile/profile.component.html index b0bcabf62b..a1e9ec0523 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/profile.component.html +++ b/ui-ngx/src/app/modules/home/pages/profile/profile.component.html @@ -93,8 +93,7 @@ {{ expirationJwtData }}
-
- +
diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile.component.scss b/ui-ngx/src/app/modules/home/pages/profile/profile.component.scss index 08916ca584..f3f41c41d8 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/profile.component.scss +++ b/ui-ngx/src/app/modules/home/pages/profile/profile.component.scss @@ -59,4 +59,21 @@ } } } + .description { + padding-bottom: 8px; + color: #808080; + margin-right: 8px; + } + + .provider { + padding: 14px 0; + + .mat-h3 { + margin-bottom: 8px; + } + + .checkbox-label { + font-size: 14px; + } + } } diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts b/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts index 610006a3fd..0c676aca46 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts +++ b/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, ViewChild } from '@angular/core'; import { UserService } from '@core/http/user.service'; import { AuthUser, User } from '@shared/models/user.model'; import { Authority } from '@shared/models/authority.enum'; @@ -37,6 +37,12 @@ import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { DatePipe } from '@angular/common'; import { ClipboardService } from 'ngx-clipboard'; +import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; +import { AccountTwoFaSettings, TwoFactorAuthProviderType } from '@shared/models/two-factor-auth.models'; +import { MatSlideToggle } from '@angular/material/slide-toggle'; +import { TotpAuthDialogComponent } from '@home/pages/profile/authentication-dialog/totp-auth-dialog.component'; +import { SMSAuthDialogComponent } from '@home/pages/profile/authentication-dialog/sms-auth-dialog.component'; +import { EmailAuthDialogComponent, } from '@home/pages/profile/authentication-dialog/email-auth-dialog.component'; @Component({ selector: 'tb-profile', @@ -47,8 +53,25 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir authorities = Authority; profile: FormGroup; + twoFactorAuth: FormGroup; user: User; languageList = env.supportedLangs; + allowTwoFactorAuth = false; + allowSMS2faProvider = false; + allowTOTP2faProvider = false; + allowEmail2faProvider = false; + twoFactorAuthProviderType = TwoFactorAuthProviderType; + + @ViewChild('totp') totp: MatSlideToggle; + + private authDialogMap = new Map( +[ + [TwoFactorAuthProviderType.TOTP, TotpAuthDialogComponent], + [TwoFactorAuthProviderType.SMS, SMSAuthDialogComponent], + [TwoFactorAuthProviderType.EMAIL, EmailAuthDialogComponent] + ] + ); + private readonly authUser: AuthUser; get jwtToken(): string { @@ -69,6 +92,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir private userService: UserService, private authService: AuthService, private translate: TranslateService, + private twoFaService: TwoFactorAuthenticationService, public dialog: MatDialog, public dialogService: DialogService, public fb: FormBuilder, @@ -80,10 +104,12 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir ngOnInit() { this.buildProfileForm(); + this.buildTwoFactorForm(); this.userLoaded(this.route.snapshot.data.user); + this.twoFactorLoad(this.route.snapshot.data.providers); } - buildProfileForm() { + private buildProfileForm() { this.profile = this.fb.group({ email: ['', [Validators.required, Validators.email]], firstName: [''], @@ -94,6 +120,19 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir }); } + private buildTwoFactorForm() { + this.twoFactorAuth = this.fb.group({ + TOTP: [false], + SMS: [false], + EMAIL: [false], + useByDefault: [null] + }); + this.twoFactorAuth.get('useByDefault').valueChanges.subscribe(value => { + this.twoFaService.updateTwoFaAccountConfig(value, true, {ignoreLoading: true}) + .subscribe(data => this.processTwoFactorAuthConfig(data)); + }); + } + save(): void { this.user = {...this.user, ...this.profile.value}; if (!this.user.additionalInfo) { @@ -128,7 +167,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir }); } - userLoaded(user: User) { + private userLoaded(user: User) { this.user = user; this.profile.reset(user); let lang; @@ -151,6 +190,37 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir this.profile.get('homeDashboardHideToolbar').setValue(homeDashboardHideToolbar); } + private twoFactorLoad(providers: TwoFactorAuthProviderType[]) { + if (providers.length) { + this.allowTwoFactorAuth = true; + this.twoFaService.getAccountTwoFaSettings().subscribe(data => this.processTwoFactorAuthConfig(data)); + providers.forEach(provider => { + switch (provider) { + case TwoFactorAuthProviderType.SMS: + this.allowSMS2faProvider = true; + break; + case TwoFactorAuthProviderType.TOTP: + this.allowTOTP2faProvider = true; + break; + case TwoFactorAuthProviderType.EMAIL: + this.allowEmail2faProvider = true; + break; + } + }); + } + } + + private processTwoFactorAuthConfig(setting?: AccountTwoFaSettings) { + if (setting) { + Object.values(setting.configs).forEach(config => { + this.twoFactorAuth.get(config.providerType).setValue(true); + if (config.useByDefault) { + this.twoFactorAuth.get('useByDefault').setValue(config.providerType, {emitEvent: false}); + } + }); + } + } + confirmForm(): FormGroup { return this.profile; } @@ -179,4 +249,31 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir })); } } + + confirm2FAChange(event: MouseEvent, provider: TwoFactorAuthProviderType) { + event.stopPropagation(); + event.preventDefault(); + const providerName = provider === TwoFactorAuthProviderType.TOTP ? 'authenticator app' : `${provider.toLowerCase()} authentication`; + if (this.twoFactorAuth.get(provider).value) { + this.dialogService.confirm(`Are you sure you want to disable ${providerName}?`, + `Disabling ${providerName} will make your account less secure`).subscribe(res => { + if (res) { + this.twoFaService.deleteTwoFaAccountConfig(provider).subscribe(data => this.processTwoFactorAuthConfig(data)); + } + }); + } else { + this.dialog.open(this.authDialogMap.get(provider), { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + email: this.user.email + } + }).afterClosed().subscribe(res => { + if (res) { + this.twoFactorAuth.get(provider).setValue(res); + this.twoFactorAuth.get('useByDefault').setValue(provider, {emitEvent: false}); + } + }); + } + } } diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile.module.ts b/ui-ngx/src/app/modules/home/pages/profile/profile.module.ts index 4a8bcab074..5210158537 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/profile.module.ts +++ b/ui-ngx/src/app/modules/home/pages/profile/profile.module.ts @@ -20,11 +20,17 @@ import { ProfileComponent } from './profile.component'; import { SharedModule } from '@shared/shared.module'; import { ProfileRoutingModule } from './profile-routing.module'; import { ChangePasswordDialogComponent } from '@modules/home/pages/profile/change-password-dialog.component'; +import { TotpAuthDialogComponent } from './authentication-dialog/totp-auth-dialog.component'; +import { SMSAuthDialogComponent } from '@home/pages/profile/authentication-dialog/sms-auth-dialog.component'; +import { EmailAuthDialogComponent } from '@home/pages/profile/authentication-dialog/email-auth-dialog.component'; @NgModule({ declarations: [ ProfileComponent, - ChangePasswordDialogComponent + ChangePasswordDialogComponent, + TotpAuthDialogComponent, + SMSAuthDialogComponent, + EmailAuthDialogComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 4bfce6414a..833efd39ab 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -63,6 +63,7 @@ export const HelpLinks = { smsProviderSettings: helpBaseUrl + '/docs/user-guide/ui/sms-provider-settings', securitySettings: helpBaseUrl + '/docs/user-guide/ui/security-settings', oauth2Settings: helpBaseUrl + '/docs/user-guide/oauth-2-support/', + twoFactorAuthSettings: helpBaseUrl + '/docs/', ruleEngine: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/overview/', ruleNodeCheckRelation: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#check-relation-filter-node', ruleNodeCheckExistenceFields: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#check-existence-fields-node', diff --git a/ui-ngx/src/app/shared/models/two-factor-auth.models.ts b/ui-ngx/src/app/shared/models/two-factor-auth.models.ts index ab64143a66..731c1212e9 100644 --- a/ui-ngx/src/app/shared/models/two-factor-auth.models.ts +++ b/ui-ngx/src/app/shared/models/two-factor-auth.models.ts @@ -23,7 +23,8 @@ export interface TwoFactorAuthSettings { verificationCodeSendRateLimit: string; } -export type TwoFactorAuthProviderConfig = Partial; +export type TwoFactorAuthProviderConfig = Partial; export interface TotpTwoFactorAuthProviderConfig { providerType: TwoFactorAuthProviderType; @@ -36,7 +37,37 @@ export interface SmsTwoFactorAuthProviderConfig { verificationCodeLifetime: number; } +export interface EmailTwoFactorAuthProviderConfig { + providerType: TwoFactorAuthProviderType; + verificationCodeLifetime: number; +} + export enum TwoFactorAuthProviderType{ TOTP = 'TOTP', - SMS = 'SMS' + SMS = 'SMS', + EMAIL = 'EMAIL' +} + +interface GeneralTwoFactorAuthAccountConfig { + providerType: TwoFactorAuthProviderType; + useByDefault: boolean; +} + +export interface TotpTwoFactorAuthAccountConfig extends GeneralTwoFactorAuthAccountConfig { + authUrl: string; +} + +export interface SmsTwoFactorAuthAccountConfig extends GeneralTwoFactorAuthAccountConfig { + phoneNumber: string; +} + +export interface EmailTwoFactorAuthAccountConfig extends GeneralTwoFactorAuthAccountConfig { + email: string; +} + +export type TwoFactorAuthAccountConfig = TotpTwoFactorAuthAccountConfig | SmsTwoFactorAuthAccountConfig | EmailTwoFactorAuthAccountConfig; + + +export interface AccountTwoFaSettings { + configs?: {TwoFactorAuthProviderType: TwoFactorAuthAccountConfig}; } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 89b9bc5d2d..743068fc22 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -255,6 +255,7 @@ }, "2fa": { "2fa": "Two-factor authentication", + "add-provider": "Add provider", "available-providers": "Available providers:", "issuer-name": "Issuer name", "issuer-name-required": "Issuer name is required.", @@ -262,14 +263,14 @@ "max-verification-failures-before-user-lockout-pattern": "Max verification failures must be a positive integer.", "max-verification-failures-before-user-lockout-required": "Max verification failures is required.", "provider": "Provider", - "total-allowed-time-for-verification": "Total allowed time for verification", + "total-allowed-time-for-verification": "Total allowed time for verification (sec)", "total-allowed-time-for-verification-pattern": "Total allowed time must be a positive integer.", "total-allowed-time-for-verification-required": "Total allowed time is required.", "use-system-two-factor-auth-settings": "Use system two factor auth settings", "verification-code-check-rate-limit": "Verification code check rate limit", "verification-code-check-rate-limit-pattern": "Verification code check limit has invalid format", "verification-code-check-rate-limit-required": "Verification code check rate limit is required.", - "verification-code-lifetime": "Verification code lifetime", + "verification-code-lifetime": "Verification code lifetime (sec)", "verification-code-lifetime-pattern": "Verification code lifetime must be a positive integer.", "verification-code-lifetime-required": "Verification code lifetime is required.", "verification-code-send-rate-limit": "Verification code send rate limit", From 9f3f2985ab4a7cebfe949686eb07c5231cdf6c37 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Fri, 6 May 2022 19:12:41 +0300 Subject: [PATCH 099/276] Redis Cache implementation --- .../src/main/resources/thingsboard.yml | 4 +- ...CaffeineCacheDefaultConfigurationTest.java | 8 +- .../server/cache/CacheSpecsMap.java | 33 +++++ .../cache/TBRedisCacheConfiguration.java | 5 + .../cache/TBRedisClusterConfiguration.java | 2 +- .../cache/TBRedisStandaloneConfiguration.java | 2 +- .../server/cache/TbCacheTransaction.java | 5 - ...java => TbCaffeineCacheConfiguration.java} | 23 ++-- .../server/cache/TbTransactionalCache.java | 2 + ...rationTest.java => CacheSpecsMapTest.java} | 12 +- .../dao/attributes/AttributeRedisCache.java | 22 +++- .../server/dao/attributes/AttributesDao.java | 6 +- .../dao/attributes/BaseAttributesService.java | 6 +- .../attributes/CachedAttributesService.java | 74 ++++++----- .../dao/cache/CaffeineTbCacheTransaction.java | 19 --- .../cache/CaffeineTbTransactionalCache.java | 6 + .../dao/cache/RedisTbCacheTransaction.java | 70 ++++++++++ .../dao/cache/RedisTbTransactionalCache.java | 124 +++++++++++++++++- .../dao/cache/SimpleTbCacheValueWrapper.java | 8 ++ .../dao/sql/attributes/JpaAttributeDao.java | 17 +-- .../sql/attributes/AttributeServiceTest.java | 28 ++++ .../attributes/RedisAttributeServiceTest.java | 7 +- .../resources/application-test.properties | 60 ++++----- 23 files changed, 401 insertions(+), 142 deletions(-) create mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/CacheSpecsMap.java rename common/cache/src/main/java/org/thingsboard/server/cache/{CaffeineCacheConfiguration.java => TbCaffeineCacheConfiguration.java} (87%) rename common/cache/src/test/java/org/thingsboard/server/cache/{CaffeineCacheConfigurationTest.java => CacheSpecsMapTest.java} (87%) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/cache/RedisTbCacheTransaction.java diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 0aa7b3bb66..03dbb49763 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -387,8 +387,6 @@ cache: attributes: # make sure that if cache.type is 'redis' and cache.attributes.enabled is 'true' that you change 'maxmemory-policy' Redis config property to 'allkeys-lru', 'allkeys-lfu' or 'allkeys-random' enabled: "${CACHE_ATTRIBUTES_ENABLED:true}" - -caffeine: specs: relations: timeToLiveInMinutes: "${CACHE_SPECS_RELATIONS_TTL:1440}" @@ -475,6 +473,8 @@ redis: maxWaitMills: "${REDIS_POOL_CONFIG_MAX_WAIT_MS:60000}" numberTestsPerEvictionRun: "${REDIS_POOL_CONFIG_NUMBER_TESTS_PER_EVICTION_RUN:3}" blockWhenExhausted: "${REDIS_POOL_CONFIG_BLOCK_WHEN_EXHAUSTED:true}" + # TTL for short-living SET commands that are used to replace DEL in order to enable transaction support + evictTtlInMs: "${REDIS_EVICT_TTL_MS:60000}" # Check new version updates parameters updates: diff --git a/application/src/test/java/org/thingsboard/server/cache/CaffeineCacheDefaultConfigurationTest.java b/application/src/test/java/org/thingsboard/server/cache/CaffeineCacheDefaultConfigurationTest.java index 77d36878ac..9893f08aff 100644 --- a/application/src/test/java/org/thingsboard/server/cache/CaffeineCacheDefaultConfigurationTest.java +++ b/application/src/test/java/org/thingsboard/server/cache/CaffeineCacheDefaultConfigurationTest.java @@ -36,15 +36,15 @@ import static org.assertj.core.api.Assertions.assertThat; public class CaffeineCacheDefaultConfigurationTest { @Autowired - CaffeineCacheConfiguration caffeineCacheConfiguration; + CacheSpecsMap cacheSpecsMap; @Test public void verifyTransactionAwareCacheManagerProxy() { - assertThat(caffeineCacheConfiguration.getSpecs()).as("specs").isNotNull(); - caffeineCacheConfiguration.getSpecs().forEach((name, cacheSpecs)->assertThat(cacheSpecs).as("cache %s specs", name).isNotNull()); + assertThat(cacheSpecsMap.getSpecs()).as("specs").isNotNull(); + cacheSpecsMap.getSpecs().forEach((name, cacheSpecs)->assertThat(cacheSpecs).as("cache %s specs", name).isNotNull()); SoftAssertions softly = new SoftAssertions(); - caffeineCacheConfiguration.getSpecs().forEach((name, cacheSpecs)->{ + cacheSpecsMap.getSpecs().forEach((name, cacheSpecs)->{ softly.assertThat(name).as("cache name").isNotEmpty(); softly.assertThat(cacheSpecs.getTimeToLiveInMinutes()).as("cache %s time to live", name).isGreaterThan(0); softly.assertThat(cacheSpecs.getMaxSize()).as("cache %s max size", name).isGreaterThan(0); diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/CacheSpecsMap.java b/common/cache/src/main/java/org/thingsboard/server/cache/CacheSpecsMap.java new file mode 100644 index 0000000000..594bfff146 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/CacheSpecsMap.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.cache; + +import lombok.Data; +import lombok.Getter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.Map; + +@Configuration +@ConfigurationProperties(prefix = "cache") +@Data +public class CacheSpecsMap { + + @Getter + private Map specs; + +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java index 32127a5a39..2909839011 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java @@ -16,6 +16,8 @@ package org.thingsboard.server.cache; import lombok.Data; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.CacheManager; @@ -41,6 +43,9 @@ import java.time.Duration; @Data public abstract class TBRedisCacheConfiguration { + @Value("${redis.evictTtlInMs:60000}") + private int evictTtlInMs; + @Value("${redis.pool_config.maxTotal:128}") private int maxTotal; diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java index 4925842ad3..2cf67e35aa 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java @@ -29,7 +29,7 @@ import java.util.Collections; import java.util.List; @Configuration -@ConditionalOnMissingBean(CaffeineCacheConfiguration.class) +@ConditionalOnMissingBean(TbCaffeineCacheConfiguration.class) @ConditionalOnProperty(prefix = "redis.connection", value = "type", havingValue = "cluster") public class TBRedisClusterConfiguration extends TBRedisCacheConfiguration { diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisStandaloneConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisStandaloneConfiguration.java index 482c96d0da..1c83f529ce 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisStandaloneConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisStandaloneConfiguration.java @@ -26,7 +26,7 @@ import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import java.time.Duration; @Configuration -@ConditionalOnMissingBean(CaffeineCacheConfiguration.class) +@ConditionalOnMissingBean(TbCaffeineCacheConfiguration.class) @ConditionalOnProperty(prefix = "redis.connection", value = "type", havingValue = "standalone") public class TBRedisStandaloneConfiguration extends TBRedisCacheConfiguration { diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbCacheTransaction.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbCacheTransaction.java index 47779c251d..210dab139f 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TbCacheTransaction.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbCacheTransaction.java @@ -15,10 +15,6 @@ */ package org.thingsboard.server.cache; -import com.google.common.util.concurrent.ListenableFuture; - -import java.util.concurrent.Executor; - public interface TbCacheTransaction { void putIfAbsent(K key, V value); @@ -27,5 +23,4 @@ public interface TbCacheTransaction { void rollback(); - void rollBackOnFailure(ListenableFuture result, Executor cacheExecutor); } diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineCacheConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java similarity index 87% rename from common/cache/src/main/java/org/thingsboard/server/cache/CaffeineCacheConfiguration.java rename to common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java index ef8ef7d8b9..5dee54a699 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineCacheConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java @@ -19,12 +19,9 @@ import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.RemovalCause; import com.github.benmanes.caffeine.cache.Ticker; import com.github.benmanes.caffeine.cache.Weigher; -import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.caffeine.CaffeineCache; @@ -36,23 +33,20 @@ import org.springframework.context.annotation.Configuration; import java.util.Arrays; import java.util.Collection; import java.util.List; -import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @Configuration @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) -@ConfigurationProperties(prefix = "caffeine") @EnableCaching -@Data @Slf4j -public class CaffeineCacheConfiguration { +public class TbCaffeineCacheConfiguration { - @Value("${cache.type}") - private String test; - - private Map specs; + private final CacheSpecsMap configuration; + public TbCaffeineCacheConfiguration(CacheSpecsMap configuration) { + this.configuration = configuration; + } /** * Transaction aware CaffeineCache implementation with TransactionAwareCacheManagerProxy @@ -60,11 +54,11 @@ public class CaffeineCacheConfiguration { */ @Bean public CacheManager cacheManager() { - log.trace("Initializing cache: {} specs {}", Arrays.toString(RemovalCause.values()), specs); + log.trace("Initializing cache: {} specs {}", Arrays.toString(RemovalCause.values()), configuration.getSpecs()); SimpleCacheManager manager = new SimpleCacheManager(); - if (specs != null) { + if (configuration.getSpecs() != null) { List caches = - specs.entrySet().stream() + configuration.getSpecs().entrySet().stream() .map(entry -> buildCache(entry.getKey(), entry.getValue())) .collect(Collectors.toList()); @@ -100,4 +94,5 @@ public class CaffeineCacheConfiguration { return 1; }; } + } diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java index 208e5856ab..c6cfc4d883 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java @@ -28,6 +28,8 @@ public interface TbTransactionalCache newTransactionForKey(K key); TbCacheTransaction newTransactionForKeys(List keys); diff --git a/common/cache/src/test/java/org/thingsboard/server/cache/CaffeineCacheConfigurationTest.java b/common/cache/src/test/java/org/thingsboard/server/cache/CacheSpecsMapTest.java similarity index 87% rename from common/cache/src/test/java/org/thingsboard/server/cache/CaffeineCacheConfigurationTest.java rename to common/cache/src/test/java/org/thingsboard/server/cache/CacheSpecsMapTest.java index 37bb9c147b..7e65d6a230 100644 --- a/common/cache/src/test/java/org/thingsboard/server/cache/CaffeineCacheConfigurationTest.java +++ b/common/cache/src/test/java/org/thingsboard/server/cache/CacheSpecsMapTest.java @@ -30,16 +30,16 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; @ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = CaffeineCacheConfiguration.class) +@ContextConfiguration(classes = {CacheSpecsMap.class, TbCaffeineCacheConfiguration.class}) @EnableConfigurationProperties @TestPropertySource(properties = { "cache.type=caffeine", - "caffeine.specs.relations.timeToLiveInMinutes=1440", - "caffeine.specs.relations.maxSize=0", - "caffeine.specs.devices.timeToLiveInMinutes=60", - "caffeine.specs.devices.maxSize=100"}) + "cache.specs.relations.timeToLiveInMinutes=1440", + "cache.specs.relations.maxSize=0", + "cache.specs.devices.timeToLiveInMinutes=60", + "cache.specs.devices.maxSize=100"}) @Slf4j -public class CaffeineCacheConfigurationTest { +public class CacheSpecsMapTest { @Autowired CacheManager cacheManager; diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java index 940c96eb00..8d2b8deb1f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java @@ -15,10 +15,15 @@ */ package org.thingsboard.server.dao.attributes; +import lombok.Getter; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.CacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; @@ -27,7 +32,20 @@ import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; @Service("AttributeCache") public class AttributeRedisCache extends RedisTbTransactionalCache { - public AttributeRedisCache(CacheManager cacheManager, RedisConnectionFactory connectionFactory) { - super(cacheManager, CacheConstants.ATTRIBUTES_CACHE, connectionFactory); + public AttributeRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.ATTRIBUTES_CACHE, cacheSpecsMap, connectionFactory, configuration, new RedisSerializer<>() { + + private final RedisSerializer java = RedisSerializer.java(); + + @Override + public byte[] serialize(AttributeKvEntry attributeKvEntry) throws SerializationException { + return java.serialize(attributeKvEntry); + } + + @Override + public AttributeKvEntry deserialize(byte[] bytes) throws SerializationException { + return (AttributeKvEntry) java.deserialize(bytes); + } + }); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java index efa4a30ce4..9f8d9d815d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java @@ -31,11 +31,11 @@ import java.util.Optional; */ public interface AttributesDao { - ListenableFuture> find(TenantId tenantId, EntityId entityId, String attributeType, String attributeKey); + Optional find(TenantId tenantId, EntityId entityId, String attributeType, String attributeKey); - ListenableFuture> find(TenantId tenantId, EntityId entityId, String attributeType, Collection attributeKey); + List find(TenantId tenantId, EntityId entityId, String attributeType, Collection attributeKey); - ListenableFuture> findAll(TenantId tenantId, EntityId entityId, String attributeType); + List findAll(TenantId tenantId, EntityId entityId, String attributeType); ListenableFuture save(TenantId tenantId, EntityId entityId, String attributeType, AttributeKvEntry attribute); diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java index a147159158..97c263ad97 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java @@ -53,20 +53,20 @@ public class BaseAttributesService implements AttributesService { public ListenableFuture> find(TenantId tenantId, EntityId entityId, String scope, String attributeKey) { validate(entityId, scope); Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey); - return attributesDao.find(tenantId, entityId, scope, attributeKey); + return Futures.immediateFuture(attributesDao.find(tenantId, entityId, scope, attributeKey)); } @Override public ListenableFuture> find(TenantId tenantId, EntityId entityId, String scope, Collection attributeKeys) { validate(entityId, scope); attributeKeys.forEach(attributeKey -> Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey)); - return attributesDao.find(tenantId, entityId, scope, attributeKeys); + return Futures.immediateFuture(attributesDao.find(tenantId, entityId, scope, attributeKeys)); } @Override public ListenableFuture> findAll(TenantId tenantId, EntityId entityId, String scope) { validate(entityId, scope); - return attributesDao.findAll(tenantId, entityId, scope); + return Futures.immediateFuture(attributesDao.findAll(tenantId, entityId, scope)); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index 919ff1f4cd..edc259fd13 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -17,17 +17,16 @@ package org.thingsboard.server.dao.attributes; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.cache.Cache; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; import org.thingsboard.server.cache.TbCacheValueWrapper; -import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -48,7 +47,6 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.concurrent.Executor; import java.util.stream.Collectors; import static org.thingsboard.server.dao.attributes.AttributeUtils.validate; @@ -66,7 +64,7 @@ public class CachedAttributesService implements AttributesService { private final DefaultCounter hitCounter; private final DefaultCounter missCounter; private final TbTransactionalCache cache; - private Executor cacheExecutor; + private ListeningExecutorService cacheExecutor; @Value("${cache.type}") private String cacheType; @@ -93,13 +91,13 @@ public class CachedAttributesService implements AttributesService { * - for the local cache type (cache.type="coffeine"): directExecutor (run callback immediately in the same thread) * - for the remote cache: dedicated thread pool for the cache IO calls to unblock any caller thread */ - Executor getExecutor(String cacheType, CacheExecutorService cacheExecutorService) { + ListeningExecutorService getExecutor(String cacheType, CacheExecutorService cacheExecutorService) { if (StringUtils.isEmpty(cacheType) || LOCAL_CACHE_TYPE.equals(cacheType)) { log.info("Going to use directExecutor for the local cache type {}", cacheType); - return MoreExecutors.directExecutor(); + return MoreExecutors.newDirectExecutorService(); } log.info("Going to use cacheExecutorService for the remote cache type {}", cacheType); - return cacheExecutorService; + return cacheExecutorService.executor(); } @@ -116,14 +114,18 @@ public class CachedAttributesService implements AttributesService { return Futures.immediateFuture(Optional.ofNullable(cachedAttributeKvEntry)); } else { missCounter.increment(); - var cacheTransaction = cache.newTransactionForKey(attributeCacheKey); - ListenableFuture> result = attributesDao.find(tenantId, entityId, scope, attributeKey); - cacheTransaction.rollBackOnFailure(result, cacheExecutor); - return Futures.transform(result, foundAttrKvEntry -> { - cacheTransaction.putIfAbsent(attributeCacheKey, foundAttrKvEntry.orElse(null)); - cacheTransaction.commit(); - return foundAttrKvEntry; - }, cacheExecutor); + return cacheExecutor.submit(() -> { + var cacheTransaction = cache.newTransactionForKey(attributeCacheKey); + try { + Optional result = attributesDao.find(tenantId, entityId, scope, attributeKey); + cacheTransaction.putIfAbsent(attributeCacheKey, result.orElse(null)); + cacheTransaction.commit(); + return result; + } catch (Throwable e) { + cacheTransaction.rollback(); + throw e; + } + }); } } @@ -147,23 +149,27 @@ public class CachedAttributesService implements AttributesService { List notFoundKeys = notFoundAttributeKeys.stream().map(k -> new AttributeCacheKey(scope, entityId, k)).collect(Collectors.toList()); - var cacheTransaction = cache.newTransactionForKeys(notFoundKeys); - ListenableFuture> result = attributesDao.find(tenantId, entityId, scope, notFoundAttributeKeys); - return Futures.transform(result, foundInDbAttributes -> { - for (AttributeKvEntry foundInDbAttribute : foundInDbAttributes) { - AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, foundInDbAttribute.getKey()); - cacheTransaction.putIfAbsent(attributeCacheKey, foundInDbAttribute); - notFoundAttributeKeys.remove(foundInDbAttribute.getKey()); - } - for (String key : notFoundAttributeKeys) { - cacheTransaction.putIfAbsent(new AttributeCacheKey(scope, entityId, key), null); + return cacheExecutor.submit(() -> { + var cacheTransaction = cache.newTransactionForKeys(notFoundKeys); + try { + List result = attributesDao.find(tenantId, entityId, scope, notFoundAttributeKeys); + for (AttributeKvEntry foundInDbAttribute : result) { + AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, foundInDbAttribute.getKey()); + cacheTransaction.putIfAbsent(attributeCacheKey, foundInDbAttribute); + notFoundAttributeKeys.remove(foundInDbAttribute.getKey()); + } + for (String key : notFoundAttributeKeys) { + cacheTransaction.putIfAbsent(new AttributeCacheKey(scope, entityId, key), null); + } + List mergedAttributes = new ArrayList<>(cachedAttributes); + mergedAttributes.addAll(result); + cacheTransaction.commit(); + return mergedAttributes; + } catch (Throwable e) { + cacheTransaction.rollback(); + throw e; } - List mergedAttributes = new ArrayList<>(cachedAttributes); - mergedAttributes.addAll(foundInDbAttributes); - cacheTransaction.commit(); - return mergedAttributes; - }, cacheExecutor); - + }); } private Map> findCachedAttributes(EntityId entityId, String scope, Collection attributeKeys) { @@ -183,7 +189,7 @@ public class CachedAttributesService implements AttributesService { @Override public ListenableFuture> findAll(TenantId tenantId, EntityId entityId, String scope) { validate(entityId, scope); - return attributesDao.findAll(tenantId, entityId, scope); + return Futures.immediateFuture(attributesDao.findAll(tenantId, entityId, scope)); } @Override @@ -205,7 +211,7 @@ public class CachedAttributesService implements AttributesService { for (var attribute : attributes) { ListenableFuture future = attributesDao.save(tenantId, entityId, scope, attribute); futures.add(Futures.transform(future, key -> { - cache.evict(new AttributeCacheKey(scope, entityId, key)); + cache.evictOrPut(new AttributeCacheKey(scope, entityId, key), attribute); return key; }, cacheExecutor)); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbCacheTransaction.java b/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbCacheTransaction.java index dc3ad0cf08..dad5da394d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbCacheTransaction.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbCacheTransaction.java @@ -15,14 +15,10 @@ */ package org.thingsboard.server.dao.cache; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.checkerframework.checker.nullness.qual.Nullable; import org.thingsboard.server.cache.TbCacheTransaction; import java.io.Serializable; @@ -30,7 +26,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.UUID; -import java.util.concurrent.Executor; @Slf4j @RequiredArgsConstructor @@ -61,19 +56,5 @@ public class CaffeineTbCacheTransaction void rollBackOnFailure(ListenableFuture future, Executor executor) { - Futures.addCallback(future, new FutureCallback<>() { - @Override - public void onSuccess(@Nullable T result) { - } - - @Override - public void onFailure(Throwable t) { - log.trace("[{}] Rollback transaction due to error", id, t); - rollback(); - } - }, executor); - } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbTransactionalCache.java b/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbTransactionalCache.java index 563c41ef8a..ccca1d483d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbTransactionalCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbTransactionalCache.java @@ -71,6 +71,12 @@ public abstract class CaffeineTbTransactionalCache newTransactionForKey(K key) { return newTransaction(Collections.singletonList(key)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/RedisTbCacheTransaction.java b/dao/src/main/java/org/thingsboard/server/dao/cache/RedisTbCacheTransaction.java new file mode 100644 index 0000000000..bcbd0f1965 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/cache/RedisTbCacheTransaction.java @@ -0,0 +1,70 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.cache; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.data.redis.connection.RedisConnection; +import org.thingsboard.server.cache.TbCacheTransaction; + +import java.io.Serializable; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.Executor; + +@Slf4j +@RequiredArgsConstructor +public class RedisTbCacheTransaction implements TbCacheTransaction { + + private final RedisTbTransactionalCache cache; + private final RedisConnection connection; + + @Override + public void putIfAbsent(K key, V value) { + cache.putIfAbsent(connection, key, value); + } + + @Override + public boolean commit() { + try { + var execResult = connection.exec(); + var result = execResult!= null && execResult.stream().anyMatch(Objects::nonNull); + log.warn("Transaction result: {}", result); + return result; + } finally { + connection.close(); + } + } + + @Override + public void rollback() { + try { + connection.discard(); + } finally { + connection.close(); + } + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/RedisTbTransactionalCache.java b/dao/src/main/java/org/thingsboard/server/dao/cache/RedisTbTransactionalCache.java index dcb16b42e8..7b619032b9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/RedisTbTransactionalCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cache/RedisTbTransactionalCache.java @@ -17,45 +17,155 @@ package org.thingsboard.server.dao.cache; import lombok.Getter; import lombok.RequiredArgsConstructor; -import org.springframework.cache.CacheManager; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.support.NullValue; +import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStringCommands; +import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.thingsboard.server.cache.CacheSpecs; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.cache.TbCacheTransaction; import org.thingsboard.server.cache.TbCacheValueWrapper; import org.thingsboard.server.cache.TbTransactionalCache; import java.io.Serializable; +import java.util.Arrays; import java.util.List; +import java.util.concurrent.TimeUnit; -@RequiredArgsConstructor +@Slf4j public abstract class RedisTbTransactionalCache implements TbTransactionalCache { - private final CacheManager cacheManager; + private static final byte[] BINARY_NULL_VALUE = RedisSerializer.java().serialize(NullValue.INSTANCE); + @Getter private final String cacheName; private final RedisConnectionFactory connectionFactory; + private final RedisSerializer keySerializer = new StringRedisSerializer(); + private final RedisSerializer valueSerializer; + private final Expiration evictExpiration; + private final Expiration cacheTtl; + + public RedisTbTransactionalCache(String cacheName, + CacheSpecsMap cacheSpecsMap, + RedisConnectionFactory connectionFactory, + TBRedisCacheConfiguration configuration, + RedisSerializer valueSerializer) { + this.cacheName = cacheName; + this.connectionFactory = connectionFactory; + this.valueSerializer = valueSerializer; + this.evictExpiration = Expiration.from(configuration.getEvictTtlInMs(), TimeUnit.MILLISECONDS); + CacheSpecs cacheSpecs = cacheSpecsMap.getSpecs().get(cacheName); + if (cacheSpecs == null) { + throw new RuntimeException("Missing cache specs for " + cacheSpecs); + } + this.cacheTtl = Expiration.from(cacheSpecs.getTimeToLiveInMinutes(), TimeUnit.MINUTES); + } @Override public TbCacheValueWrapper get(K key) { - return null; + try (var connection = connectionFactory.getConnection()) { + byte[] rawKey = getRawKey(key); + byte[] rawValue = connection.get(rawKey); + if (rawValue == null) { + return null; + } else if (Arrays.equals(rawValue, BINARY_NULL_VALUE)) { + return SimpleTbCacheValueWrapper.empty(); + } else { + V value = valueSerializer.deserialize(rawValue); + return SimpleTbCacheValueWrapper.wrap(value); + } + } } @Override public void putIfAbsent(K key, V value) { - + try (var connection = connectionFactory.getConnection()) { + putIfAbsent(connection, key, value); + } } @Override public void evict(K key) { + try (var connection = connectionFactory.getConnection()) { + connection.del(getRawKey(key)); + } + } + @Override + public void evictOrPut(K key, V value) { + try (var connection = connectionFactory.getConnection()) { + var rawKey = getRawKey(key); + var records = connection.del(rawKey); + if (records == null || records == 0) { + //We need to put the value in case of Redis, because evict will NOT cancel concurrent transaction used to "get" the missing value from cache. + connection.set(rawKey, getRawValue(value), evictExpiration, RedisStringCommands.SetOption.UPSERT); + } + } } @Override public TbCacheTransaction newTransactionForKey(K key) { - return null; + byte[][] rawKey = new byte[][]{getRawKey(key)}; + RedisConnection connection = watch(rawKey); + return new RedisTbCacheTransaction<>(this, connection); } @Override public TbCacheTransaction newTransactionForKeys(List keys) { - return null; + byte[][] rawKeysList = keys.stream().map(this::getRawKey).toArray(byte[][]::new); + RedisConnection connection = watch(rawKeysList); + return new RedisTbCacheTransaction<>(this, connection); + } + + private RedisConnection watch(byte[][] rawKeysList) { + var connection = connectionFactory.getConnection(); + try { + connection.watch(rawKeysList); + connection.multi(); + } catch (Exception e) { + connection.close(); + } + return connection; } + + private byte[] getRawKey(K key) { + String keyString = cacheName + key.toString(); + byte[] rawKey; + try { + rawKey = keySerializer.serialize(keyString); + } catch (Exception e) { + log.warn("Failed to serialize the cache key: {}", key, e); + throw new RuntimeException(e); + } + if (rawKey == null) { + log.warn("Failed to serialize the cache key: {}", key); + throw new IllegalArgumentException("Failed to serialize the cache key!"); + } + return rawKey; + } + + private byte[] getRawValue(V value) { + if (value == null) { + return BINARY_NULL_VALUE; + } else { + try { + return valueSerializer.serialize(value); + } catch (Exception e) { + log.warn("Failed to serialize the cache value: {}", value, e); + throw new RuntimeException(e); + } + } + } + + public void putIfAbsent(RedisConnection connection, K key, V value) { + byte[] rawKey = getRawKey(key); + byte[] rawValue = getRawValue(value); + connection.set(rawKey, rawValue, cacheTtl, RedisStringCommands.SetOption.SET_IF_ABSENT); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/SimpleTbCacheValueWrapper.java b/dao/src/main/java/org/thingsboard/server/dao/cache/SimpleTbCacheValueWrapper.java index 8e08582477..4d751151e2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/SimpleTbCacheValueWrapper.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cache/SimpleTbCacheValueWrapper.java @@ -30,6 +30,14 @@ public class SimpleTbCacheValueWrapper implements TbCacheValueWrapper { return value; } + public static SimpleTbCacheValueWrapper empty() { + return new SimpleTbCacheValueWrapper<>(null); + } + + public static SimpleTbCacheValueWrapper wrap(T value) { + return new SimpleTbCacheValueWrapper<>(value); + } + @SuppressWarnings("unchecked") public static SimpleTbCacheValueWrapper wrap(Cache.ValueWrapper source) { return source == null ? null : new SimpleTbCacheValueWrapper<>((T) source.get()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java index 0c5fbd5d3a..169ef55fbe 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java @@ -110,33 +110,30 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl } @Override - public ListenableFuture> find(TenantId tenantId, EntityId entityId, String attributeType, String attributeKey) { + public Optional find(TenantId tenantId, EntityId entityId, String attributeType, String attributeKey) { AttributeKvCompositeKey compositeKey = getAttributeKvCompositeKey(entityId, attributeType, attributeKey); - return Futures.immediateFuture( - Optional.ofNullable(DaoUtil.getData(attributeKvRepository.findById(compositeKey)))); + return Optional.ofNullable(DaoUtil.getData(attributeKvRepository.findById(compositeKey))); } @Override - public ListenableFuture> find(TenantId tenantId, EntityId entityId, String attributeType, Collection attributeKeys) { + public List find(TenantId tenantId, EntityId entityId, String attributeType, Collection attributeKeys) { List compositeKeys = attributeKeys .stream() .map(attributeKey -> getAttributeKvCompositeKey(entityId, attributeType, attributeKey)) .collect(Collectors.toList()); - return Futures.immediateFuture( - DaoUtil.convertDataList(Lists.newArrayList(attributeKvRepository.findAllById(compositeKeys)))); + return DaoUtil.convertDataList(Lists.newArrayList(attributeKvRepository.findAllById(compositeKeys))); } @Override - public ListenableFuture> findAll(TenantId tenantId, EntityId entityId, String attributeType) { - return Futures.immediateFuture( - DaoUtil.convertDataList(Lists.newArrayList( + public List findAll(TenantId tenantId, EntityId entityId, String attributeType) { + return DaoUtil.convertDataList(Lists.newArrayList( attributeKvRepository.findAllByEntityTypeAndEntityIdAndAttributeType( entityId.getEntityType(), entityId.getId(), - attributeType)))); + attributeType))); } @Override diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/AttributeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/AttributeServiceTest.java index c67122994c..1017269d5f 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/AttributeServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/AttributeServiceTest.java @@ -19,9 +19,11 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import lombok.extern.slf4j.Slf4j; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; @@ -29,6 +31,7 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.dao.AbstractDaoServiceTest; +import org.thingsboard.server.dao.attributes.AttributeCacheKey; import org.thingsboard.server.dao.attributes.CachedAttributesService; import java.util.ArrayList; @@ -41,11 +44,15 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +@Slf4j public class AttributeServiceTest extends AbstractDaoServiceTest { private static final String OLD_VALUE = "OLD VALUE"; private static final String NEW_VALUE = "NEW VALUE"; + @Autowired + private TbTransactionalCache cache; + @Autowired private CachedAttributesService attributesService; @@ -57,6 +64,24 @@ public class AttributeServiceTest extends AbstractDaoServiceTest { Assert.assertTrue(result.isEmpty()); } + @Test + public void testConcurrentTransaction() throws Exception { + var tenantId = new TenantId(UUID.randomUUID()); + var deviceId = new DeviceId(UUID.randomUUID()); + var scope = DataConstants.SERVER_SCOPE; + var key = "TEST"; + + var attrKey = new AttributeCacheKey(scope, deviceId, "TEST"); + var oldValue = new BaseAttributeKvEntry(System.currentTimeMillis(), new StringDataEntry(key, OLD_VALUE)); + var newValue = new BaseAttributeKvEntry(System.currentTimeMillis(), new StringDataEntry(key, NEW_VALUE)); + + var trx = cache.newTransactionForKey(attrKey); + cache.putIfAbsent(attrKey, newValue); + trx.putIfAbsent(attrKey, oldValue); + Assert.assertFalse(trx.commit()); + Assert.assertEquals(NEW_VALUE, getAttributeValue(tenantId, deviceId, scope, key)); + } + @Test public void testConcurrentFetchAndUpdate() throws Exception { var tenantId = new TenantId(UUID.randomUUID()); @@ -173,6 +198,7 @@ public class AttributeServiceTest extends AbstractDaoServiceTest { Optional entry = attributesService.find(tenantId, deviceId, scope, key).get(10, TimeUnit.SECONDS); return entry.orElseThrow(RuntimeException::new).getStrValue().orElse("Unknown"); } catch (Exception e) { + log.warn("Failed to get attribute", e.getCause()); throw new RuntimeException(e); } } @@ -182,6 +208,7 @@ public class AttributeServiceTest extends AbstractDaoServiceTest { List entry = attributesService.find(tenantId, deviceId, scope, keys).get(10, TimeUnit.SECONDS); return entry.stream().map(e -> e.getStrValue().orElse(null)).collect(Collectors.toList()); } catch (Exception e) { + log.warn("Failed to get attributes", e.getCause()); throw new RuntimeException(e); } } @@ -191,6 +218,7 @@ public class AttributeServiceTest extends AbstractDaoServiceTest { AttributeKvEntry newEntry = new BaseAttributeKvEntry(System.currentTimeMillis(), new StringDataEntry(key, s)); attributesService.save(tenantId, deviceId, scope, Collections.singletonList(newEntry)).get(10, TimeUnit.SECONDS); } catch (Exception e) { + log.warn("Failed to save attribute", e.getCause()); Assert.assertNull(e); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/RedisAttributeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/RedisAttributeServiceTest.java index ce0afb8f60..fb1b5f6854 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/RedisAttributeServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/RedisAttributeServiceTest.java @@ -17,12 +17,16 @@ package org.thingsboard.server.dao.sql.attributes; import lombok.extern.slf4j.Slf4j; import org.junit.ClassRule; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.support.TestPropertySourceUtils; import org.testcontainers.containers.GenericContainer; +import org.thingsboard.server.cache.TbTransactionalCache; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.dao.attributes.AttributeCacheKey; @TestPropertySource(properties = { "cache.type=redis", "redis.connection.type=standalone" @@ -32,7 +36,7 @@ import org.testcontainers.containers.GenericContainer; public class RedisAttributeServiceTest extends AttributeServiceTest implements ApplicationContextInitializer { @ClassRule - public static GenericContainer redis = new GenericContainer("redis:4.0").withExposedPorts(6379); + public static GenericContainer redis = new GenericContainer("redis:latest").withExposedPorts(6379); @Override public void initialize(ConfigurableApplicationContext applicationContext) { @@ -42,4 +46,5 @@ public class RedisAttributeServiceTest extends AttributeServiceTest implements A applicationContext, "redis.standalone.port=" + redis.getMappedPort(6379)); } + } diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index d61bd4eb65..3ebcf5a7ec 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -14,50 +14,50 @@ cache.maximumPoolSize=16 cache.attributes.enabled=true #cache.type=redis -caffeine.specs.relations.timeToLiveInMinutes=1440 -caffeine.specs.relations.maxSize=100000 +cache.specs.relations.timeToLiveInMinutes=1440 +cache.specs.relations.maxSize=100000 -caffeine.specs.deviceCredentials.timeToLiveInMinutes=1440 -caffeine.specs.deviceCredentials.maxSize=100000 +cache.specs.deviceCredentials.timeToLiveInMinutes=1440 +cache.specs.deviceCredentials.maxSize=100000 -caffeine.specs.devices.timeToLiveInMinutes=1440 -caffeine.specs.devices.maxSize=100000 +cache.specs.devices.timeToLiveInMinutes=1440 +cache.specs.devices.maxSize=100000 -caffeine.specs.sessions.timeToLiveInMinutes=1440 -caffeine.specs.sessions.maxSize=100000 +cache.specs.sessions.timeToLiveInMinutes=1440 +cache.specs.sessions.maxSize=100000 -caffeine.specs.assets.timeToLiveInMinutes=1440 -caffeine.specs.assets.maxSize=100000 +cache.specs.assets.timeToLiveInMinutes=1440 +cache.specs.assets.maxSize=100000 -caffeine.specs.entityViews.timeToLiveInMinutes=1440 -caffeine.specs.entityViews.maxSize=100000 +cache.specs.entityViews.timeToLiveInMinutes=1440 +cache.specs.entityViews.maxSize=100000 -caffeine.specs.claimDevices.timeToLiveInMinutes=1440 -caffeine.specs.claimDevices.maxSize=100000 +cache.specs.claimDevices.timeToLiveInMinutes=1440 +cache.specs.claimDevices.maxSize=100000 -caffeine.specs.securitySettings.timeToLiveInMinutes=1440 -caffeine.specs.securitySettings.maxSize=100000 +cache.specs.securitySettings.timeToLiveInMinutes=1440 +cache.specs.securitySettings.maxSize=100000 -caffeine.specs.tenantProfiles.timeToLiveInMinutes=1440 -caffeine.specs.tenantProfiles.maxSize=100000 +cache.specs.tenantProfiles.timeToLiveInMinutes=1440 +cache.specs.tenantProfiles.maxSize=100000 -caffeine.specs.deviceProfiles.timeToLiveInMinutes=1440 -caffeine.specs.deviceProfiles.maxSize=100000 +cache.specs.deviceProfiles.timeToLiveInMinutes=1440 +cache.specs.deviceProfiles.maxSize=100000 -caffeine.specs.attributes.timeToLiveInMinutes=1440 -caffeine.specs.attributes.maxSize=100000 +cache.specs.attributes.timeToLiveInMinutes=1440 +cache.specs.attributes.maxSize=100000 -caffeine.specs.tokensOutdatageTime.timeToLiveInMinutes=1440 -caffeine.specs.tokensOutdatageTime.maxSize=100000 +cache.specs.tokensOutdatageTime.timeToLiveInMinutes=1440 +cache.specs.tokensOutdatageTime.maxSize=100000 -caffeine.specs.otaPackages.timeToLiveInMinutes=1440 -caffeine.specs.otaPackages.maxSize=100000 +cache.specs.otaPackages.timeToLiveInMinutes=1440 +cache.specs.otaPackages.maxSize=100000 -caffeine.specs.otaPackagesData.timeToLiveInMinutes=1440 -caffeine.specs.otaPackagesData.maxSize=100000 +cache.specs.otaPackagesData.timeToLiveInMinutes=1440 +cache.specs.otaPackagesData.maxSize=100000 -caffeine.specs.edges.timeToLiveInMinutes=1440 -caffeine.specs.edges.maxSize=100000 +cache.specs.edges.timeToLiveInMinutes=1440 +cache.specs.edges.maxSize=100000 redis.connection.host=localhost From 7dcb572dfd681d00f8e7ef2e5cc814aef135aa6f Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Fri, 6 May 2022 21:18:48 +0200 Subject: [PATCH 100/276] added queue constraint processing --- .../server/dao/queue/BaseQueueService.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java index c95635a439..a908ab8cdc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.queue; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.TenantProfile; @@ -62,7 +63,16 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ @Override public void deleteQueue(TenantId tenantId, QueueId queueId) { log.trace("Executing deleteQueue, queueId: [{}]", queueId); - queueDao.removeById(tenantId, queueId.getId()); + try { + queueDao.removeById(tenantId, queueId.getId()); + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_default_queue_device_profile")) { + throw new DataValidationException("The queue referenced by the device profiles cannot be deleted!"); + } else { + throw t; + } + } } @Override From e241f73254a372e9733b80c7876458c2891c8365 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Fri, 6 May 2022 21:19:11 +0200 Subject: [PATCH 101/276] fixed queue updates --- .../thingsboard/server/queue/discovery/HashPartitionService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index c5d10d918f..92278f7b4a 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -145,6 +145,7 @@ public class HashPartitionService implements PartitionService { queuesById.put(queue.getQueueId(), queue); partitionTopicsMap.put(queueKey, queueUpdateMsg.getQueueTopic()); partitionSizesMap.put(queueKey, queueUpdateMsg.getPartitions()); + myPartitions.remove(queueKey); } @Override From a6f38902b70d1ccdf1b83c536d0d8cbbaf0bf70a Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Sat, 7 May 2022 12:56:27 +0300 Subject: [PATCH 102/276] Start of the RelationService refactoring --- .../server/cache/TbTransactionalCache.java | 30 ++- .../server/dao/relation/RelationService.java | 2 - .../cache/CaffeineTbTransactionalCache.java | 16 +- .../dao/cache/RedisTbTransactionalCache.java | 13 +- .../dao/relation/AttributeRedisCache.java | 50 +++++ .../dao/relation/BaseRelationService.java | 191 ++++++------------ .../dao/relation/EntityRelationEvent.java | 23 +++ .../server/dao/relation/RelationCacheKey.java | 70 +++++++ .../dao/relation/RelationCacheValue.java | 40 ++++ .../dao/relation/RelationCaffeineCache.java | 34 ++++ .../server/dao/relation/RelationDao.java | 2 +- .../dao/sql/relation/JpaRelationDao.java | 4 +- 12 files changed, 341 insertions(+), 134 deletions(-) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/relation/AttributeRedisCache.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/relation/EntityRelationEvent.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheKey.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheValue.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/relation/RelationCaffeineCache.java diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java index c6cfc4d883..2c48b1134e 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -16,7 +16,10 @@ package org.thingsboard.server.cache; import java.io.Serializable; +import java.util.Collection; import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; public interface TbTransactionalCache { @@ -28,10 +31,35 @@ public interface TbTransactionalCache keys); + void evictOrPut(K key, V value); TbCacheTransaction newTransactionForKey(K key); TbCacheTransaction newTransactionForKeys(List keys); + default R getAndPutInTransaction(K key, Supplier dbCall, Function cacheValueToResult, Function dbValueToCacheValue, boolean cacheNullValue) { + TbCacheValueWrapper cacheValueWrapper = get(key); + if (cacheValueWrapper != null) { + var cacheValue = cacheValueWrapper.get(); + return cacheValue == null ? null : cacheValueToResult.apply(cacheValue); + } + var cacheTransaction = newTransactionForKey(key); + try { + R dbValue = dbCall.get(); + if (dbValue != null || cacheNullValue) { + cacheTransaction.putIfAbsent(key, dbValueToCacheValue.apply(dbValue)); + cacheTransaction.commit(); + return dbValue; + } else { + cacheTransaction.rollback(); + return null; + } + } catch (Throwable e) { + cacheTransaction.rollback(); + throw e; + } + } + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java index 023e757689..dbd641c1c8 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java @@ -38,8 +38,6 @@ public interface RelationService { EntityRelation getRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup); - ListenableFuture getRelationAsync(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup); - boolean saveRelation(TenantId tenantId, EntityRelation relation); ListenableFuture saveRelationAsync(TenantId tenantId, EntityRelation relation); diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbTransactionalCache.java b/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbTransactionalCache.java index ccca1d483d..19859ddea7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbTransactionalCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbTransactionalCache.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -23,6 +23,7 @@ import org.thingsboard.server.cache.TbCacheValueWrapper; import org.thingsboard.server.cache.TbTransactionalCache; import java.io.Serializable; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -71,6 +72,19 @@ public abstract class CaffeineTbTransactionalCache keys) { + lock.lock(); + try { + keys.forEach(key -> { + failAllTransactionsByKey(key); + doEvict(key); + }); + } finally { + lock.unlock(); + } + } + @Override public void evictOrPut(K key, V value) { //No need to put the value in case of Caffeine, because evict will cancel concurrent transaction used to "get" the missing value from cache. diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/RedisTbTransactionalCache.java b/dao/src/main/java/org/thingsboard/server/dao/cache/RedisTbTransactionalCache.java index 7b619032b9..8202a8dbbe 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/RedisTbTransactionalCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cache/RedisTbTransactionalCache.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -34,6 +34,7 @@ import org.thingsboard.server.cache.TbTransactionalCache; import java.io.Serializable; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.concurrent.TimeUnit; @@ -96,6 +97,13 @@ public abstract class RedisTbTransactionalCache keys) { + try (var connection = connectionFactory.getConnection()) { + connection.del(keys.stream().map(this::getRawKey).toArray(byte[][]::new)); + } + } + @Override public void evictOrPut(K key, V value) { try (var connection = connectionFactory.getConnection()) { @@ -117,8 +125,7 @@ public abstract class RedisTbTransactionalCache newTransactionForKeys(List keys) { - byte[][] rawKeysList = keys.stream().map(this::getRawKey).toArray(byte[][]::new); - RedisConnection connection = watch(rawKeysList); + RedisConnection connection = watch(keys.stream().map(this::getRawKey).toArray(byte[][]::new)); return new RedisTbCacheTransaction<>(this, connection); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/AttributeRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/relation/AttributeRedisCache.java new file mode 100644 index 0000000000..383d900d34 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/AttributeRedisCache.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.relation; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.dao.attributes.AttributeCacheKey; +import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("AttributeCache") +public class AttributeRedisCache extends RedisTbTransactionalCache { + + public AttributeRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.ATTRIBUTES_CACHE, cacheSpecsMap, connectionFactory, configuration, new RedisSerializer<>() { + + private final RedisSerializer java = RedisSerializer.java(); + + @Override + public byte[] serialize(AttributeKvEntry attributeKvEntry) throws SerializationException { + return java.serialize(attributeKvEntry); + } + + @Override + public AttributeKvEntry deserialize(byte[] bytes) throws SerializationException { + return (AttributeKvEntry) java.deserialize(bytes); + } + }); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index 36ab0d9f85..3394d4ee8e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -20,23 +20,20 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.Cache; -import org.springframework.cache.CacheManager; -import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; -import org.springframework.cache.annotation.Caching; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.dao.ConcurrencyFailureException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; import org.springframework.util.StringUtils; -import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationInfo; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; @@ -50,8 +47,6 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.ConstraintValidator; import javax.annotation.Nullable; -import java.io.PrintWriter; -import java.io.StringWriter; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -68,17 +63,25 @@ import static org.thingsboard.server.dao.service.Validator.validateId; * Created by ashvayka on 28.04.17. */ @Service +@RequiredArgsConstructor @Slf4j public class BaseRelationService implements RelationService { - @Autowired - private RelationDao relationDao; + private final RelationDao relationDao; + private final EntityService entityService; + private final TbTransactionalCache cache; + private final ApplicationEventPublisher publisher; - @Autowired - private EntityService entityService; - - @Autowired - private CacheManager cacheManager; + @TransactionalEventListener(classes = EntityRelationEvent.class) + public void handleRelationEvictEvent(EntityRelationEvent event) { + List keys = new ArrayList<>(5); + keys.add(new RelationCacheKey(event.getFrom(), event.getTo(), event.getType(), event.getTypeGroup())); + keys.add(new RelationCacheKey(event.getFrom(), null, event.getType(), event.getTypeGroup(), EntitySearchDirection.FROM)); + keys.add(new RelationCacheKey(event.getFrom(), null, null, event.getTypeGroup(), EntitySearchDirection.FROM)); + keys.add(new RelationCacheKey(null, event.getTo(), event.getType(), event.getTypeGroup(), EntitySearchDirection.TO)); + keys.add(new RelationCacheKey(null, event.getTo(), null, event.getTypeGroup(), EntitySearchDirection.TO)); + cache.evict(keys); + } @Override public ListenableFuture checkRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { @@ -87,106 +90,76 @@ public class BaseRelationService implements RelationService { return relationDao.checkRelation(tenantId, from, to, relationType, typeGroup); } - @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType, #typeGroup}") @Transactional(propagation = Propagation.SUPPORTS) @Override public EntityRelation getRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { - return getRelation(tenantId, from, to, relationType, typeGroup); - } - - @Override - public ListenableFuture getRelationAsync(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { log.trace("Executing EntityRelation [{}][{}][{}][{}]", from, to, relationType, typeGroup); validate(from, to, relationType, typeGroup); - return relationDao.getRelation(tenantId, from, to, relationType, typeGroup); + RelationCacheKey cacheKey = new RelationCacheKey(from, to, relationType, typeGroup); + return cache.getAndPutInTransaction(cacheKey, + () -> relationDao.getRelation(tenantId, from, to, relationType, typeGroup), + RelationCacheValue::getRelation, + relations -> RelationCacheValue.builder().relation(relations).build(), false); } - @Caching(evict = { - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup, 'FROM'}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup, 'FROM'}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup, 'TO'}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup, 'TO'}") - }) @Override @Transactional(propagation = Propagation.SUPPORTS) public boolean saveRelation(TenantId tenantId, EntityRelation relation) { log.trace("Executing saveRelation [{}]", relation); validate(relation); - return relationDao.saveRelation(tenantId, relation); + var result = relationDao.saveRelation(tenantId, relation); + publisher.publishEvent(EntityRelationEvent.from(relation)); + return result; } - @Caching(evict = { - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup, 'FROM'}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup, 'FROM'}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup, 'TO'}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup, 'TO'}") - }) @Override public ListenableFuture saveRelationAsync(TenantId tenantId, EntityRelation relation) { log.trace("Executing saveRelationAsync [{}]", relation); validate(relation); - return relationDao.saveRelationAsync(tenantId, relation); + var future = relationDao.saveRelationAsync(tenantId, relation); + future.addListener(() -> handleRelationEvictEvent(EntityRelationEvent.from(relation)), MoreExecutors.directExecutor()); + return future; } - @Caching(evict = { - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup, 'FROM'}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup, 'FROM'}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup, 'TO'}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup, 'TO'}") - }) @Transactional(propagation = Propagation.SUPPORTS) @Override public boolean deleteRelation(TenantId tenantId, EntityRelation relation) { log.trace("Executing deleteRelation [{}]", relation); validate(relation); - return relationDao.deleteRelation(tenantId, relation); + var result = relationDao.deleteRelation(tenantId, relation); + //TODO: evict cache only if the relation was deleted. Note: relationDao.deleteRelation requires improvement. + publisher.publishEvent(EntityRelationEvent.from(relation)); + return result; } - @Caching(evict = { - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup, 'FROM'}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup, 'FROM'}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup, 'TO'}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup, 'TO'}") - }) @Override public ListenableFuture deleteRelationAsync(TenantId tenantId, EntityRelation relation) { log.trace("Executing deleteRelationAsync [{}]", relation); validate(relation); - return relationDao.deleteRelationAsync(tenantId, relation); + var future = relationDao.deleteRelationAsync(tenantId, relation); + future.addListener(() -> handleRelationEvictEvent(EntityRelationEvent.from(relation)), MoreExecutors.directExecutor()); + return future; } - @Caching(evict = { - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType, #typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType, #typeGroup, 'FROM'}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup, 'FROM'}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #typeGroup, 'TO'}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType, #typeGroup, 'TO'}") - }) @Transactional(propagation = Propagation.SUPPORTS) @Override public boolean deleteRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { log.trace("Executing deleteRelation [{}][{}][{}][{}]", from, to, relationType, typeGroup); validate(from, to, relationType, typeGroup); - return relationDao.deleteRelation(tenantId, from, to, relationType, typeGroup); + var result = relationDao.deleteRelation(tenantId, from, to, relationType, typeGroup); + //TODO: evict cache only if the relation was deleted. Note: relationDao.deleteRelation requires improvement. + publisher.publishEvent(new EntityRelationEvent(from, to, relationType, typeGroup)); + return result; } - @Caching(evict = { - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType, #typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType, #typeGroup, 'FROM'}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup, 'FROM'}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #typeGroup, 'TO'}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType, #typeGroup, 'TO'}") - }) @Override public ListenableFuture deleteRelationAsync(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { log.trace("Executing deleteRelationAsync [{}][{}][{}][{}]", from, to, relationType, typeGroup); validate(from, to, relationType, typeGroup); - //TODO: clear cache using transform - return relationDao.deleteRelationAsync(tenantId, from, to, relationType, typeGroup); + var future = relationDao.deleteRelationAsync(tenantId, from, to, relationType, typeGroup); + EntityRelationEvent event = new EntityRelationEvent(from, to, relationType, typeGroup); + future.addListener(() -> handleRelationEvictEvent(event), MoreExecutors.directExecutor()); + return future; } @Transactional(propagation = Propagation.SUPPORTS) @@ -194,7 +167,6 @@ public class BaseRelationService implements RelationService { public void deleteEntityRelations(TenantId tenantId, EntityId entityId) { log.trace("Executing deleteEntityRelations [{}]", entityId); validate(entityId); - final Cache cache = cacheManager.getCache(RELATIONS_CACHE); List inboundRelations = new ArrayList<>(); for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) { inboundRelations.addAll(relationDao.findAllByTo(tenantId, entityId, typeGroup)); @@ -206,11 +178,11 @@ public class BaseRelationService implements RelationService { } for (EntityRelation relation : inboundRelations) { - delete(tenantId, cache, relation, true); + delete(tenantId, relation, true); } for (EntityRelation relation : outboundRelations) { - delete(tenantId, cache, relation, false); + delete(tenantId, relation, false); } relationDao.deleteOutboundRelations(tenantId, entityId); @@ -219,7 +191,6 @@ public class BaseRelationService implements RelationService { @Override public ListenableFuture deleteEntityRelationsAsync(TenantId tenantId, EntityId entityId) { - Cache cache = cacheManager.getCache(RELATIONS_CACHE); log.trace("Executing deleteEntityRelationsAsync [{}]", entityId); validate(entityId); List>> inboundRelationsList = new ArrayList<>(); @@ -238,13 +209,13 @@ public class BaseRelationService implements RelationService { ListenableFuture> inboundDeletions = Futures.transformAsync(inboundRelations, relations -> { - List> results = deleteRelationGroupsAsync(tenantId, relations, cache, true); + List> results = deleteRelationGroupsAsync(tenantId, relations, true); return Futures.allAsList(results); }, MoreExecutors.directExecutor()); ListenableFuture> outboundDeletions = Futures.transformAsync(outboundRelations, relations -> { - List> results = deleteRelationGroupsAsync(tenantId, relations, cache, false); + List> results = deleteRelationGroupsAsync(tenantId, relations, false); return Futures.allAsList(results); }, MoreExecutors.directExecutor()); @@ -256,25 +227,29 @@ public class BaseRelationService implements RelationService { result -> null, MoreExecutors.directExecutor()); } - private List> deleteRelationGroupsAsync(TenantId tenantId, List> relations, Cache cache, boolean deleteFromDb) { + private List> deleteRelationGroupsAsync(TenantId tenantId, List> relations, boolean deleteFromDb) { List> results = new ArrayList<>(); for (List relationList : relations) { - relationList.forEach(relation -> results.add(deleteAsync(tenantId, cache, relation, deleteFromDb))); + relationList.forEach(relation -> results.add(deleteAsync(tenantId, relation, deleteFromDb))); } return results; } - private ListenableFuture deleteAsync(TenantId tenantId, Cache cache, EntityRelation relation, boolean deleteFromDb) { - cacheEviction(relation, cache); + private ListenableFuture deleteAsync(TenantId tenantId, EntityRelation relation, boolean deleteFromDb) { if (deleteFromDb) { - return relationDao.deleteRelationAsync(tenantId, relation); + return Futures.transform(relationDao.deleteRelationAsync(tenantId, relation), + bool -> { + handleRelationEvictEvent(EntityRelationEvent.from(relation)); + return bool; + }, MoreExecutors.directExecutor()); } else { + handleRelationEvictEvent(EntityRelationEvent.from(relation)); return Futures.immediateFuture(false); } } - boolean delete(TenantId tenantId, Cache cache, EntityRelation relation, boolean deleteFromDb) { - cacheEviction(relation, cache); + boolean delete(TenantId tenantId, EntityRelation relation, boolean deleteFromDb) { + publisher.publishEvent(EntityRelationEvent.from(relation)); if (deleteFromDb) { try { return relationDao.deleteRelation(tenantId, relation); @@ -285,49 +260,17 @@ public class BaseRelationService implements RelationService { return false; } - private void cacheEviction(EntityRelation relation, Cache cache) { - List fromToTypeAndTypeGroup = new ArrayList<>(); - fromToTypeAndTypeGroup.add(relation.getFrom()); - fromToTypeAndTypeGroup.add(relation.getTo()); - fromToTypeAndTypeGroup.add(relation.getType()); - fromToTypeAndTypeGroup.add(relation.getTypeGroup()); - cache.evict(fromToTypeAndTypeGroup); - - List fromTypeAndTypeGroup = new ArrayList<>(); - fromTypeAndTypeGroup.add(relation.getFrom()); - fromTypeAndTypeGroup.add(relation.getType()); - fromTypeAndTypeGroup.add(relation.getTypeGroup()); - fromTypeAndTypeGroup.add(EntitySearchDirection.FROM.name()); - cache.evict(fromTypeAndTypeGroup); - - List fromAndTypeGroup = new ArrayList<>(); - fromAndTypeGroup.add(relation.getFrom()); - fromAndTypeGroup.add(relation.getTypeGroup()); - fromAndTypeGroup.add(EntitySearchDirection.FROM.name()); - cache.evict(fromAndTypeGroup); - - List toAndTypeGroup = new ArrayList<>(); - toAndTypeGroup.add(relation.getTo()); - toAndTypeGroup.add(relation.getTypeGroup()); - toAndTypeGroup.add(EntitySearchDirection.TO.name()); - cache.evict(toAndTypeGroup); - - List toTypeAndTypeGroup = new ArrayList<>(); - toTypeAndTypeGroup.add(relation.getTo()); - toTypeAndTypeGroup.add(relation.getType()); - toTypeAndTypeGroup.add(relation.getTypeGroup()); - toTypeAndTypeGroup.add(EntitySearchDirection.TO.name()); - cache.evict(toTypeAndTypeGroup); - } - -// @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup, 'FROM'}") @Transactional(propagation = Propagation.SUPPORTS) @Override public List findByFrom(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup) { validate(from); validateTypeGroup(typeGroup); log.trace("[{}] Find by from: [{}][{}]: ", tenantId, from, typeGroup, new RuntimeException()); - return relationDao.findAllByFrom(tenantId, from, typeGroup); + RelationCacheKey cacheKey = RelationCacheKey.builder().from(from).typeGroup(typeGroup).direction(EntitySearchDirection.FROM).build(); + return cache.getAndPutInTransaction(cacheKey, + () -> relationDao.findAllByFrom(tenantId, from, typeGroup), + RelationCacheValue::getRelations, + relations -> RelationCacheValue.builder().relations(relations).build(), false); } @Override @@ -381,7 +324,7 @@ public class BaseRelationService implements RelationService { }, MoreExecutors.directExecutor()); } -// @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType, #typeGroup, 'FROM'}") + // @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType, #typeGroup, 'FROM'}") @Override public List findByFromAndType(TenantId tenantId, EntityId from, String relationType, RelationTypeGroup typeGroup) { try { diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/EntityRelationEvent.java b/dao/src/main/java/org/thingsboard/server/dao/relation/EntityRelationEvent.java new file mode 100644 index 0000000000..e03f61b074 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/EntityRelationEvent.java @@ -0,0 +1,23 @@ +package org.thingsboard.server.dao.relation; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; + +@RequiredArgsConstructor +public class EntityRelationEvent { + @Getter + private final EntityId from; + @Getter + private final EntityId to; + @Getter + private final String type; + @Getter + private final RelationTypeGroup typeGroup; + + public static EntityRelationEvent from(EntityRelation relation) { + return new EntityRelationEvent(relation.getFrom(), relation.getTo(), relation.getType(), relation.getTypeGroup()); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheKey.java new file mode 100644 index 0000000000..e4c597808e --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheKey.java @@ -0,0 +1,70 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.relation; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; + +import java.io.Serializable; + +@EqualsAndHashCode +@Getter +@RequiredArgsConstructor +@Builder +public class RelationCacheKey implements Serializable { + + private static final long serialVersionUID = 3911151843961657570L; + + private final EntityId from; + private final EntityId to; + private final String type; + private final RelationTypeGroup typeGroup; + private final EntitySearchDirection direction; + + public RelationCacheKey(EntityId from, EntityId to, String type, RelationTypeGroup typeGroup) { + this(from, to, type, typeGroup, null); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + boolean first = true; + first = addElement(sb, first, from); + first = addElement(sb, first, to); + first = addElement(sb, first, typeGroup); + first = addElement(sb, first, type); + first = addElement(sb, first, direction); + return sb.toString(); + } + + private boolean addElement(StringBuilder sb, boolean first, Object element) { + if (element != null) { + if (!first) { + sb.append("_"); + } + sb.append(element); + return false; + } else { + return first; + } + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheValue.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheValue.java new file mode 100644 index 0000000000..348a750b08 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheValue.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.relation; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; + +import java.io.Serializable; +import java.util.List; + +@EqualsAndHashCode +@Getter +@RequiredArgsConstructor +@Builder +public class RelationCacheValue implements Serializable { + + private static final long serialVersionUID = 3911151843961657570L; + + private final EntityRelation relation; + private final List relations; + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCaffeineCache.java new file mode 100644 index 0000000000..b6d20331fc --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCaffeineCache.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.relation; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.dao.attributes.AttributeCacheKey; +import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("RelationCache") +public class RelationCaffeineCache extends CaffeineTbTransactionalCache { + + public RelationCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.RELATIONS_CACHE); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java index df285602ba..380923acf4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java @@ -43,7 +43,7 @@ public interface RelationDao { ListenableFuture checkRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup); - ListenableFuture getRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup); + EntityRelation getRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup); boolean saveRelation(TenantId tenantId, EntityRelation relation); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java index 78a94bfaa3..fd59ada95f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java @@ -107,9 +107,9 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple } @Override - public ListenableFuture getRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { + public EntityRelation getRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { RelationCompositeKey key = getRelationCompositeKey(from, to, relationType, typeGroup); - return service.submit(() -> DaoUtil.getData(relationRepository.findById(key))); + return DaoUtil.getData(relationRepository.findById(key)); } private RelationCompositeKey getRelationCompositeKey(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { From a3b8021ec5c246b17ab12d6876e9b5187dee5a48 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Sat, 7 May 2022 15:07:54 +0300 Subject: [PATCH 103/276] Redis Cache for RelationsService --- .../server/cache/TbTransactionalCache.java | 2 +- .../server/dao/CacheDaoConfig.java | 36 ----- .../thingsboard/server/dao/JpaDaoConfig.java | 2 +- .../server/dao/JpaServiceDaoConfig.java | 38 ----- .../cache/CaffeineTbTransactionalCache.java | 2 +- .../dao/cache/RedisTbCacheTransaction.java | 14 +- .../dao/cache/RedisTbTransactionalCache.java | 3 +- .../dao/customer/CustomerServiceImpl.java | 2 + .../dao/entity/AbstractEntityService.java | 3 + .../dao/relation/BaseRelationService.java | 150 ++++++------------ .../dao/relation/EntityRelationEvent.java | 15 ++ .../server/dao/relation/RelationCacheKey.java | 2 +- .../dao/relation/RelationCacheValue.java | 2 +- .../server/dao/relation/RelationDao.java | 8 +- ...edisCache.java => RelationRedisCache.java} | 14 +- .../dao/sql/relation/JpaRelationDao.java | 28 +--- .../dao/timeseries/BaseTimeseriesService.java | 1 + .../usagerecord/ApiUsageStateServiceImpl.java | 12 +- .../server/dao/AbstractDaoServiceTest.java | 2 +- .../CachedAttributesServiceTest.java | 65 -------- .../dao/service/BaseRelationCacheTest.java | 7 +- .../attributes/RedisAttributeServiceTest.java | 4 +- 22 files changed, 114 insertions(+), 298 deletions(-) delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/CacheDaoConfig.java delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/JpaServiceDaoConfig.java rename dao/src/main/java/org/thingsboard/server/dao/relation/{AttributeRedisCache.java => RelationRedisCache.java} (71%) delete mode 100644 dao/src/test/java/org/thingsboard/server/dao/attributes/CachedAttributesServiceTest.java diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java index 2c48b1134e..fdcc7c4d7a 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/dao/src/main/java/org/thingsboard/server/dao/CacheDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/CacheDaoConfig.java deleted file mode 100644 index f23ae62165..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/CacheDaoConfig.java +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.dao; - -import org.springframework.boot.autoconfigure.domain.EntityScan; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; -import org.springframework.transaction.annotation.EnableTransactionManagement; -import org.thingsboard.server.dao.util.TbAutoConfiguration; - -/** - * @author Valerii Sosliuk - */ -@Configuration -@TbAutoConfiguration -@ComponentScan({"org.thingsboard.server.dao.sql", "org.thingsboard.server.dao.attributes"}) -@EnableJpaRepositories("org.thingsboard.server.dao.sql") -@EntityScan("org.thingsboard.server.dao.model.sql") -@EnableTransactionManagement -public class CacheDaoConfig { - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/JpaDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/JpaDaoConfig.java index de695a9035..dc0b838c43 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/JpaDaoConfig.java +++ b/dao/src/main/java/org/thingsboard/server/dao/JpaDaoConfig.java @@ -27,7 +27,7 @@ import org.thingsboard.server.dao.util.TbAutoConfiguration; */ @Configuration @TbAutoConfiguration -@ComponentScan("org.thingsboard.server.dao.sql") +@ComponentScan({"org.thingsboard.server.dao.sql", "org.thingsboard.server.dao.attributes", "org.thingsboard.server.dao.cache", "org.thingsboard.server.cache"}) @EnableJpaRepositories("org.thingsboard.server.dao.sql") @EntityScan("org.thingsboard.server.dao.model.sql") @EnableTransactionManagement diff --git a/dao/src/main/java/org/thingsboard/server/dao/JpaServiceDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/JpaServiceDaoConfig.java deleted file mode 100644 index 46d067fc4a..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/JpaServiceDaoConfig.java +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.dao; - -import org.springframework.boot.autoconfigure.domain.EntityScan; -import org.springframework.cache.annotation.EnableCaching; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; -import org.springframework.transaction.annotation.EnableTransactionManagement; -import org.thingsboard.server.dao.util.TbAutoConfiguration; - -/** - * @author Valerii Sosliuk - */ -@Configuration -@EnableCaching -@TbAutoConfiguration -@ComponentScan({"org.thingsboard.server.dao.sql", "org.thingsboard.server.dao.attributes", "org.thingsboard.server.dao.cache", "org.thingsboard.server.cache"}) -@EnableJpaRepositories("org.thingsboard.server.dao.sql") -@EntityScan("org.thingsboard.server.dao.model.sql") -@EnableTransactionManagement -public class JpaServiceDaoConfig { - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbTransactionalCache.java b/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbTransactionalCache.java index 19859ddea7..59e5b52f28 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbTransactionalCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbTransactionalCache.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/RedisTbCacheTransaction.java b/dao/src/main/java/org/thingsboard/server/dao/cache/RedisTbCacheTransaction.java index bcbd0f1965..0fc6f349d3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/RedisTbCacheTransaction.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cache/RedisTbCacheTransaction.java @@ -15,24 +15,13 @@ */ package org.thingsboard.server.dao.cache; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import lombok.Getter; import lombok.RequiredArgsConstructor; -import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.data.redis.connection.RedisConnection; import org.thingsboard.server.cache.TbCacheTransaction; import java.io.Serializable; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; import java.util.Objects; -import java.util.UUID; -import java.util.concurrent.Executor; @Slf4j @RequiredArgsConstructor @@ -50,8 +39,7 @@ public class RedisTbCacheTransaction cache; private final ApplicationEventPublisher publisher; + private final JpaExecutorService executor; + + public BaseRelationService(RelationDao relationDao, @Lazy EntityService entityService, + TbTransactionalCache cache, + ApplicationEventPublisher publisher, JpaExecutorService executor) { + this.relationDao = relationDao; + this.entityService = entityService; + this.cache = cache; + this.publisher = publisher; + this.executor = executor; + } @TransactionalEventListener(classes = EntityRelationEvent.class) public void handleRelationEvictEvent(EntityRelationEvent event) { @@ -195,14 +201,14 @@ public class BaseRelationService implements RelationService { validate(entityId); List>> inboundRelationsList = new ArrayList<>(); for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) { - inboundRelationsList.add(relationDao.findAllByToAsync(tenantId, entityId, typeGroup)); + inboundRelationsList.add(executor.submit(() -> relationDao.findAllByTo(tenantId, entityId, typeGroup))); } ListenableFuture>> inboundRelations = Futures.allAsList(inboundRelationsList); List>> outboundRelationsList = new ArrayList<>(); for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) { - outboundRelationsList.add(relationDao.findAllByFromAsync(tenantId, entityId, typeGroup)); + outboundRelationsList.add(executor.submit(() -> relationDao.findAllByFrom(tenantId, entityId, typeGroup))); } ListenableFuture>> outboundRelations = Futures.allAsList(outboundRelationsList); @@ -265,7 +271,6 @@ public class BaseRelationService implements RelationService { public List findByFrom(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup) { validate(from); validateTypeGroup(typeGroup); - log.trace("[{}] Find by from: [{}][{}]: ", tenantId, from, typeGroup, new RuntimeException()); RelationCacheKey cacheKey = RelationCacheKey.builder().from(from).typeGroup(typeGroup).direction(EntitySearchDirection.FROM).build(); return cache.getAndPutInTransaction(cacheKey, () -> relationDao.findAllByFrom(tenantId, from, typeGroup), @@ -279,30 +284,13 @@ public class BaseRelationService implements RelationService { validate(from); validateTypeGroup(typeGroup); - List fromAndTypeGroup = new ArrayList<>(); - fromAndTypeGroup.add(from); - fromAndTypeGroup.add(typeGroup); - fromAndTypeGroup.add(EntitySearchDirection.FROM.name()); + var cacheValue = cache.get(RelationCacheKey.builder().from(from).typeGroup(typeGroup).direction(EntitySearchDirection.FROM).build()); - Cache cache = cacheManager.getCache(RELATIONS_CACHE); - @SuppressWarnings("unchecked") - List fromCache = cache.get(fromAndTypeGroup, List.class); - if (fromCache != null) { - return Futures.immediateFuture(fromCache); + if (cacheValue != null && cacheValue.get() != null) { + return Futures.immediateFuture(cacheValue.get().getRelations()); } else { - ListenableFuture> relationsFuture = relationDao.findAllByFromAsync(tenantId, from, typeGroup); - Futures.addCallback(relationsFuture, - new FutureCallback<>() { - @Override - public void onSuccess(@Nullable List result) { - cache.putIfAbsent(fromAndTypeGroup, result); - } - - @Override - public void onFailure(Throwable t) { - } - }, MoreExecutors.directExecutor()); - return relationsFuture; + //Disabled cache put for the async requests due to limitations of the cache implementation (Redis lib does not support thread-safe transactions) + return executor.submit(() -> findByFrom(tenantId, from, typeGroup)); } } @@ -311,7 +299,7 @@ public class BaseRelationService implements RelationService { log.trace("Executing findInfoByFrom [{}][{}]", from, typeGroup); validate(from); validateTypeGroup(typeGroup); - ListenableFuture> relations = relationDao.findAllByFromAsync(tenantId, from, typeGroup); + ListenableFuture> relations = executor.submit(() -> relationDao.findAllByFrom(tenantId, from, typeGroup)); return Futures.transformAsync(relations, relations1 -> { List> futures = new ArrayList<>(); @@ -324,15 +312,14 @@ public class BaseRelationService implements RelationService { }, MoreExecutors.directExecutor()); } - // @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType, #typeGroup, 'FROM'}") + @Transactional(propagation = Propagation.SUPPORTS) @Override public List findByFromAndType(TenantId tenantId, EntityId from, String relationType, RelationTypeGroup typeGroup) { - try { - //TODO refactor - return findByFromAndTypeAsync(tenantId, from, relationType, typeGroup).get(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } + RelationCacheKey cacheKey = RelationCacheKey.builder().from(from).type(relationType).typeGroup(typeGroup).direction(EntitySearchDirection.FROM).build(); + return cache.getAndPutInTransaction(cacheKey, + () -> relationDao.findAllByFromAndType(tenantId, from, relationType, typeGroup), + RelationCacheValue::getRelations, + relations -> RelationCacheValue.builder().relations(relations).build(), false); } @Override @@ -341,49 +328,28 @@ public class BaseRelationService implements RelationService { validate(from); validateType(relationType); validateTypeGroup(typeGroup); - return relationDao.findAllByFromAndType(tenantId, from, relationType, typeGroup); + return executor.submit(() -> findByFromAndType(tenantId, from, relationType, typeGroup)); } - @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#to, #typeGroup, 'TO'}") @Transactional(propagation = Propagation.SUPPORTS) @Override public List findByTo(TenantId tenantId, EntityId to, RelationTypeGroup typeGroup) { validate(to); validateTypeGroup(typeGroup); - return relationDao.findAllByTo(tenantId, to, typeGroup); + RelationCacheKey cacheKey = RelationCacheKey.builder().to(to).typeGroup(typeGroup).direction(EntitySearchDirection.TO).build(); + return cache.getAndPutInTransaction(cacheKey, + () -> relationDao.findAllByTo(tenantId, to, typeGroup), + RelationCacheValue::getRelations, + relations -> RelationCacheValue.builder().relations(relations).build(), false); + } @Override public ListenableFuture> findByToAsync(TenantId tenantId, EntityId to, RelationTypeGroup typeGroup) { - log.trace("Executing findByTo [{}][{}]", to, typeGroup); + log.trace("Executing findByToAsync [{}][{}]", to, typeGroup); validate(to); validateTypeGroup(typeGroup); - - List toAndTypeGroup = new ArrayList<>(); - toAndTypeGroup.add(to); - toAndTypeGroup.add(typeGroup); - toAndTypeGroup.add(EntitySearchDirection.TO.name()); - - Cache cache = cacheManager.getCache(RELATIONS_CACHE); - @SuppressWarnings("unchecked") - List fromCache = cache.get(toAndTypeGroup, List.class); - if (fromCache != null) { - return Futures.immediateFuture(fromCache); - } else { - ListenableFuture> relationsFuture = relationDao.findAllByToAsync(tenantId, to, typeGroup); - Futures.addCallback(relationsFuture, - new FutureCallback>() { - @Override - public void onSuccess(@Nullable List result) { - cache.putIfAbsent(toAndTypeGroup, result); - } - - @Override - public void onFailure(Throwable t) { - } - }, MoreExecutors.directExecutor()); - return relationsFuture; - } + return executor.submit(() -> findByTo(tenantId, to, typeGroup)); } @Override @@ -391,7 +357,7 @@ public class BaseRelationService implements RelationService { log.trace("Executing findInfoByTo [{}][{}]", to, typeGroup); validate(to); validateTypeGroup(typeGroup); - ListenableFuture> relations = relationDao.findAllByToAsync(tenantId, to, typeGroup); + ListenableFuture> relations = findByToAsync(tenantId, to, typeGroup); return Futures.transformAsync(relations, relations1 -> { List> futures = new ArrayList<>(); @@ -415,24 +381,27 @@ public class BaseRelationService implements RelationService { }, MoreExecutors.directExecutor()); } - @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType, #typeGroup, 'TO'}") @Override public List findByToAndType(TenantId tenantId, EntityId to, String relationType, RelationTypeGroup typeGroup) { - try { - //TODO refactor - return findByToAndTypeAsync(tenantId, to, relationType, typeGroup).get(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } + log.trace("Executing findByToAndType [{}][{}][{}]", to, relationType, typeGroup); + validate(to); + validateType(relationType); + validateTypeGroup(typeGroup); + RelationCacheKey cacheKey = RelationCacheKey.builder().to(to).type(relationType).typeGroup(typeGroup).direction(EntitySearchDirection.TO).build(); + return cache.getAndPutInTransaction(cacheKey, + () -> relationDao.findAllByToAndType(tenantId, to, relationType, typeGroup), + RelationCacheValue::getRelations, + relations -> RelationCacheValue.builder().relations(relations).build(), false); + } @Override public ListenableFuture> findByToAndTypeAsync(TenantId tenantId, EntityId to, String relationType, RelationTypeGroup typeGroup) { - log.trace("Executing findByToAndType [{}][{}][{}]", to, relationType, typeGroup); + log.trace("Executing findByToAndTypeAsync [{}][{}][{}]", to, relationType, typeGroup); validate(to); validateType(relationType); validateTypeGroup(typeGroup); - return relationDao.findAllByToAndType(tenantId, to, relationType, typeGroup); + return executor.submit(() -> findByToAndType(tenantId, to, relationType, typeGroup)); } @Override @@ -495,7 +464,6 @@ public class BaseRelationService implements RelationService { @Override public void removeRelations(TenantId tenantId, EntityId entityId) { log.trace("removeRelations {}", entityId); - Cache cache = cacheManager.getCache(RELATIONS_CACHE); List relations = new ArrayList<>(); for (RelationTypeGroup relationTypeGroup : RelationTypeGroup.values()) { @@ -505,7 +473,6 @@ public class BaseRelationService implements RelationService { for (EntityRelation relation : relations) { deleteRelation(tenantId, relation); - cacheEviction(relation, cache); } } @@ -553,21 +520,6 @@ public class BaseRelationService implements RelationService { } } - private Function, Boolean> getListToBooleanFunction() { - return new Function, Boolean>() { - @Nullable - @Override - public Boolean apply(@Nullable List results) { - for (Boolean result : results) { - if (result == null || !result) { - return false; - } - } - return true; - } - }; - } - private boolean matchFilters(List filters, EntityRelation relation, EntitySearchDirection direction) { for (RelationEntityTypeFilter filter : filters) { if (match(filter, relation, direction)) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/EntityRelationEvent.java b/dao/src/main/java/org/thingsboard/server/dao/relation/EntityRelationEvent.java index e03f61b074..ff2b5c5743 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/EntityRelationEvent.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/EntityRelationEvent.java @@ -1,3 +1,18 @@ +/** + * Copyright © 2016-2022 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. + */ package org.thingsboard.server.dao.relation; import lombok.Getter; diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheKey.java index e4c597808e..0df5e3e1c4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheKey.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheValue.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheValue.java index 348a750b08..b2cbac681f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheValue.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheValue.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java index 380923acf4..e15f4f3d69 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java @@ -29,17 +29,13 @@ import java.util.List; */ public interface RelationDao { - ListenableFuture> findAllByFromAsync(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup); - List findAllByFrom(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup); - ListenableFuture> findAllByFromAndType(TenantId tenantId, EntityId from, String relationType, RelationTypeGroup typeGroup); - - ListenableFuture> findAllByToAsync(TenantId tenantId, EntityId to, RelationTypeGroup typeGroup); + List findAllByFromAndType(TenantId tenantId, EntityId from, String relationType, RelationTypeGroup typeGroup); List findAllByTo(TenantId tenantId, EntityId to, RelationTypeGroup typeGroup); - ListenableFuture> findAllByToAndType(TenantId tenantId, EntityId to, String relationType, RelationTypeGroup typeGroup); + List findAllByToAndType(TenantId tenantId, EntityId to, String relationType, RelationTypeGroup typeGroup); ListenableFuture checkRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup); diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/AttributeRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationRedisCache.java similarity index 71% rename from dao/src/main/java/org/thingsboard/server/dao/relation/AttributeRedisCache.java rename to dao/src/main/java/org/thingsboard/server/dao/relation/RelationRedisCache.java index 383d900d34..0e0dc261fb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/AttributeRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationRedisCache.java @@ -28,22 +28,22 @@ import org.thingsboard.server.dao.attributes.AttributeCacheKey; import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") -@Service("AttributeCache") -public class AttributeRedisCache extends RedisTbTransactionalCache { +@Service("RelationCache") +public class RelationRedisCache extends RedisTbTransactionalCache { - public AttributeRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { - super(CacheConstants.ATTRIBUTES_CACHE, cacheSpecsMap, connectionFactory, configuration, new RedisSerializer<>() { + public RelationRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.RELATIONS_CACHE, cacheSpecsMap, connectionFactory, configuration, new RedisSerializer<>() { private final RedisSerializer java = RedisSerializer.java(); @Override - public byte[] serialize(AttributeKvEntry attributeKvEntry) throws SerializationException { + public byte[] serialize(RelationCacheValue attributeKvEntry) throws SerializationException { return java.serialize(attributeKvEntry); } @Override - public AttributeKvEntry deserialize(byte[] bytes) throws SerializationException { - return (AttributeKvEntry) java.deserialize(bytes); + public RelationCacheValue deserialize(byte[] bytes) throws SerializationException { + return (RelationCacheValue) java.deserialize(bytes); } }); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java index fd59ada95f..71e36fcae1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java @@ -19,11 +19,9 @@ import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.ConcurrencyFailureException; -import org.springframework.data.domain.PageRequest; import org.springframework.dao.DataAccessException; -import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -35,8 +33,6 @@ import org.thingsboard.server.dao.model.sql.RelationEntity; import org.thingsboard.server.dao.relation.RelationDao; import org.thingsboard.server.dao.sql.JpaAbstractDaoListeningExecutorService; -import javax.persistence.criteria.Predicate; -import java.util.ArrayList; import java.util.List; /** @@ -52,11 +48,6 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple @Autowired private RelationInsertRepository relationInsertRepository; - @Override - public ListenableFuture> findAllByFromAsync(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup) { - return service.submit(() -> findAllByFrom(tenantId, from, typeGroup)); - } - @Override public List findAllByFrom(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup) { return DaoUtil.convertDataList( @@ -67,18 +58,13 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple } @Override - public ListenableFuture> findAllByFromAndType(TenantId tenantId, EntityId from, String relationType, RelationTypeGroup typeGroup) { - return service.submit(() -> DaoUtil.convertDataList( + public List findAllByFromAndType(TenantId tenantId, EntityId from, String relationType, RelationTypeGroup typeGroup) { + return DaoUtil.convertDataList( relationRepository.findAllByFromIdAndFromTypeAndRelationTypeAndRelationTypeGroup( from.getId(), from.getEntityType().name(), relationType, - typeGroup.name()))); - } - - @Override - public ListenableFuture> findAllByToAsync(TenantId tenantId, EntityId to, RelationTypeGroup typeGroup) { - return service.submit(() -> findAllByTo(tenantId, to, typeGroup)); + typeGroup.name())); } @Override @@ -91,13 +77,13 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple } @Override - public ListenableFuture> findAllByToAndType(TenantId tenantId, EntityId to, String relationType, RelationTypeGroup typeGroup) { - return service.submit(() -> DaoUtil.convertDataList( + public List findAllByToAndType(TenantId tenantId, EntityId to, String relationType, RelationTypeGroup typeGroup) { + return DaoUtil.convertDataList( relationRepository.findAllByToIdAndToTypeAndRelationTypeAndRelationTypeGroup( to.getId(), to.getEntityType().name(), relationType, - typeGroup.name()))); + typeGroup.name())); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java index 0d71e3f716..11fab052b5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java @@ -24,6 +24,7 @@ import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; diff --git a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java index 2193305837..a1a3e7ae1f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.usagerecord; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.ApiFeature; import org.thingsboard.server.common.data.ApiUsageRecordKey; @@ -47,7 +48,6 @@ import static org.thingsboard.server.dao.service.Validator.validateId; @Service @Slf4j -@AllArgsConstructor public class ApiUsageStateServiceImpl extends AbstractEntityService implements ApiUsageStateService { public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; @@ -57,6 +57,16 @@ public class ApiUsageStateServiceImpl extends AbstractEntityService implements A private final TimeseriesService tsService; private final DataValidator apiUsageStateValidator; + public ApiUsageStateServiceImpl(ApiUsageStateDao apiUsageStateDao, TenantProfileDao tenantProfileDao, + TenantDao tenantDao, @Lazy TimeseriesService tsService, + DataValidator apiUsageStateValidator) { + this.apiUsageStateDao = apiUsageStateDao; + this.tenantProfileDao = tenantProfileDao; + this.tenantDao = tenantDao; + this.tsService = tsService; + this.apiUsageStateValidator = apiUsageStateValidator; + } + @Override public void deleteApiUsageStateByTenantId(TenantId tenantId) { log.trace("Executing deleteUsageRecordsByTenantId [{}]", tenantId); diff --git a/dao/src/test/java/org/thingsboard/server/dao/AbstractDaoServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/AbstractDaoServiceTest.java index 25c423eddb..c69926f6e9 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/AbstractDaoServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/AbstractDaoServiceTest.java @@ -27,7 +27,7 @@ import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.service.DaoSqlTest; @RunWith(SpringRunner.class) -@ContextConfiguration(classes = {JpaServiceDaoConfig.class, PsqlTsDaoConfig.class, PsqlTsLatestDaoConfig.class, SqlTimeseriesDaoConfig.class}) +@ContextConfiguration(classes = {JpaDaoConfig.class, PsqlTsDaoConfig.class, PsqlTsLatestDaoConfig.class, SqlTimeseriesDaoConfig.class}) @DaoSqlTest @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, diff --git a/dao/src/test/java/org/thingsboard/server/dao/attributes/CachedAttributesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/attributes/CachedAttributesServiceTest.java deleted file mode 100644 index 4f699912e3..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/attributes/CachedAttributesServiceTest.java +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.dao.attributes; - -import com.google.common.util.concurrent.MoreExecutors; -import org.junit.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.thingsboard.server.dao.AbstractJpaDaoTest; -import org.thingsboard.server.dao.alarm.AlarmDao; -import org.thingsboard.server.dao.cache.CacheExecutorService; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.willCallRealMethod; -import static org.mockito.Mockito.mock; - -public class CachedAttributesServiceTest extends AbstractJpaDaoTest { - - public static final String REDIS = "redis"; - - @Test - public void givenLocalCacheTypeName_whenEquals_thenOK() { - assertThat(CachedAttributesService.LOCAL_CACHE_TYPE, is("caffeine")); - } - - @Test - public void givenCacheType_whenGetExecutor_thenDirectExecutor() { - CachedAttributesService cachedAttributesService = mock(CachedAttributesService.class); - CacheExecutorService cacheExecutorService = mock(CacheExecutorService.class); - willCallRealMethod().given(cachedAttributesService).getExecutor(any(), any()); - - assertThat(cachedAttributesService.getExecutor(null, cacheExecutorService), is(MoreExecutors.directExecutor())); - - assertThat(cachedAttributesService.getExecutor("", cacheExecutorService), is(MoreExecutors.directExecutor())); - - assertThat(cachedAttributesService.getExecutor(CachedAttributesService.LOCAL_CACHE_TYPE, cacheExecutorService), is(MoreExecutors.directExecutor())); - - } - - @Test - public void givenCacheType_whenGetExecutor_thenReturnCacheExecutorService() { - CachedAttributesService cachedAttributesService = mock(CachedAttributesService.class); - CacheExecutorService cacheExecutorService = mock(CacheExecutorService.class); - willCallRealMethod().given(cachedAttributesService).getExecutor(any(String.class), any(CacheExecutorService.class)); - - assertThat(cachedAttributesService.getExecutor(REDIS, cacheExecutorService), is(cacheExecutorService)); - - assertThat(cachedAttributesService.getExecutor("unknownCacheType", cacheExecutorService), is(cacheExecutorService)); - } - -} \ No newline at end of file diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationCacheTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationCacheTest.java index 8023133059..9901438bdc 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationCacheTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationCacheTest.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.service; -import com.google.common.util.concurrent.Futures; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -75,7 +74,7 @@ public abstract class BaseRelationCacheTest extends AbstractServiceTest { @Test public void testFindRelationByFrom_Cached() throws ExecutionException, InterruptedException { when(relationDao.getRelation(SYSTEM_TENANT_ID, ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON)) - .thenReturn(Futures.immediateFuture(new EntityRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE))); + .thenReturn(new EntityRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE)); relationService.getRelation(SYSTEM_TENANT_ID, ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON); relationService.getRelation(SYSTEM_TENANT_ID, ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON); @@ -86,7 +85,7 @@ public abstract class BaseRelationCacheTest extends AbstractServiceTest { @Test public void testDeleteRelations_EvictsCache() { when(relationDao.getRelation(SYSTEM_TENANT_ID, ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON)) - .thenReturn(Futures.immediateFuture(new EntityRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE))); + .thenReturn(new EntityRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE)); relationService.getRelation(SYSTEM_TENANT_ID, ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON); relationService.getRelation(SYSTEM_TENANT_ID, ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON); @@ -98,7 +97,7 @@ public abstract class BaseRelationCacheTest extends AbstractServiceTest { relationService.getRelation(SYSTEM_TENANT_ID, ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON); relationService.getRelation(SYSTEM_TENANT_ID, ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON); - verify(relationDao, times(2)).getRelation(SYSTEM_TENANT_ID, ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON); + verify(relationDao, times(1)).getRelation(SYSTEM_TENANT_ID, ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/RedisAttributeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/RedisAttributeServiceTest.java index fb1b5f6854..f545c5f3bc 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/RedisAttributeServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/RedisAttributeServiceTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.sql.attributes; import lombok.extern.slf4j.Slf4j; import org.junit.ClassRule; +import org.junit.Ignore; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; @@ -33,10 +34,11 @@ import org.thingsboard.server.dao.attributes.AttributeCacheKey; }) @ContextConfiguration(initializers = RedisAttributeServiceTest.class) @Slf4j +@Ignore public class RedisAttributeServiceTest extends AttributeServiceTest implements ApplicationContextInitializer { @ClassRule - public static GenericContainer redis = new GenericContainer("redis:latest").withExposedPorts(6379); + public static GenericContainer redis = new GenericContainer("redis:6.0").withExposedPorts(6379); @Override public void initialize(ConfigurableApplicationContext applicationContext) { From 14e16cc6f3e936f42daab73a39f105cb5fe76caa Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Mon, 9 May 2022 10:54:22 +0300 Subject: [PATCH 104/276] Redis Tests Suite to run all the SQL tests with Redis instead of Caffeine --- .../server/dao/AbstractDaoServiceTest.java | 2 + ...sTestSuite.java => RedisSqlTestSuite.java} | 11 +- .../attributes/BaseAttributesServiceTest.java | 189 +++++++++++++++ .../sql/attributes/AttributeServiceTest.java | 227 ------------------ .../attributes/RedisAttributeServiceTest.java | 52 ---- 5 files changed, 197 insertions(+), 284 deletions(-) rename dao/src/test/java/org/thingsboard/server/dao/{RedisTestSuite.java => RedisSqlTestSuite.java} (86%) delete mode 100644 dao/src/test/java/org/thingsboard/server/dao/sql/attributes/AttributeServiceTest.java delete mode 100644 dao/src/test/java/org/thingsboard/server/dao/sql/attributes/RedisAttributeServiceTest.java diff --git a/dao/src/test/java/org/thingsboard/server/dao/AbstractDaoServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/AbstractDaoServiceTest.java index c69926f6e9..d54933a19d 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/AbstractDaoServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/AbstractDaoServiceTest.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao; import org.junit.runner.RunWith; import org.mockito.Answers; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit4.SpringRunner; @@ -29,6 +30,7 @@ import org.thingsboard.server.dao.service.DaoSqlTest; @RunWith(SpringRunner.class) @ContextConfiguration(classes = {JpaDaoConfig.class, PsqlTsDaoConfig.class, PsqlTsLatestDaoConfig.class, SqlTimeseriesDaoConfig.class}) @DaoSqlTest +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class}) diff --git a/dao/src/test/java/org/thingsboard/server/dao/RedisTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/RedisSqlTestSuite.java similarity index 86% rename from dao/src/test/java/org/thingsboard/server/dao/RedisTestSuite.java rename to dao/src/test/java/org/thingsboard/server/dao/RedisSqlTestSuite.java index 21838ebc2c..01871e5d3b 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/RedisTestSuite.java +++ b/dao/src/test/java/org/thingsboard/server/dao/RedisSqlTestSuite.java @@ -25,12 +25,13 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.support.TestPropertySourceUtils; import org.testcontainers.containers.GenericContainer; -@ContextConfiguration(initializers = RedisTestSuite.class) +@ContextConfiguration(initializers = RedisSqlTestSuite.class) @RunWith(ClasspathSuite.class) -@ClassnameFilters({ - "org.thingsboard.server.dao.sql.attributes.*ServiceTest", -}) -public class RedisTestSuite implements ApplicationContextInitializer { +@ClassnameFilters( + //All the same tests using redis instead of caffeine. + "org.thingsboard.server.dao.service.*ServiceSqlTest" +) +public class RedisSqlTestSuite implements ApplicationContextInitializer { @ClassRule public static GenericContainer redis = new GenericContainer("redis:4.0").withExposedPorts(6379); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java index 043db133b6..4abd33ab1a 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java @@ -16,28 +16,49 @@ package org.thingsboard.server.dao.service.attributes; import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import lombok.extern.slf4j.Slf4j; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.dao.attributes.AttributeCacheKey; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.service.AbstractServiceTest; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; /** * @author Andrew Shvayka */ +@Slf4j public abstract class BaseAttributesServiceTest extends AbstractServiceTest { + private static final String OLD_VALUE = "OLD VALUE"; + private static final String NEW_VALUE = "NEW VALUE"; + + @Autowired + private TbTransactionalCache cache; + @Autowired private AttributesService attributesService; @@ -100,4 +121,172 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest { Assert.assertEquals(attrBNew, saved.get(1)); } + @Test + public void testDummyRequestWithEmptyResult() throws Exception { + var future = attributesService.find(new TenantId(UUID.randomUUID()), new DeviceId(UUID.randomUUID()), DataConstants.SERVER_SCOPE, "TEST"); + Assert.assertNotNull(future); + var result = future.get(10, TimeUnit.SECONDS); + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void testConcurrentTransaction() throws Exception { + var tenantId = new TenantId(UUID.randomUUID()); + var deviceId = new DeviceId(UUID.randomUUID()); + var scope = DataConstants.SERVER_SCOPE; + var key = "TEST"; + + var attrKey = new AttributeCacheKey(scope, deviceId, "TEST"); + var oldValue = new BaseAttributeKvEntry(System.currentTimeMillis(), new StringDataEntry(key, OLD_VALUE)); + var newValue = new BaseAttributeKvEntry(System.currentTimeMillis(), new StringDataEntry(key, NEW_VALUE)); + + var trx = cache.newTransactionForKey(attrKey); + cache.putIfAbsent(attrKey, newValue); + trx.putIfAbsent(attrKey, oldValue); + Assert.assertFalse(trx.commit()); + Assert.assertEquals(NEW_VALUE, getAttributeValue(tenantId, deviceId, scope, key)); + } + + @Test + public void testConcurrentFetchAndUpdate() throws Exception { + var tenantId = new TenantId(UUID.randomUUID()); + ListeningExecutorService pool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(2)); + try { + for (int i = 0; i < 100; i++) { + var deviceId = new DeviceId(UUID.randomUUID()); + testConcurrentFetchAndUpdate(tenantId, deviceId, pool); + } + } finally { + pool.shutdownNow(); + } + } + + @Test + public void testConcurrentFetchAndUpdateMulti() throws Exception { + var tenantId = new TenantId(UUID.randomUUID()); + ListeningExecutorService pool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(2)); + try { + for (int i = 0; i < 100; i++) { + var deviceId = new DeviceId(UUID.randomUUID()); + testConcurrentFetchAndUpdateMulti(tenantId, deviceId, pool); + } + } finally { + pool.shutdownNow(); + } + } + + @Test + public void testFetchAndUpdateEmpty() throws Exception { + var tenantId = new TenantId(UUID.randomUUID()); + var deviceId = new DeviceId(UUID.randomUUID()); + var scope = DataConstants.SERVER_SCOPE; + var key = "TEST"; + + Optional emptyValue = attributesService.find(tenantId, deviceId, scope, key).get(10, TimeUnit.SECONDS); + Assert.assertTrue(emptyValue.isEmpty()); + + saveAttribute(tenantId, deviceId, scope, key, NEW_VALUE); + Assert.assertEquals(NEW_VALUE, getAttributeValue(tenantId, deviceId, scope, key)); + } + + @Test + public void testFetchAndUpdateMulti() throws Exception { + var tenantId = new TenantId(UUID.randomUUID()); + var deviceId = new DeviceId(UUID.randomUUID()); + var scope = DataConstants.SERVER_SCOPE; + var key1 = "TEST1"; + var key2 = "TEST2"; + + var value = getAttributeValues(tenantId, deviceId, scope, Arrays.asList(key1, key2)); + Assert.assertTrue(value.isEmpty()); + + saveAttribute(tenantId, deviceId, scope, key1, OLD_VALUE); + + value = getAttributeValues(tenantId, deviceId, scope, Arrays.asList(key1, key2)); + Assert.assertEquals(1, value.size()); + Assert.assertEquals(OLD_VALUE, value.get(0)); + + saveAttribute(tenantId, deviceId, scope, key2, NEW_VALUE); + + value = getAttributeValues(tenantId, deviceId, scope, Arrays.asList(key1, key2)); + Assert.assertEquals(2, value.size()); + Assert.assertTrue(value.contains(OLD_VALUE)); + Assert.assertTrue(value.contains(NEW_VALUE)); + + saveAttribute(tenantId, deviceId, scope, key1, NEW_VALUE); + + value = getAttributeValues(tenantId, deviceId, scope, Arrays.asList(key1, key2)); + Assert.assertEquals(2, value.size()); + Assert.assertEquals(NEW_VALUE, value.get(0)); + Assert.assertEquals(NEW_VALUE, value.get(1)); + } + + private void testConcurrentFetchAndUpdate(TenantId tenantId, DeviceId deviceId, ListeningExecutorService pool) throws Exception { + var scope = DataConstants.SERVER_SCOPE; + var key = "TEST"; + saveAttribute(tenantId, deviceId, scope, key, OLD_VALUE); + List> futures = new ArrayList<>(); + futures.add(pool.submit(() -> { + var value = getAttributeValue(tenantId, deviceId, scope, key); + Assert.assertTrue(value.equals(OLD_VALUE) || value.equals(NEW_VALUE)); + })); + futures.add(pool.submit(() -> saveAttribute(tenantId, deviceId, scope, key, NEW_VALUE))); + Futures.allAsList(futures).get(10, TimeUnit.SECONDS); + Assert.assertEquals(NEW_VALUE, getAttributeValue(tenantId, deviceId, scope, key)); + } + + private void testConcurrentFetchAndUpdateMulti(TenantId tenantId, DeviceId deviceId, ListeningExecutorService pool) throws Exception { + var scope = DataConstants.SERVER_SCOPE; + var key1 = "TEST1"; + var key2 = "TEST2"; + saveAttribute(tenantId, deviceId, scope, key1, OLD_VALUE); + saveAttribute(tenantId, deviceId, scope, key2, OLD_VALUE); + List> futures = new ArrayList<>(); + futures.add(pool.submit(() -> { + var value = getAttributeValues(tenantId, deviceId, scope, Arrays.asList(key1, key2)); + Assert.assertEquals(2, value.size()); + Assert.assertTrue(value.contains(OLD_VALUE) || value.contains(NEW_VALUE)); + })); + futures.add(pool.submit(() -> { + saveAttribute(tenantId, deviceId, scope, key1, NEW_VALUE); + saveAttribute(tenantId, deviceId, scope, key2, NEW_VALUE); + })); + Futures.allAsList(futures).get(10, TimeUnit.SECONDS); + var newResult = getAttributeValues(tenantId, deviceId, scope, Arrays.asList(key1, key2)); + Assert.assertEquals(2, newResult.size()); + Assert.assertEquals(NEW_VALUE, newResult.get(0)); + Assert.assertEquals(NEW_VALUE, newResult.get(1)); + } + + private String getAttributeValue(TenantId tenantId, DeviceId deviceId, String scope, String key) { + try { + Optional entry = attributesService.find(tenantId, deviceId, scope, key).get(10, TimeUnit.SECONDS); + return entry.orElseThrow(RuntimeException::new).getStrValue().orElse("Unknown"); + } catch (Exception e) { + log.warn("Failed to get attribute", e.getCause()); + throw new RuntimeException(e); + } + } + + private List getAttributeValues(TenantId tenantId, DeviceId deviceId, String scope, List keys) { + try { + List entry = attributesService.find(tenantId, deviceId, scope, keys).get(10, TimeUnit.SECONDS); + return entry.stream().map(e -> e.getStrValue().orElse(null)).collect(Collectors.toList()); + } catch (Exception e) { + log.warn("Failed to get attributes", e.getCause()); + throw new RuntimeException(e); + } + } + + private void saveAttribute(TenantId tenantId, DeviceId deviceId, String scope, String key, String s) { + try { + AttributeKvEntry newEntry = new BaseAttributeKvEntry(System.currentTimeMillis(), new StringDataEntry(key, s)); + attributesService.save(tenantId, deviceId, scope, Collections.singletonList(newEntry)).get(10, TimeUnit.SECONDS); + } catch (Exception e) { + log.warn("Failed to save attribute", e.getCause()); + Assert.assertNull(e); + } + } + + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/AttributeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/AttributeServiceTest.java deleted file mode 100644 index 1017269d5f..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/AttributeServiceTest.java +++ /dev/null @@ -1,227 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.dao.sql.attributes; - -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; -import lombok.extern.slf4j.Slf4j; -import org.junit.Assert; -import org.junit.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.thingsboard.server.cache.TbTransactionalCache; -import org.thingsboard.server.common.data.DataConstants; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; -import org.thingsboard.server.common.data.kv.StringDataEntry; -import org.thingsboard.server.dao.AbstractDaoServiceTest; -import org.thingsboard.server.dao.attributes.AttributeCacheKey; -import org.thingsboard.server.dao.attributes.CachedAttributesService; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -@Slf4j -public class AttributeServiceTest extends AbstractDaoServiceTest { - - private static final String OLD_VALUE = "OLD VALUE"; - private static final String NEW_VALUE = "NEW VALUE"; - - @Autowired - private TbTransactionalCache cache; - - @Autowired - private CachedAttributesService attributesService; - - @Test - public void testDummyRequestWithEmptyResult() throws Exception { - var future = attributesService.find(new TenantId(UUID.randomUUID()), new DeviceId(UUID.randomUUID()), DataConstants.SERVER_SCOPE, "TEST"); - Assert.assertNotNull(future); - var result = future.get(10, TimeUnit.SECONDS); - Assert.assertTrue(result.isEmpty()); - } - - @Test - public void testConcurrentTransaction() throws Exception { - var tenantId = new TenantId(UUID.randomUUID()); - var deviceId = new DeviceId(UUID.randomUUID()); - var scope = DataConstants.SERVER_SCOPE; - var key = "TEST"; - - var attrKey = new AttributeCacheKey(scope, deviceId, "TEST"); - var oldValue = new BaseAttributeKvEntry(System.currentTimeMillis(), new StringDataEntry(key, OLD_VALUE)); - var newValue = new BaseAttributeKvEntry(System.currentTimeMillis(), new StringDataEntry(key, NEW_VALUE)); - - var trx = cache.newTransactionForKey(attrKey); - cache.putIfAbsent(attrKey, newValue); - trx.putIfAbsent(attrKey, oldValue); - Assert.assertFalse(trx.commit()); - Assert.assertEquals(NEW_VALUE, getAttributeValue(tenantId, deviceId, scope, key)); - } - - @Test - public void testConcurrentFetchAndUpdate() throws Exception { - var tenantId = new TenantId(UUID.randomUUID()); - ListeningExecutorService pool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(2)); - try { - for (int i = 0; i < 100; i++) { - var deviceId = new DeviceId(UUID.randomUUID()); - testConcurrentFetchAndUpdate(tenantId, deviceId, pool); - } - } finally { - pool.shutdownNow(); - } - } - - @Test - public void testConcurrentFetchAndUpdateMulti() throws Exception { - var tenantId = new TenantId(UUID.randomUUID()); - ListeningExecutorService pool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(2)); - try { - for (int i = 0; i < 100; i++) { - var deviceId = new DeviceId(UUID.randomUUID()); - testConcurrentFetchAndUpdateMulti(tenantId, deviceId, pool); - } - } finally { - pool.shutdownNow(); - } - } - - @Test - public void testFetchAndUpdateEmpty() throws Exception { - var tenantId = new TenantId(UUID.randomUUID()); - var deviceId = new DeviceId(UUID.randomUUID()); - var scope = DataConstants.SERVER_SCOPE; - var key = "TEST"; - - Optional emptyValue = attributesService.find(tenantId, deviceId, scope, key).get(10, TimeUnit.SECONDS); - Assert.assertTrue(emptyValue.isEmpty()); - - saveAttribute(tenantId, deviceId, scope, key, NEW_VALUE); - Assert.assertEquals(NEW_VALUE, getAttributeValue(tenantId, deviceId, scope, key)); - } - - @Test - public void testFetchAndUpdateMulti() throws Exception { - var tenantId = new TenantId(UUID.randomUUID()); - var deviceId = new DeviceId(UUID.randomUUID()); - var scope = DataConstants.SERVER_SCOPE; - var key1 = "TEST1"; - var key2 = "TEST2"; - - var value = getAttributeValues(tenantId, deviceId, scope, Arrays.asList(key1, key2)); - Assert.assertTrue(value.isEmpty()); - - saveAttribute(tenantId, deviceId, scope, key1, OLD_VALUE); - - value = getAttributeValues(tenantId, deviceId, scope, Arrays.asList(key1, key2)); - Assert.assertEquals(1, value.size()); - Assert.assertEquals(OLD_VALUE, value.get(0)); - - saveAttribute(tenantId, deviceId, scope, key2, NEW_VALUE); - - value = getAttributeValues(tenantId, deviceId, scope, Arrays.asList(key1, key2)); - Assert.assertEquals(2, value.size()); - Assert.assertTrue(value.contains(OLD_VALUE)); - Assert.assertTrue(value.contains(NEW_VALUE)); - - saveAttribute(tenantId, deviceId, scope, key1, NEW_VALUE); - - value = getAttributeValues(tenantId, deviceId, scope, Arrays.asList(key1, key2)); - Assert.assertEquals(2, value.size()); - Assert.assertEquals(NEW_VALUE, value.get(0)); - Assert.assertEquals(NEW_VALUE, value.get(1)); - } - - private void testConcurrentFetchAndUpdate(TenantId tenantId, DeviceId deviceId, ListeningExecutorService pool) throws Exception { - var scope = DataConstants.SERVER_SCOPE; - var key = "TEST"; - saveAttribute(tenantId, deviceId, scope, key, OLD_VALUE); - List> futures = new ArrayList<>(); - futures.add(pool.submit(() -> { - var value = getAttributeValue(tenantId, deviceId, scope, key); - Assert.assertTrue(value.equals(OLD_VALUE) || value.equals(NEW_VALUE)); - })); - futures.add(pool.submit(() -> saveAttribute(tenantId, deviceId, scope, key, NEW_VALUE))); - Futures.allAsList(futures).get(10, TimeUnit.SECONDS); - Assert.assertEquals(NEW_VALUE, getAttributeValue(tenantId, deviceId, scope, key)); - } - - private void testConcurrentFetchAndUpdateMulti(TenantId tenantId, DeviceId deviceId, ListeningExecutorService pool) throws Exception { - var scope = DataConstants.SERVER_SCOPE; - var key1 = "TEST1"; - var key2 = "TEST2"; - saveAttribute(tenantId, deviceId, scope, key1, OLD_VALUE); - saveAttribute(tenantId, deviceId, scope, key2, OLD_VALUE); - List> futures = new ArrayList<>(); - futures.add(pool.submit(() -> { - var value = getAttributeValues(tenantId, deviceId, scope, Arrays.asList(key1, key2)); - Assert.assertEquals(2, value.size()); - Assert.assertTrue(value.contains(OLD_VALUE) || value.contains(NEW_VALUE)); - })); - futures.add(pool.submit(() -> { - saveAttribute(tenantId, deviceId, scope, key1, NEW_VALUE); - saveAttribute(tenantId, deviceId, scope, key2, NEW_VALUE); - })); - Futures.allAsList(futures).get(10, TimeUnit.SECONDS); - var newResult = getAttributeValues(tenantId, deviceId, scope, Arrays.asList(key1, key2)); - Assert.assertEquals(2, newResult.size()); - Assert.assertEquals(NEW_VALUE, newResult.get(0)); - Assert.assertEquals(NEW_VALUE, newResult.get(1)); - } - - private String getAttributeValue(TenantId tenantId, DeviceId deviceId, String scope, String key) { - try { - Optional entry = attributesService.find(tenantId, deviceId, scope, key).get(10, TimeUnit.SECONDS); - return entry.orElseThrow(RuntimeException::new).getStrValue().orElse("Unknown"); - } catch (Exception e) { - log.warn("Failed to get attribute", e.getCause()); - throw new RuntimeException(e); - } - } - - private List getAttributeValues(TenantId tenantId, DeviceId deviceId, String scope, List keys) { - try { - List entry = attributesService.find(tenantId, deviceId, scope, keys).get(10, TimeUnit.SECONDS); - return entry.stream().map(e -> e.getStrValue().orElse(null)).collect(Collectors.toList()); - } catch (Exception e) { - log.warn("Failed to get attributes", e.getCause()); - throw new RuntimeException(e); - } - } - - private void saveAttribute(TenantId tenantId, DeviceId deviceId, String scope, String key, String s) { - try { - AttributeKvEntry newEntry = new BaseAttributeKvEntry(System.currentTimeMillis(), new StringDataEntry(key, s)); - attributesService.save(tenantId, deviceId, scope, Collections.singletonList(newEntry)).get(10, TimeUnit.SECONDS); - } catch (Exception e) { - log.warn("Failed to save attribute", e.getCause()); - Assert.assertNull(e); - } - } - - -} diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/RedisAttributeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/RedisAttributeServiceTest.java deleted file mode 100644 index f545c5f3bc..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/RedisAttributeServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.dao.sql.attributes; - -import lombok.extern.slf4j.Slf4j; -import org.junit.ClassRule; -import org.junit.Ignore; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContextInitializer; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.support.TestPropertySourceUtils; -import org.testcontainers.containers.GenericContainer; -import org.thingsboard.server.cache.TbTransactionalCache; -import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.dao.attributes.AttributeCacheKey; - -@TestPropertySource(properties = { - "cache.type=redis", "redis.connection.type=standalone" -}) -@ContextConfiguration(initializers = RedisAttributeServiceTest.class) -@Slf4j -@Ignore -public class RedisAttributeServiceTest extends AttributeServiceTest implements ApplicationContextInitializer { - - @ClassRule - public static GenericContainer redis = new GenericContainer("redis:6.0").withExposedPorts(6379); - - @Override - public void initialize(ConfigurableApplicationContext applicationContext) { - TestPropertySourceUtils.addInlinedPropertiesToEnvironment( - applicationContext, "redis.standalone.host=localhost"); - TestPropertySourceUtils.addInlinedPropertiesToEnvironment( - applicationContext, "redis.standalone.port=" + redis.getMappedPort(6379)); - } - - -} From d3c23c914d2a29a1b27fdfde8b7632aa3d1e312c Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Mon, 9 May 2022 11:45:21 +0300 Subject: [PATCH 105/276] Ensure at least one 2FA account config is default --- .../controller/TwoFaConfigController.java | 17 +++-------------- .../mfa/config/DefaultTwoFaConfigManager.java | 17 +++++++++++++---- .../auth/mfa/config/TwoFaConfigManager.java | 1 - .../controller/TwoFactorAuthConfigTest.java | 3 +++ 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFaConfigController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFaConfigController.java index 60541b1ce4..d974189bab 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFaConfigController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFaConfigController.java @@ -50,7 +50,6 @@ import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; @@ -191,20 +190,10 @@ public class TwoFaConfigController extends BaseController { @RequestBody TwoFaAccountConfigUpdateRequest updateRequest) throws ThingsboardException { SecurityUser user = getCurrentUser(); - AccountTwoFaSettings settings = twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user.getId()) - .orElseThrow(() -> new IllegalArgumentException("No 2FA config found")); - Map configs = settings.getConfigs(); - - TwoFaAccountConfig accountConfig; - if ((accountConfig = configs.get(providerType)) == null) { - throw new IllegalArgumentException("Config for " + providerType + " 2FA provider not found"); - } - if (updateRequest.isUseByDefault()) { - configs.values().forEach(config -> config.setUseByDefault(false)); - } + TwoFaAccountConfig accountConfig = twoFaConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType) + .orElseThrow(() -> new IllegalArgumentException("Config for " + providerType + " 2FA provider not found")); accountConfig.setUseByDefault(updateRequest.isUseByDefault()); - - return twoFaConfigManager.saveAccountTwoFaSettings(user.getTenantId(), user.getId(), settings); + return twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig); } @ApiOperation(value = "Delete 2FA account config (deleteTwoFaAccountConfig)", notes = diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java index 75ca7b1e45..a1c56e4cd7 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java @@ -38,11 +38,10 @@ import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.user.UserAuthSettingsDao; import java.util.Collections; +import java.util.Comparator; import java.util.LinkedHashMap; -import java.util.Map; import java.util.Optional; import java.util.concurrent.ExecutionException; -import java.util.function.Consumer; @Service @RequiredArgsConstructor @@ -68,8 +67,7 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { }); } - @Override - public AccountTwoFaSettings saveAccountTwoFaSettings(TenantId tenantId, UserId userId, AccountTwoFaSettings settings) { + protected AccountTwoFaSettings saveAccountTwoFaSettings(TenantId tenantId, UserId userId, AccountTwoFaSettings settings) { UserAuthSettings userAuthSettings = Optional.ofNullable(userAuthSettingsDao.findByUserId(userId)) .orElseGet(() -> { UserAuthSettings newUserAuthSettings = new UserAuthSettings(); @@ -99,7 +97,13 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { newSettings.setConfigs(new LinkedHashMap<>()); return newSettings; }); + if (accountConfig.isUseByDefault()) { + settings.getConfigs().values().forEach(config -> config.setUseByDefault(false)); + } settings.getConfigs().put(accountConfig.getProviderType(), accountConfig); + if (settings.getConfigs().values().stream().noneMatch(TwoFaAccountConfig::isUseByDefault)) { + settings.getConfigs().values().stream().findFirst().ifPresent(config -> config.setUseByDefault(true)); + } return saveAccountTwoFaSettings(tenantId, userId, settings); } @@ -108,6 +112,11 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { AccountTwoFaSettings settings = getAccountTwoFaSettings(tenantId, userId) .orElseThrow(() -> new IllegalArgumentException("2FA not configured")); settings.getConfigs().remove(providerType); + if (!settings.getConfigs().isEmpty() && settings.getConfigs().values().stream().noneMatch(TwoFaAccountConfig::isUseByDefault)) { + settings.getConfigs().values().stream() + .min(Comparator.comparing(TwoFaAccountConfig::getProviderType)) + .ifPresent(config -> config.setUseByDefault(true)); + } return saveAccountTwoFaSettings(tenantId, userId, settings); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java index f1a4590572..212c4697bf 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java @@ -28,7 +28,6 @@ public interface TwoFaConfigManager { Optional getAccountTwoFaSettings(TenantId tenantId, UserId userId); - AccountTwoFaSettings saveAccountTwoFaSettings(TenantId tenantId, UserId userId, AccountTwoFaSettings settings); Optional getTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType); diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java index 3438ed19f8..f9dc02e8d3 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java @@ -303,6 +303,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { loginTenantAdmin(); TotpTwoFaAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig); + generatedTotpTwoFaAccountConfig.setUseByDefault(true); String secret = UriComponentsBuilder.fromUriString(generatedTotpTwoFaAccountConfig.getAuthUrl()).build() .getQueryParams().getFirst("secret"); @@ -421,6 +422,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig(); smsTwoFaAccountConfig.setPhoneNumber("+38051889445"); + smsTwoFaAccountConfig.setUseByDefault(true); ArgumentCaptor verificationCodeCaptor = ArgumentCaptor.forClass(String.class); doPost("/api/2fa/account/config/submit", smsTwoFaAccountConfig).andExpect(status().isOk()); @@ -459,6 +461,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { SmsTwoFaAccountConfig initialSmsTwoFaAccountConfig = new SmsTwoFaAccountConfig(); initialSmsTwoFaAccountConfig.setPhoneNumber("+38051889445"); + initialSmsTwoFaAccountConfig.setUseByDefault(true); ArgumentCaptor verificationCodeCaptor = ArgumentCaptor.forClass(String.class); doPost("/api/2fa/account/config/submit", initialSmsTwoFaAccountConfig).andExpect(status().isOk()); From eb6458738d3444b4ea363377f02ee3f36efb3959 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Mon, 9 May 2022 17:13:23 +0300 Subject: [PATCH 106/276] Device Cache Impl --- ...actRuleEngineLifecycleIntegrationTest.java | 8 +- ...RuleEngineLifecycleSqlIntegrationTest.java | 1 - .../server/cache/CacheKeyUtil.java | 41 +++++ .../server/cache/TbTransactionalCache.java | 22 +++ .../server/dao/edge/EdgeEventService.java | 4 + .../dao/cache/EntitiesCacheManager.java | 5 - .../dao/cache/EntitiesCacheManagerImpl.java | 17 -- .../server/dao/device/DeviceCacheKey.java | 51 ++++++ .../dao/device/DeviceCaffeineCache.java | 33 ++++ .../server/dao/device/DeviceEvent.java | 30 ++++ .../server/dao/device/DeviceRedisCache.java | 49 ++++++ .../server/dao/device/DeviceServiceImpl.java | 150 ++++++++++-------- .../entity/AbstractCachedEntityService.java | 43 +++++ .../dao/entity/AbstractEntityService.java | 3 + .../dao/relation/BaseRelationService.java | 46 ++++-- .../server/dao/relation/RelationCacheKey.java | 22 +-- .../server/dao/service/DataValidator.java | 11 +- .../validator/AdminSettingsDataValidator.java | 3 +- .../service/validator/AssetDataValidator.java | 3 +- ...ientRegistrationTemplateDataValidator.java | 3 +- .../validator/CustomerDataValidator.java | 8 +- .../DeviceCredentialsDataValidator.java | 3 +- .../validator/DeviceDataValidator.java | 10 +- .../validator/DeviceProfileDataValidator.java | 3 +- .../service/validator/EdgeDataValidator.java | 3 +- .../validator/EntityViewDataValidator.java | 15 +- .../validator/OtaPackageDataValidator.java | 3 +- .../OtaPackageInfoDataValidator.java | 3 +- .../validator/TenantDataValidator.java | 3 +- .../validator/TenantProfileDataValidator.java | 3 +- .../validator/WidgetTypeDataValidator.java | 5 +- .../validator/WidgetsBundleDataValidator.java | 3 +- .../dao/service/BaseEdgeEventServiceTest.java | 29 +++- .../dao/service/BaseRelationCacheTest.java | 3 +- .../service/event/BaseEventServiceTest.java | 46 +++--- 35 files changed, 500 insertions(+), 185 deletions(-) create mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/CacheKeyUtil.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/device/DeviceCacheKey.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/device/DeviceCaffeineCache.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/device/DeviceEvent.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/device/DeviceRedisCache.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/entity/AbstractCachedEntityService.java diff --git a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java index 0f08e618c3..5d86e6a2a5 100644 --- a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java @@ -20,9 +20,13 @@ import org.awaitility.Awaitility; import org.junit.After; import org.junit.Assert; import org.junit.Before; +import org.junit.ClassRule; import org.junit.Test; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.support.TestPropertySourceUtils; +import org.testcontainers.containers.GenericContainer; import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.data.DataConstants; @@ -110,7 +114,7 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac .atMost(TIMEOUT, TimeUnit.SECONDS) .until(() -> { List debugEvents = getEvents(tenantId, ruleChainFinal.getFirstRuleNodeId(), DataConstants.LC_EVENT, 1000) - .getData().stream().filter(e-> { + .getData().stream().filter(e -> { var body = e.getBody(); return body.has("event") && body.get("event").asText().equals("STARTED") && body.has("success") && body.get("success").asBoolean(); @@ -128,7 +132,7 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac log.warn("before update attr"); attributesService.save(device.getTenantId(), device.getId(), DataConstants.SERVER_SCOPE, - Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry("serverAttributeKey", "serverAttributeValue"), System.currentTimeMillis()))) + Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry("serverAttributeKey", "serverAttributeValue"), System.currentTimeMillis()))) .get(TIMEOUT, TimeUnit.SECONDS); log.warn("attr updated"); TbMsgCallback tbMsgCallback = Mockito.mock(TbMsgCallback.class); diff --git a/application/src/test/java/org/thingsboard/server/rules/lifecycle/sql/RuleEngineLifecycleSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/lifecycle/sql/RuleEngineLifecycleSqlIntegrationTest.java index de7bab4e4f..02205248fe 100644 --- a/application/src/test/java/org/thingsboard/server/rules/lifecycle/sql/RuleEngineLifecycleSqlIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/rules/lifecycle/sql/RuleEngineLifecycleSqlIntegrationTest.java @@ -16,7 +16,6 @@ package org.thingsboard.server.rules.lifecycle.sql; import org.thingsboard.server.dao.service.DaoSqlTest; -import org.thingsboard.server.rules.flow.AbstractRuleEngineFlowIntegrationTest; import org.thingsboard.server.rules.lifecycle.AbstractRuleEngineLifecycleIntegrationTest; /** diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/CacheKeyUtil.java b/common/cache/src/main/java/org/thingsboard/server/cache/CacheKeyUtil.java new file mode 100644 index 0000000000..1c1a096f59 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/CacheKeyUtil.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.cache; + +public class CacheKeyUtil { + + public static String toString(Object... keyElements) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (Object keyElement : keyElements) { + first = addElement(sb, first, keyElement); + } + return sb.toString(); + } + + private static boolean addElement(StringBuilder sb, boolean first, Object element) { + if (element != null) { + if (!first) { + sb.append("_"); + } + sb.append(element); + return false; + } else { + return first; + } + } + +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java index fdcc7c4d7a..cc43f0c537 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java @@ -39,6 +39,28 @@ public interface TbTransactionalCache newTransactionForKeys(List keys); + default V getAndPutInTransaction(K key, Supplier dbCall, boolean cacheNullValue) { + TbCacheValueWrapper cacheValueWrapper = get(key); + if (cacheValueWrapper != null) { + return cacheValueWrapper.get(); + } + var cacheTransaction = newTransactionForKey(key); + try { + V dbValue = dbCall.get(); + if (dbValue != null || cacheNullValue) { + cacheTransaction.putIfAbsent(key, dbValue); + cacheTransaction.commit(); + return dbValue; + } else { + cacheTransaction.rollback(); + return null; + } + } catch (Throwable e) { + cacheTransaction.rollback(); + throw e; + } + } + default R getAndPutInTransaction(K key, Supplier dbCall, Function cacheValueToResult, Function dbValueToCacheValue, boolean cacheNullValue) { TbCacheValueWrapper cacheValueWrapper = get(key); if (cacheValueWrapper != null) { diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeEventService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeEventService.java index e9950447db..f6dae741b1 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeEventService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeEventService.java @@ -28,5 +28,9 @@ public interface EdgeEventService { PageData findEdgeEvents(TenantId tenantId, EdgeId edgeId, TimePageLink pageLink, boolean withTsUpdate); + /** + * Executes stored procedure to cleanup old edge events. + * @param ttl the ttl for edge events in seconds + */ void cleanupEvents(long ttl); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/EntitiesCacheManager.java b/dao/src/main/java/org/thingsboard/server/dao/cache/EntitiesCacheManager.java index 19842eacc4..06dcd51e61 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/EntitiesCacheManager.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cache/EntitiesCacheManager.java @@ -15,15 +15,10 @@ */ package org.thingsboard.server.dao.cache; -import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; public interface EntitiesCacheManager { - void removeDeviceFromCacheByName(TenantId tenantId, String name); - - void removeDeviceFromCacheById(TenantId tenantId, DeviceId deviceId); - void removeAssetFromCacheByName(TenantId tenantId, String name); void removeEdgeFromCacheByName(TenantId tenantId, String name); diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/EntitiesCacheManagerImpl.java b/dao/src/main/java/org/thingsboard/server/dao/cache/EntitiesCacheManagerImpl.java index 2a28f07424..d24bd98822 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/EntitiesCacheManagerImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cache/EntitiesCacheManagerImpl.java @@ -19,13 +19,11 @@ import lombok.AllArgsConstructor; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import java.util.Arrays; import static org.thingsboard.server.common.data.CacheConstants.ASSET_CACHE; -import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CACHE; import static org.thingsboard.server.common.data.CacheConstants.EDGE_CACHE; @Component @@ -34,21 +32,6 @@ public class EntitiesCacheManagerImpl implements EntitiesCacheManager { private final CacheManager cacheManager; - @Override - public void removeDeviceFromCacheByName(TenantId tenantId, String name) { - Cache cache = cacheManager.getCache(DEVICE_CACHE); - cache.evict(Arrays.asList(tenantId, name)); - } - - @Override - public void removeDeviceFromCacheById(TenantId tenantId, DeviceId deviceId) { - if (deviceId == null) { - return; - } - Cache cache = cacheManager.getCache(DEVICE_CACHE); - cache.evict(Arrays.asList(tenantId, deviceId)); - } - @Override public void removeAssetFromCacheByName(TenantId tenantId, String name) { Cache cache = cacheManager.getCache(ASSET_CACHE); diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCacheKey.java new file mode 100644 index 0000000000..574a5c2617 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCacheKey.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.device; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.thingsboard.server.cache.CacheKeyUtil; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.io.Serializable; + +@Getter +@EqualsAndHashCode +@RequiredArgsConstructor +@Builder +public class DeviceCacheKey implements Serializable { + + private final TenantId tenantId; + private final DeviceId deviceId; + private final String deviceName; + + public DeviceCacheKey(TenantId tenantId, DeviceId deviceId) { + this(tenantId, deviceId, null); + } + + public DeviceCacheKey(TenantId tenantId, String deviceName) { + this(tenantId, null, deviceName); + } + + @Override + public String toString() { + return CacheKeyUtil.toString(tenantId, deviceId, deviceName); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCaffeineCache.java new file mode 100644 index 0000000000..2349acbc53 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCaffeineCache.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.device; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("DeviceCache") +public class DeviceCaffeineCache extends CaffeineTbTransactionalCache { + + public DeviceCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.DEVICE_CACHE); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceEvent.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceEvent.java new file mode 100644 index 0000000000..51ad354ea3 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceEvent.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.device; + +import lombok.Data; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +public class DeviceEvent { + + private final TenantId tenantId; + private final DeviceId deviceId; + private final String newDeviceName; + private final String oldDeviceName; + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceRedisCache.java new file mode 100644 index 0000000000..a3c63b49f9 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceRedisCache.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.device; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("DeviceCache") +public class DeviceRedisCache extends RedisTbTransactionalCache { + + public DeviceRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.DEVICE_CACHE, cacheSpecsMap, connectionFactory, configuration, new RedisSerializer<>() { + + private final RedisSerializer java = RedisSerializer.java(); + + @Override + public byte[] serialize(Device attributeKvEntry) throws SerializationException { + return java.serialize(attributeKvEntry); + } + + @Override + public Device deserialize(byte[] bytes) throws SerializationException { + return (Device) java.deserialize(bytes); + } + }); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index a3260ffa17..7d12d505d5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -24,20 +24,23 @@ import org.apache.commons.lang3.RandomStringUtils; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.cache.annotation.Caching; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; +import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.device.DeviceSearchQuery; import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; import org.thingsboard.server.common.data.device.data.CoapDeviceTransportConfiguration; @@ -62,10 +65,10 @@ import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; -import org.thingsboard.server.dao.cache.EntitiesCacheManager; import org.thingsboard.server.dao.device.provision.ProvisionFailedException; import org.thingsboard.server.dao.device.provision.ProvisionRequest; import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus; +import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.exception.DataValidationException; @@ -77,7 +80,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; -import java.util.Optional; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; @@ -91,7 +93,7 @@ import static org.thingsboard.server.dao.service.Validator.validateString; @Service @Slf4j -public class DeviceServiceImpl extends AbstractEntityService implements DeviceService { +public class DeviceServiceImpl extends AbstractCachedEntityService implements DeviceService { public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; public static final String INCORRECT_DEVICE_PROFILE_ID = "Incorrect deviceProfileId "; @@ -109,15 +111,13 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @Autowired private DeviceProfileService deviceProfileService; - @Autowired - private EntitiesCacheManager cacheManager; - @Autowired private EventService eventService; @Autowired private DataValidator deviceValidator; + @Transactional(propagation = Propagation.SUPPORTS) @Override public DeviceInfo findDeviceInfoById(TenantId tenantId, DeviceId deviceId) { log.trace("Executing findDeviceInfoById [{}]", deviceId); @@ -125,16 +125,20 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return deviceDao.findDeviceInfoById(tenantId, deviceId.getId()); } - @Cacheable(cacheNames = DEVICE_CACHE, key = "{#tenantId, #deviceId}") + @Transactional(propagation = Propagation.SUPPORTS) @Override public Device findDeviceById(TenantId tenantId, DeviceId deviceId) { log.trace("Executing findDeviceById [{}]", deviceId); validateId(deviceId, INCORRECT_DEVICE_ID + deviceId); - if (TenantId.SYS_TENANT_ID.equals(tenantId)) { - return deviceDao.findById(tenantId, deviceId.getId()); - } else { - return deviceDao.findDeviceByTenantIdAndId(tenantId, deviceId.getId()); - } + return cache.getAndPutInTransaction(new DeviceCacheKey(tenantId, deviceId), + () -> { + //TODO: possible bug source since sometimes we need to clear cache by tenant id and sometimes by sys tenant id? + if (TenantId.SYS_TENANT_ID.equals(tenantId)) { + return deviceDao.findById(tenantId, deviceId.getId()); + } else { + return deviceDao.findDeviceByTenantIdAndId(tenantId, deviceId.getId()); + } + }, true); } @Override @@ -148,47 +152,33 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe } } - @Cacheable(cacheNames = DEVICE_CACHE, key = "{#tenantId, #name}") + @Transactional(propagation = Propagation.SUPPORTS) @Override public Device findDeviceByTenantIdAndName(TenantId tenantId, String name) { log.trace("Executing findDeviceByTenantIdAndName [{}][{}]", tenantId, name); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - Optional deviceOpt = deviceDao.findDeviceByTenantIdAndName(tenantId.getId(), name); - return deviceOpt.orElse(null); + return cache.getAndPutInTransaction(new DeviceCacheKey(tenantId, name), + () -> deviceDao.findDeviceByTenantIdAndName(tenantId.getId(), name).orElse(null), true); } - @Caching(evict= { - @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.name}"), - @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.id}") - }) - @Transactional + @Transactional(propagation = Propagation.SUPPORTS) @Override public Device saveDeviceWithAccessToken(Device device, String accessToken) { return doSaveDevice(device, accessToken, true); } - @Caching(evict= { - @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.name}"), - @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.id}") - }) + @Transactional(propagation = Propagation.SUPPORTS) @Override public Device saveDevice(Device device, boolean doValidate) { return doSaveDevice(device, null, doValidate); } - @Caching(evict= { - @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.name}"), - @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.id}") - }) + @Transactional(propagation = Propagation.SUPPORTS) @Override public Device saveDevice(Device device) { return doSaveDevice(device, null, true); } - @Caching(evict= { - @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.name}"), - @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.id}") - }) @Transactional @Override public Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials) { @@ -226,9 +216,13 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe private Device saveDeviceWithoutCredentials(Device device, boolean doValidate) { log.trace("Executing saveDevice [{}]", device); + Device oldDevice = null; if (doValidate) { - deviceValidator.validate(device, Device::getTenantId); + oldDevice = deviceValidator.validate(device, Device::getTenantId); + } else if (device.getId() != null) { + oldDevice = findDeviceById(device.getTenantId(), device.getId()); } + DeviceEvent deviceEvent = new DeviceEvent(device.getTenantId(), device.getId(), device.getName(), oldDevice != null ? oldDevice.getName() : null); try { DeviceProfile deviceProfile; if (device.getDeviceProfileId() == null) { @@ -246,13 +240,14 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe } device.setType(deviceProfile.getName()); device.setDeviceData(syncDeviceData(deviceProfile, device.getDeviceData())); - return deviceDao.saveAndFlush(device.getTenantId(), device); + Device result = deviceDao.saveAndFlush(device.getTenantId(), device); + publishEvictEvent(deviceEvent); + return result; } catch (Exception t) { ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_name_unq_key")) { // remove device from cache in case null value cached in the distributed redis. - cacheManager.removeDeviceFromCacheByName(device.getTenantId(), device.getName()); - cacheManager.removeDeviceFromCacheById(device.getTenantId(), device.getId()); + handleEvictEvent(deviceEvent); throw new DataValidationException("Device with such name already exists!"); } else { throw t; @@ -260,15 +255,27 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe } } + @TransactionalEventListener(classes = DeviceEvent.class) + @Override + public void handleEvictEvent(DeviceEvent event) { + List keys = new ArrayList<>(3); + keys.add(new DeviceCacheKey(event.getTenantId(), event.getNewDeviceName())); + if (event.getDeviceId() != null) { + keys.add(new DeviceCacheKey(event.getTenantId(), event.getDeviceId())); + } + if (StringUtils.isNotEmpty(event.getOldDeviceName()) && !event.getOldDeviceName().equals(event.getNewDeviceName())) { + keys.add(new DeviceCacheKey(event.getTenantId(), event.getOldDeviceName())); + } + cache.evict(keys); + } + private DeviceData syncDeviceData(DeviceProfile deviceProfile, DeviceData deviceData) { if (deviceData == null) { deviceData = new DeviceData(); } if (deviceData.getConfiguration() == null || !deviceProfile.getType().equals(deviceData.getConfiguration().getType())) { - switch (deviceProfile.getType()) { - case DEFAULT: - deviceData.setConfiguration(new DefaultDeviceConfiguration()); - break; + if (deviceProfile.getType() == DeviceProfileType.DEFAULT) { + deviceData.setConfiguration(new DefaultDeviceConfiguration()); } } if (deviceData.getTransportConfiguration() == null || !deviceProfile.getTransportType().equals(deviceData.getTransportConfiguration().getType())) { @@ -293,24 +300,20 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return deviceData; } + @Transactional @Override public Device assignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, CustomerId customerId) { Device device = findDeviceById(tenantId, deviceId); device.setCustomerId(customerId); - Device savedDevice = saveDevice(device); - cacheManager.removeDeviceFromCacheByName(tenantId, device.getName()); - cacheManager.removeDeviceFromCacheById(tenantId, device.getId()); - return savedDevice; + return saveDevice(device); } + @Transactional @Override public Device unassignDeviceFromCustomer(TenantId tenantId, DeviceId deviceId) { Device device = findDeviceById(tenantId, deviceId); device.setCustomerId(null); - Device savedDevice = saveDevice(device); - cacheManager.removeDeviceFromCacheByName(tenantId, device.getName()); - cacheManager.removeDeviceFromCacheById(tenantId, device.getId()); - return savedDevice; + return saveDevice(device); } @Transactional @@ -320,7 +323,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe validateId(deviceId, INCORRECT_DEVICE_ID + deviceId); Device device = deviceDao.findById(tenantId, deviceId.getId()); - final String deviceName = device.getName(); + DeviceEvent deviceEvent = new DeviceEvent(device.getTenantId(), device.getId(), device.getName(), null); try { List entityViews = entityViewService.findEntityViewsByTenantIdAndEntityIdAsync(device.getTenantId(), deviceId).get(); if (entityViews != null && !entityViews.isEmpty()) { @@ -339,12 +342,11 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe deviceDao.removeById(tenantId, deviceId.getId()); - cacheManager.removeDeviceFromCacheByName(tenantId, deviceName); - cacheManager.removeDeviceFromCacheById(tenantId, deviceId); + publishEvictEvent(deviceEvent); } - + @Transactional(propagation = Propagation.SUPPORTS) @Override public PageData findDevicesByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findDevicesByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); @@ -353,6 +355,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return deviceDao.findDevicesByTenantId(tenantId.getId(), pageLink); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public PageData findDeviceInfosByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findDeviceInfosByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); @@ -361,6 +364,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return deviceDao.findDeviceInfosByTenantId(tenantId.getId(), pageLink); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public PageData findDevicesByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink) { log.trace("Executing findDevicesByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink); @@ -370,6 +374,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return deviceDao.findDevicesByTenantIdAndType(tenantId.getId(), type, pageLink); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public PageData findDevicesByTenantIdAndTypeAndEmptyOtaPackage(TenantId tenantId, DeviceProfileId deviceProfileId, @@ -383,6 +388,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return deviceDao.findDevicesByTenantIdAndTypeAndEmptyOtaPackage(tenantId.getId(), deviceProfileId.getId(), type, pageLink); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public Long countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage(TenantId tenantId, DeviceProfileId deviceProfileId, OtaPackageType type) { log.trace("Executing countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage, tenantId [{}], deviceProfileId [{}], type [{}]", tenantId, deviceProfileId, type); @@ -391,6 +397,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return deviceDao.countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage(tenantId.getId(), deviceProfileId.getId(), type); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public PageData findDeviceInfosByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink) { log.trace("Executing findDeviceInfosByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink); @@ -400,6 +407,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return deviceDao.findDeviceInfosByTenantIdAndType(tenantId.getId(), type, pageLink); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public PageData findDeviceInfosByTenantIdAndDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId, PageLink pageLink) { log.trace("Executing findDeviceInfosByTenantIdAndDeviceProfileId, tenantId [{}], deviceProfileId [{}], pageLink [{}]", tenantId, deviceProfileId, pageLink); @@ -409,6 +417,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return deviceDao.findDeviceInfosByTenantIdAndDeviceProfileId(tenantId.getId(), deviceProfileId.getId(), pageLink); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public ListenableFuture> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List deviceIds) { log.trace("Executing findDevicesByTenantIdAndIdsAsync, tenantId [{}], deviceIds [{}]", tenantId, deviceIds); @@ -417,7 +426,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return deviceDao.findDevicesByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(deviceIds)); } - + @Transactional @Override public void deleteDevicesByTenantId(TenantId tenantId) { log.trace("Executing deleteDevicesByTenantId, tenantId [{}]", tenantId); @@ -425,6 +434,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe tenantDevicesRemover.removeEntities(tenantId, tenantId); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public PageData findDevicesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { log.trace("Executing findDevicesByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); @@ -434,6 +444,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return deviceDao.findDevicesByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public PageData findDeviceInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { log.trace("Executing findDeviceInfosByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); @@ -443,6 +454,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return deviceDao.findDeviceInfosByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public PageData findDevicesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) { log.trace("Executing findDevicesByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink); @@ -453,6 +465,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return deviceDao.findDevicesByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public PageData findDeviceInfosByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) { log.trace("Executing findDeviceInfosByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink); @@ -463,6 +476,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return deviceDao.findDeviceInfosByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public PageData findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, PageLink pageLink) { log.trace("Executing findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId, tenantId [{}], customerId [{}], deviceProfileId [{}], pageLink [{}]", tenantId, customerId, deviceProfileId, pageLink); @@ -473,6 +487,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return deviceDao.findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(tenantId.getId(), customerId.getId(), deviceProfileId.getId(), pageLink); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public ListenableFuture> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List deviceIds) { log.trace("Executing findDevicesByTenantIdCustomerIdAndIdsAsync, tenantId [{}], customerId [{}], deviceIds [{}]", tenantId, customerId, deviceIds); @@ -483,6 +498,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe customerId.getId(), toUUIDs(deviceIds)); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public void unassignCustomerDevices(TenantId tenantId, CustomerId customerId) { log.trace("Executing unassignCustomerDevices, tenantId [{}], customerId [{}]", tenantId, customerId); @@ -491,6 +507,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe customerDeviceUnasigner.removeEntities(tenantId, customerId); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public ListenableFuture> findDevicesByQuery(TenantId tenantId, DeviceSearchQuery query) { ListenableFuture> relations = relationService.findByQuery(tenantId, query.toEntitySearchQuery()); @@ -506,7 +523,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return Futures.successfulAsList(futures); }, MoreExecutors.directExecutor()); - devices = Futures.transform(devices, new Function, List>() { + devices = Futures.transform(devices, new Function<>() { @Nullable @Override public List apply(@Nullable List deviceList) { @@ -517,6 +534,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return devices; } + @Transactional(propagation = Propagation.SUPPORTS) @Override public ListenableFuture> findDeviceTypesByTenantId(TenantId tenantId) { log.trace("Executing findDeviceTypesByTenantId, tenantId [{}]", tenantId); @@ -533,7 +551,6 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @Override public Device assignDeviceToTenant(TenantId tenantId, Device device) { log.trace("Executing assignDeviceToTenant [{}][{}]", tenantId, device); - try { List entityViews = entityViewService.findEntityViewsByTenantIdAndEntityIdAsync(device.getTenantId(), device.getId()).get(); if (!CollectionUtils.isEmpty(entityViews)) { @@ -554,10 +571,13 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe device.setCustomerId(null); Device savedDevice = doSaveDevice(device, null, true); + DeviceEvent oldTenantEvent = new DeviceEvent(oldTenantId, device.getId(), device.getName(), null); + DeviceEvent newTenantEvent = new DeviceEvent(savedDevice.getTenantId(), device.getId(), device.getName(), null); + // explicitly remove device with previous tenant id from cache // result device object will have different tenant id and will not remove entity from cache - cacheManager.removeDeviceFromCacheByName(oldTenantId, device.getName()); - cacheManager.removeDeviceFromCacheById(oldTenantId, device.getId()); + publishEvictEvent(oldTenantEvent); + publishEvictEvent(newTenantEvent); return savedDevice; } @@ -605,15 +625,18 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); } } - cacheManager.removeDeviceFromCacheById(savedDevice.getTenantId(), savedDevice.getId()); // eviction by name is described as annotation @CacheEvict above + + publishEvictEvent(new DeviceEvent(savedDevice.getTenantId(), savedDevice.getId(), provisionRequest.getDeviceName(), null)); return savedDevice; } + @Transactional(propagation = Propagation.SUPPORTS) @Override public PageData findDevicesIdsByDeviceProfileTransportType(DeviceTransportType transportType, PageLink pageLink) { return deviceDao.findDevicesIdsByDeviceProfileTransportType(transportType, pageLink); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public Device assignDeviceToEdge(TenantId tenantId, DeviceId deviceId, EdgeId edgeId) { Device device = findDeviceById(tenantId, deviceId); @@ -633,6 +656,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return device; } + @Transactional(propagation = Propagation.SUPPORTS) @Override public Device unassignDeviceFromEdge(TenantId tenantId, DeviceId deviceId, EdgeId edgeId) { Device device = findDeviceById(tenantId, deviceId); @@ -652,6 +676,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return device; } + @Transactional(propagation = Propagation.SUPPORTS) @Override public PageData findDevicesByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink) { log.trace("Executing findDevicesByTenantIdAndEdgeId, tenantId [{}], edgeId [{}], pageLink [{}]", tenantId, edgeId, pageLink); @@ -661,6 +686,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return deviceDao.findDevicesByTenantIdAndEdgeId(tenantId.getId(), edgeId.getId(), pageLink); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public PageData findDevicesByTenantIdAndEdgeIdAndType(TenantId tenantId, EdgeId edgeId, String type, PageLink pageLink) { log.trace("Executing findDevicesByTenantIdAndEdgeIdAndType, tenantId [{}], edgeId [{}], type [{}] pageLink [{}]", tenantId, edgeId, type, pageLink); @@ -677,7 +703,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe } private PaginatedRemover tenantDevicesRemover = - new PaginatedRemover() { + new PaginatedRemover<>() { @Override protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractCachedEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractCachedEntityService.java new file mode 100644 index 0000000000..9d97aab675 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractCachedEntityService.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.entity; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.thingsboard.server.cache.TbTransactionalCache; + +import java.io.Serializable; + +public abstract class AbstractCachedEntityService extends AbstractEntityService { + + @Autowired + protected TbTransactionalCache cache; + + @Autowired + private ApplicationEventPublisher eventPublisher; + + protected void publishEvictEvent(E event) { + if (TransactionSynchronizationManager.isActualTransactionActive()) { + eventPublisher.publishEvent(event); + } else { + handleEvictEvent(event); + } + } + + public abstract void handleEvictEvent(E event); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java index 9321fd4e9e..94be99af12 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java @@ -18,7 +18,9 @@ package org.thingsboard.server.dao.entity; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Lazy; +import org.springframework.transaction.support.TransactionSynchronizationManager; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; @@ -29,6 +31,7 @@ import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.relation.EntityRelationEvent; import org.thingsboard.server.dao.relation.RelationService; import java.util.List; diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index 00eafeec1c..a12c2e3faa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -19,7 +19,6 @@ import com.google.common.base.Function; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Lazy; @@ -28,6 +27,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; +import org.springframework.transaction.support.TransactionSynchronizationManager; import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.EntityId; @@ -65,21 +65,21 @@ public class BaseRelationService implements RelationService { private final RelationDao relationDao; private final EntityService entityService; private final TbTransactionalCache cache; - private final ApplicationEventPublisher publisher; + private final ApplicationEventPublisher eventPublisher; private final JpaExecutorService executor; public BaseRelationService(RelationDao relationDao, @Lazy EntityService entityService, TbTransactionalCache cache, - ApplicationEventPublisher publisher, JpaExecutorService executor) { + ApplicationEventPublisher eventPublisher, JpaExecutorService executor) { this.relationDao = relationDao; this.entityService = entityService; this.cache = cache; - this.publisher = publisher; + this.eventPublisher = eventPublisher; this.executor = executor; } @TransactionalEventListener(classes = EntityRelationEvent.class) - public void handleRelationEvictEvent(EntityRelationEvent event) { + public void handleEvictEvent(EntityRelationEvent event) { List keys = new ArrayList<>(5); keys.add(new RelationCacheKey(event.getFrom(), event.getTo(), event.getType(), event.getTypeGroup())); keys.add(new RelationCacheKey(event.getFrom(), null, event.getType(), event.getTypeGroup(), EntitySearchDirection.FROM)); @@ -103,18 +103,21 @@ public class BaseRelationService implements RelationService { validate(from, to, relationType, typeGroup); RelationCacheKey cacheKey = new RelationCacheKey(from, to, relationType, typeGroup); return cache.getAndPutInTransaction(cacheKey, - () -> relationDao.getRelation(tenantId, from, to, relationType, typeGroup), + () -> { + log.trace("FETCH EntityRelation [{}][{}][{}][{}]", from, to, relationType, typeGroup); + return relationDao.getRelation(tenantId, from, to, relationType, typeGroup); + }, RelationCacheValue::getRelation, relations -> RelationCacheValue.builder().relation(relations).build(), false); } - @Override @Transactional(propagation = Propagation.SUPPORTS) + @Override public boolean saveRelation(TenantId tenantId, EntityRelation relation) { log.trace("Executing saveRelation [{}]", relation); validate(relation); var result = relationDao.saveRelation(tenantId, relation); - publisher.publishEvent(EntityRelationEvent.from(relation)); + publishEvictEvent(EntityRelationEvent.from(relation)); return result; } @@ -123,18 +126,18 @@ public class BaseRelationService implements RelationService { log.trace("Executing saveRelationAsync [{}]", relation); validate(relation); var future = relationDao.saveRelationAsync(tenantId, relation); - future.addListener(() -> handleRelationEvictEvent(EntityRelationEvent.from(relation)), MoreExecutors.directExecutor()); + future.addListener(() -> handleEvictEvent(EntityRelationEvent.from(relation)), MoreExecutors.directExecutor()); return future; } @Transactional(propagation = Propagation.SUPPORTS) @Override public boolean deleteRelation(TenantId tenantId, EntityRelation relation) { - log.trace("Executing deleteRelation [{}]", relation); + log.trace("Executing DeleteRelation [{}]", relation); validate(relation); var result = relationDao.deleteRelation(tenantId, relation); //TODO: evict cache only if the relation was deleted. Note: relationDao.deleteRelation requires improvement. - publisher.publishEvent(EntityRelationEvent.from(relation)); + publishEvictEvent(EntityRelationEvent.from(relation)); return result; } @@ -143,7 +146,7 @@ public class BaseRelationService implements RelationService { log.trace("Executing deleteRelationAsync [{}]", relation); validate(relation); var future = relationDao.deleteRelationAsync(tenantId, relation); - future.addListener(() -> handleRelationEvictEvent(EntityRelationEvent.from(relation)), MoreExecutors.directExecutor()); + future.addListener(() -> handleEvictEvent(EntityRelationEvent.from(relation)), MoreExecutors.directExecutor()); return future; } @@ -154,7 +157,7 @@ public class BaseRelationService implements RelationService { validate(from, to, relationType, typeGroup); var result = relationDao.deleteRelation(tenantId, from, to, relationType, typeGroup); //TODO: evict cache only if the relation was deleted. Note: relationDao.deleteRelation requires improvement. - publisher.publishEvent(new EntityRelationEvent(from, to, relationType, typeGroup)); + publishEvictEvent(new EntityRelationEvent(from, to, relationType, typeGroup)); return result; } @@ -164,7 +167,7 @@ public class BaseRelationService implements RelationService { validate(from, to, relationType, typeGroup); var future = relationDao.deleteRelationAsync(tenantId, from, to, relationType, typeGroup); EntityRelationEvent event = new EntityRelationEvent(from, to, relationType, typeGroup); - future.addListener(() -> handleRelationEvictEvent(event), MoreExecutors.directExecutor()); + future.addListener(() -> handleEvictEvent(event), MoreExecutors.directExecutor()); return future; } @@ -245,17 +248,17 @@ public class BaseRelationService implements RelationService { if (deleteFromDb) { return Futures.transform(relationDao.deleteRelationAsync(tenantId, relation), bool -> { - handleRelationEvictEvent(EntityRelationEvent.from(relation)); + handleEvictEvent(EntityRelationEvent.from(relation)); return bool; }, MoreExecutors.directExecutor()); } else { - handleRelationEvictEvent(EntityRelationEvent.from(relation)); + handleEvictEvent(EntityRelationEvent.from(relation)); return Futures.immediateFuture(false); } } boolean delete(TenantId tenantId, EntityRelation relation, boolean deleteFromDb) { - publisher.publishEvent(EntityRelationEvent.from(relation)); + eventPublisher.publishEvent(EntityRelationEvent.from(relation)); if (deleteFromDb) { try { return relationDao.deleteRelation(tenantId, relation); @@ -592,4 +595,13 @@ public class BaseRelationService implements RelationService { } return relations; } + + private void publishEvictEvent(EntityRelationEvent event) { + if (TransactionSynchronizationManager.isActualTransactionActive()) { + eventPublisher.publishEvent(event); + } else { + handleEvictEvent(event); + } + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheKey.java index 0df5e3e1c4..b60aa84b9b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheKey.java @@ -19,6 +19,7 @@ import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.thingsboard.server.cache.CacheKeyUtil; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationTypeGroup; @@ -45,26 +46,7 @@ public class RelationCacheKey implements Serializable { @Override public String toString() { - StringBuilder sb = new StringBuilder(); - boolean first = true; - first = addElement(sb, first, from); - first = addElement(sb, first, to); - first = addElement(sb, first, typeGroup); - first = addElement(sb, first, type); - first = addElement(sb, first, direction); - return sb.toString(); - } - - private boolean addElement(StringBuilder sb, boolean first, Object element) { - if (element != null) { - if (!first) { - sb.append("_"); - } - sb.append(element); - return false; - } else { - return first; - } + return CacheKeyUtil.toString(from, to, typeGroup, type, direction); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java index e14e414ebb..1b27a2751f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java @@ -36,7 +36,8 @@ public abstract class DataValidator> { private static final Pattern EMAIL_PATTERN = Pattern.compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$", Pattern.CASE_INSENSITIVE); - public void validate(D data, Function tenantIdFunction) { + // Returns old instance of the same object that is fetched during validation. + public D validate(D data, Function tenantIdFunction) { try { if (data == null) { throw new DataValidationException("Data object can't be null!"); @@ -46,11 +47,14 @@ public abstract class DataValidator> { TenantId tenantId = tenantIdFunction.apply(data); validateDataImpl(tenantId, data); + D old; if (data.getId() == null) { validateCreate(tenantId, data); + old = null; } else { - validateUpdate(tenantId, data); + old = validateUpdate(tenantId, data); } + return old; } catch (DataValidationException e) { log.error("Data object is invalid: [{}]", e.getMessage()); throw e; @@ -63,7 +67,8 @@ public abstract class DataValidator> { protected void validateCreate(TenantId tenantId, D data) { } - protected void validateUpdate(TenantId tenantId, D data) { + protected D validateUpdate(TenantId tenantId, D data) { + return null; } protected boolean isSameData(D existentData, D actualData) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AdminSettingsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AdminSettingsDataValidator.java index c1485cfb1a..8033688787 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AdminSettingsDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AdminSettingsDataValidator.java @@ -39,13 +39,14 @@ public class AdminSettingsDataValidator extends DataValidator { } @Override - protected void validateUpdate(TenantId tenantId, AdminSettings adminSettings) { + protected AdminSettings validateUpdate(TenantId tenantId, AdminSettings adminSettings) { AdminSettings existentAdminSettings = adminSettingsService.findAdminSettingsById(tenantId, adminSettings.getId()); if (existentAdminSettings != null) { if (!existentAdminSettings.getKey().equals(adminSettings.getKey())) { throw new DataValidationException("Changing key of admin settings entry is prohibited!"); } } + return existentAdminSettings; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java index 57d39ee781..bc03a2131b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java @@ -67,7 +67,7 @@ public class AssetDataValidator extends DataValidator { } @Override - protected void validateUpdate(TenantId tenantId, Asset asset) { + protected Asset validateUpdate(TenantId tenantId, Asset asset) { Asset old = assetDao.findById(asset.getTenantId(), asset.getId().getId()); if (old == null) { throw new DataValidationException("Can't update non existing asset!"); @@ -75,6 +75,7 @@ public class AssetDataValidator extends DataValidator { if (!old.getName().equals(asset.getName())) { cacheManager.removeAssetFromCacheByName(tenantId, old.getName()); } + return old; } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ClientRegistrationTemplateDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ClientRegistrationTemplateDataValidator.java index d558c19e9e..b5d0247e28 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ClientRegistrationTemplateDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ClientRegistrationTemplateDataValidator.java @@ -30,7 +30,8 @@ public class ClientRegistrationTemplateDataValidator extends DataValidator { @@ -59,14 +61,16 @@ public class CustomerDataValidator extends DataValidator { } @Override - protected void validateUpdate(TenantId tenantId, Customer customer) { - customerDao.findCustomersByTenantIdAndTitle(customer.getTenantId().getId(), customer.getTitle()).ifPresent( + protected Customer validateUpdate(TenantId tenantId, Customer customer) { + Optional customerOpt = customerDao.findCustomersByTenantIdAndTitle(customer.getTenantId().getId(), customer.getTitle()); + customerOpt.ifPresent( c -> { if (!c.getId().equals(customer.getId())) { throw new DataValidationException("Customer with such title already exists!"); } } ); + return customerOpt.orElse(null); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceCredentialsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceCredentialsDataValidator.java index 5fca062d46..cc7e488df7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceCredentialsDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceCredentialsDataValidator.java @@ -46,7 +46,7 @@ public class DeviceCredentialsDataValidator extends DataValidator { @Autowired private OtaPackageService otaPackageService; - @Autowired - private EntitiesCacheManager cacheManager; - @Override protected void validateCreate(TenantId tenantId, Device device) { DefaultTenantProfileConfiguration profileConfiguration = @@ -73,15 +70,12 @@ public class DeviceDataValidator extends DataValidator { } @Override - protected void validateUpdate(TenantId tenantId, Device device) { + protected Device validateUpdate(TenantId tenantId, Device device) { Device old = deviceDao.findById(device.getTenantId(), device.getId().getId()); if (old == null) { throw new DataValidationException("Can't update non existing device!"); } - if (!old.getName().equals(device.getName())) { - cacheManager.removeDeviceFromCacheByName(tenantId, old.getName()); - cacheManager.removeDeviceFromCacheById(tenantId, device.getId()); - } + return old; } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java index adc8cf9fb1..e67d4f9b35 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java @@ -236,7 +236,7 @@ public class DeviceProfileDataValidator extends DataValidator { } @Override - protected void validateUpdate(TenantId tenantId, DeviceProfile deviceProfile) { + protected DeviceProfile validateUpdate(TenantId tenantId, DeviceProfile deviceProfile) { DeviceProfile old = deviceProfileDao.findById(deviceProfile.getTenantId(), deviceProfile.getId().getId()); if (old == null) { throw new DataValidationException("Can't update non existing device profile!"); @@ -255,6 +255,7 @@ public class DeviceProfileDataValidator extends DataValidator { throw new DataValidationException(message); } } + return old; } private void validateProtoSchemas(ProtoTransportPayloadConfiguration protoTransportPayloadTypeConfiguration) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/EdgeDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/EdgeDataValidator.java index be01ec5c9b..490da123e6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/EdgeDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/EdgeDataValidator.java @@ -46,11 +46,12 @@ public class EdgeDataValidator extends DataValidator { } @Override - protected void validateUpdate(TenantId tenantId, Edge edge) { + protected Edge validateUpdate(TenantId tenantId, Edge edge) { Edge old = edgeDao.findById(edge.getTenantId(), edge.getId().getId()); if (!old.getName().equals(edge.getName())) { cacheManager.removeEdgeFromCacheByName(tenantId, old.getName()); } + return old; } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/EntityViewDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/EntityViewDataValidator.java index a62e1afc9f..a38850c63c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/EntityViewDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/EntityViewDataValidator.java @@ -48,13 +48,14 @@ public class EntityViewDataValidator extends DataValidator { } @Override - protected void validateUpdate(TenantId tenantId, EntityView entityView) { - entityViewDao.findEntityViewByTenantIdAndName(entityView.getTenantId().getId(), entityView.getName()) - .ifPresent(e -> { - if (!e.getUuidId().equals(entityView.getUuidId())) { - throw new DataValidationException("Entity view with such name already exists!"); - } - }); + protected EntityView validateUpdate(TenantId tenantId, EntityView entityView) { + var opt = entityViewDao.findEntityViewByTenantIdAndName(entityView.getTenantId().getId(), entityView.getName()); + opt.ifPresent(e -> { + if (!e.getUuidId().equals(entityView.getUuidId())) { + throw new DataValidationException("Entity view with such name already exists!"); + } + }); + return opt.orElse(null); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/OtaPackageDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/OtaPackageDataValidator.java index 8b261ad226..90db02d45e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/OtaPackageDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/OtaPackageDataValidator.java @@ -86,7 +86,7 @@ public class OtaPackageDataValidator extends BaseOtaPackageDataValidator { } @Override - protected void validateUpdate(TenantId tenantId, Tenant tenant) { + protected Tenant validateUpdate(TenantId tenantId, Tenant tenant) { Tenant old = tenantDao.findById(TenantId.SYS_TENANT_ID, tenantId.getId()); if (old == null) { throw new DataValidationException("Can't update non existing tenant!"); } validateTenantProfile(tenantId, tenant); + return old; } private void validateTenantProfile(TenantId tenantId, Tenant tenant) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java index e8207534be..4c5e300966 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java @@ -56,7 +56,7 @@ public class TenantProfileDataValidator extends DataValidator { } @Override - protected void validateUpdate(TenantId tenantId, TenantProfile tenantProfile) { + protected TenantProfile validateUpdate(TenantId tenantId, TenantProfile tenantProfile) { TenantProfile old = tenantProfileDao.findById(TenantId.SYS_TENANT_ID, tenantProfile.getId().getId()); if (old == null) { throw new DataValidationException("Can't update non existing tenant profile!"); @@ -65,5 +65,6 @@ public class TenantProfileDataValidator extends DataValidator { } else if (old.isIsolatedTbCore() != tenantProfile.isIsolatedTbCore()) { throw new DataValidationException("Can't update isolatedTbCore property!"); } + return old; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetTypeDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetTypeDataValidator.java index ec3be14866..93a1766546 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetTypeDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetTypeDataValidator.java @@ -83,8 +83,8 @@ public class WidgetTypeDataValidator extends DataValidator { } @Override - protected void validateUpdate(TenantId tenantId, WidgetTypeDetails widgetTypeDetails) { - WidgetType storedWidgetType = widgetTypeDao.findById(tenantId, widgetTypeDetails.getId().getId()); + protected WidgetTypeDetails validateUpdate(TenantId tenantId, WidgetTypeDetails widgetTypeDetails) { + WidgetTypeDetails storedWidgetType = widgetTypeDao.findById(tenantId, widgetTypeDetails.getId().getId()); if (!storedWidgetType.getTenantId().getId().equals(widgetTypeDetails.getTenantId().getId())) { throw new DataValidationException("Can't move existing widget type to different tenant!"); } @@ -94,5 +94,6 @@ public class WidgetTypeDataValidator extends DataValidator { if (!storedWidgetType.getAlias().equals(widgetTypeDetails.getAlias())) { throw new DataValidationException("Update of widget type alias is prohibited!"); } + return new WidgetTypeDetails(storedWidgetType); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetsBundleDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetsBundleDataValidator.java index d5b1a1149c..b140dfc79d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetsBundleDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetsBundleDataValidator.java @@ -69,7 +69,7 @@ public class WidgetsBundleDataValidator extends DataValidator { } @Override - protected void validateUpdate(TenantId tenantId, WidgetsBundle widgetsBundle) { + protected WidgetsBundle validateUpdate(TenantId tenantId, WidgetsBundle widgetsBundle) { WidgetsBundle storedWidgetsBundle = widgetsBundleDao.findById(tenantId, widgetsBundle.getId().getId()); if (!storedWidgetsBundle.getTenantId().getId().equals(widgetsBundle.getTenantId().getId())) { throw new DataValidationException("Can't move existing widgets bundle to different tenant!"); @@ -77,5 +77,6 @@ public class WidgetsBundleDataValidator extends DataValidator { if (!storedWidgetsBundle.getAlias().equals(widgetsBundle.getAlias())) { throw new DataValidationException("Update of widgets bundle alias is prohibited!"); } + return storedWidgetsBundle; } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeEventServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeEventServiceTest.java index 33cb7e1bc1..73b26df9e8 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeEventServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeEventServiceTest.java @@ -19,6 +19,7 @@ import com.datastax.oss.driver.api.core.uuid.Uuids; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -33,14 +34,33 @@ import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; import java.io.IOException; +import java.text.ParseException; import java.time.LocalDateTime; import java.time.Month; import java.time.ZoneOffset; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.apache.commons.lang3.time.DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT; public abstract class BaseEdgeEventServiceTest extends AbstractServiceTest { + long timeBeforeStartTime; + long startTime; + long eventTime; + long endTime; + long timeAfterEndTime; + + @Before + public void before() throws ParseException { + timeBeforeStartTime = ISO_DATETIME_TIME_ZONE_FORMAT.parse("2016-11-01T11:30:00Z").getTime(); + startTime = ISO_DATETIME_TIME_ZONE_FORMAT.parse("2016-11-01T12:00:00Z").getTime(); + eventTime = ISO_DATETIME_TIME_ZONE_FORMAT.parse("2016-11-01T12:30:00Z").getTime(); + endTime = ISO_DATETIME_TIME_ZONE_FORMAT.parse("2016-11-01T13:00:00Z").getTime(); + timeAfterEndTime = ISO_DATETIME_TIME_ZONE_FORMAT.parse("2016-11-01T13:30:30Z").getTime(); + } + @Test public void saveEdgeEvent() throws Exception { EdgeId edgeId = new EdgeId(Uuids.timeBased()); @@ -75,15 +95,8 @@ public abstract class BaseEdgeEventServiceTest extends AbstractServiceTest { return edgeEvent; } - @Test public void findEdgeEventsByTimeDescOrder() throws Exception { - long timeBeforeStartTime = LocalDateTime.of(2020, Month.NOVEMBER, 1, 11, 30).toEpochSecond(ZoneOffset.UTC); - long startTime = LocalDateTime.of(2020, Month.NOVEMBER, 1, 12, 0).toEpochSecond(ZoneOffset.UTC); - long eventTime = LocalDateTime.of(2020, Month.NOVEMBER, 1, 12, 30).toEpochSecond(ZoneOffset.UTC); - long endTime = LocalDateTime.of(2020, Month.NOVEMBER, 1, 13, 0).toEpochSecond(ZoneOffset.UTC); - long timeAfterEndTime = LocalDateTime.of(2020, Month.NOVEMBER, 1, 13, 30).toEpochSecond(ZoneOffset.UTC); - EdgeId edgeId = new EdgeId(Uuids.timeBased()); DeviceId deviceId = new DeviceId(Uuids.timeBased()); TenantId tenantId = TenantId.fromUUID(Uuids.timeBased()); @@ -113,6 +126,8 @@ public abstract class BaseEdgeEventServiceTest extends AbstractServiceTest { Assert.assertEquals(1, edgeEvents.getData().size()); Assert.assertEquals(Uuids.startOf(eventTime), edgeEvents.getData().get(0).getUuidId()); Assert.assertFalse(edgeEvents.hasNext()); + + edgeEventService.cleanupEvents(1); } @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationCacheTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationCacheTest.java index 9901438bdc..17dc127edb 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationCacheTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationCacheTest.java @@ -97,7 +97,6 @@ public abstract class BaseRelationCacheTest extends AbstractServiceTest { relationService.getRelation(SYSTEM_TENANT_ID, ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON); relationService.getRelation(SYSTEM_TENANT_ID, ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON); - verify(relationDao, times(1)).getRelation(SYSTEM_TENANT_ID, ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON); - + verify(relationDao, times(2)).getRelation(SYSTEM_TENANT_ID, ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java index 7d81e5afc6..f3f6dd8ff8 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java @@ -16,8 +16,9 @@ package org.thingsboard.server.dao.service.event; import com.datastax.oss.driver.api.core.uuid.Uuids; +import org.apache.commons.lang3.time.DateFormatUtils; import org.junit.Assert; -import org.junit.Ignore; +import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Event; @@ -31,14 +32,29 @@ import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.service.AbstractServiceTest; -import java.io.IOException; +import java.text.ParseException; import java.time.LocalDateTime; import java.time.Month; import java.time.ZoneOffset; import java.util.Optional; -import java.util.concurrent.ExecutionException; + +import static org.apache.commons.lang3.time.DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT; public abstract class BaseEventServiceTest extends AbstractServiceTest { + long timeBeforeStartTime; + long startTime; + long eventTime; + long endTime; + long timeAfterEndTime; + + @Before + public void before() throws ParseException { + timeBeforeStartTime = ISO_DATETIME_TIME_ZONE_FORMAT.parse("2016-11-01T11:30:00Z").getTime(); + startTime = ISO_DATETIME_TIME_ZONE_FORMAT.parse("2016-11-01T12:00:00Z").getTime(); + eventTime = ISO_DATETIME_TIME_ZONE_FORMAT.parse("2016-11-01T12:30:00Z").getTime(); + endTime = ISO_DATETIME_TIME_ZONE_FORMAT.parse("2016-11-01T13:00:00Z").getTime(); + timeAfterEndTime = ISO_DATETIME_TIME_ZONE_FORMAT.parse("2016-11-01T13:30:30Z").getTime(); + } @Test public void saveEvent() throws Exception { @@ -55,18 +71,12 @@ public abstract class BaseEventServiceTest extends AbstractServiceTest { @Test public void findEventsByTypeAndTimeAscOrder() throws Exception { - long timeBeforeStartTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 11, 30).toEpochSecond(ZoneOffset.UTC); - long startTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 12, 0).toEpochSecond(ZoneOffset.UTC); - long eventTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 12, 30).toEpochSecond(ZoneOffset.UTC); - long endTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 13, 0).toEpochSecond(ZoneOffset.UTC); - long timeAfterEndTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 13, 30).toEpochSecond(ZoneOffset.UTC); - CustomerId customerId = new CustomerId(Uuids.timeBased()); TenantId tenantId = TenantId.fromUUID(Uuids.timeBased()); saveEventWithProvidedTime(timeBeforeStartTime, customerId, tenantId); Event savedEvent = saveEventWithProvidedTime(eventTime, customerId, tenantId); - Event savedEvent2 = saveEventWithProvidedTime(eventTime+1, customerId, tenantId); - Event savedEvent3 = saveEventWithProvidedTime(eventTime+2, customerId, tenantId); + Event savedEvent2 = saveEventWithProvidedTime(eventTime + 1, customerId, tenantId); + Event savedEvent3 = saveEventWithProvidedTime(eventTime + 2, customerId, tenantId); saveEventWithProvidedTime(timeAfterEndTime, customerId, tenantId); TimePageLink timePageLink = new TimePageLink(2, 0, "", new SortOrder("createdTime"), startTime, endTime); @@ -86,22 +96,18 @@ public abstract class BaseEventServiceTest extends AbstractServiceTest { Assert.assertTrue(events.getData().size() == 1); Assert.assertTrue(events.getData().get(0).getUuidId().equals(savedEvent3.getUuidId())); Assert.assertFalse(events.hasNext()); + + eventService.cleanupEvents(timeBeforeStartTime - 1, timeAfterEndTime + 1, timeBeforeStartTime - 1, timeAfterEndTime + 1); } @Test public void findEventsByTypeAndTimeDescOrder() throws Exception { - long timeBeforeStartTime = LocalDateTime.of(2017, Month.NOVEMBER, 1, 11, 30).toEpochSecond(ZoneOffset.UTC); - long startTime = LocalDateTime.of(2017, Month.NOVEMBER, 1, 12, 0).toEpochSecond(ZoneOffset.UTC); - long eventTime = LocalDateTime.of(2017, Month.NOVEMBER, 1, 12, 30).toEpochSecond(ZoneOffset.UTC); - long endTime = LocalDateTime.of(2017, Month.NOVEMBER, 1, 13, 0).toEpochSecond(ZoneOffset.UTC); - long timeAfterEndTime = LocalDateTime.of(2017, Month.NOVEMBER, 1, 13, 30).toEpochSecond(ZoneOffset.UTC); - CustomerId customerId = new CustomerId(Uuids.timeBased()); TenantId tenantId = TenantId.fromUUID(Uuids.timeBased()); saveEventWithProvidedTime(timeBeforeStartTime, customerId, tenantId); Event savedEvent = saveEventWithProvidedTime(eventTime, customerId, tenantId); - Event savedEvent2 = saveEventWithProvidedTime(eventTime+1, customerId, tenantId); - Event savedEvent3 = saveEventWithProvidedTime(eventTime+2, customerId, tenantId); + Event savedEvent2 = saveEventWithProvidedTime(eventTime + 1, customerId, tenantId); + Event savedEvent3 = saveEventWithProvidedTime(eventTime + 2, customerId, tenantId); saveEventWithProvidedTime(timeAfterEndTime, customerId, tenantId); TimePageLink timePageLink = new TimePageLink(2, 0, "", new SortOrder("createdTime", SortOrder.Direction.DESC), startTime, endTime); @@ -121,6 +127,8 @@ public abstract class BaseEventServiceTest extends AbstractServiceTest { Assert.assertTrue(events.getData().size() == 1); Assert.assertTrue(events.getData().get(0).getUuidId().equals(savedEvent.getUuidId())); Assert.assertFalse(events.hasNext()); + + eventService.cleanupEvents(timeBeforeStartTime - 1, timeAfterEndTime + 1, timeBeforeStartTime - 1, timeAfterEndTime + 1); } private Event saveEventWithProvidedTime(long time, EntityId entityId, TenantId tenantId) throws Exception { From c6a8b9fbaf501e3d58674b64625db5a4b72faa1a Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Mon, 9 May 2022 22:14:07 +0300 Subject: [PATCH 107/276] refactoring: 01 - Alarm Common --- .../server/controller/DeviceController.java | 4 +- .../DefaultTbNotificationEntityService.java | 37 ++++++++ .../entitiy/TbNotificationEntityService.java | 5 ++ .../entitiy/alarm/DefaultTbAlarmService.java | 85 +++++++++++++++++++ .../service/entitiy/alarm/TbAlarmService.java | 31 +++++++ 5 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index 119522a03c..f7b3908010 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -82,6 +82,7 @@ import java.util.stream.Collectors; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_INFO_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_NAME_DESCRIPTION; @@ -102,6 +103,7 @@ import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_A import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID; import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; @@ -114,9 +116,7 @@ import static org.thingsboard.server.controller.EdgeController.EDGE_ID; @Slf4j public class DeviceController extends BaseController { - protected static final String DEVICE_ID = "deviceId"; protected static final String DEVICE_NAME = "deviceName"; - protected static final String TENANT_ID = "tenantId"; private final DeviceBulkImportService deviceBulkImportService; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java index a41c5b2f87..b6ab974988 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; @@ -189,6 +190,18 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS } } + @Override + public void notifyCreateOrUpdateAlarm(Alarm alarm, ActionType actionType, SecurityUser user, Object... additionalInfo) { + logEntityAction(alarm.getTenantId(), alarm.getOriginator(), alarm, alarm.getCustomerId(), actionType, user, additionalInfo); + sendEntityNotificationMsg(alarm.getTenantId(), alarm.getId(), edgeTypeByActionType (actionType)); + } + + @Override + public void notifyDeleteAlarm(Alarm alarm, SecurityUser user, List relatedEdgeIds) { + logEntityAction(alarm.getTenantId(), alarm.getOriginator(), alarm, alarm.getCustomerId(), ActionType.ALARM_DELETE, user, null); + sendAlarmDeleteNotificationMsg(alarm, relatedEdgeIds); + } + private void logEntityAction(TenantId tenantId, I entityId, E entity, CustomerId customerId, ActionType actionType, SecurityUser user, Object... additionalInfo) { logEntityAction(tenantId, entityId, entity, customerId, actionType, user, null, additionalInfo); @@ -219,6 +232,13 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS sendDeleteNotificationMsg(tenantId, entityId, edgeIds, null); } + protected void sendAlarmDeleteNotificationMsg(Alarm alarm, List relatedEdgeIds) { + try { + sendDeleteNotificationMsg(alarm.getTenantId(), alarm.getId(), relatedEdgeIds, json.writeValueAsString(alarm)); + } catch (Exception e) { + log.warn("Failed to push delete alarm msg to core: {}", alarm, e); + } + } private void sendDeleteNotificationMsg(TenantId tenantId, EntityId entityId, List edgeIds, String body) { if (edgeIds != null && !edgeIds.isEmpty()) { for (EdgeId edgeId : edgeIds) { @@ -259,4 +279,21 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS return null; } + private EdgeEventActionType edgeTypeByActionType (ActionType actionType) { + switch (actionType) { + case ADDED: + return EdgeEventActionType.ADDED; + case UPDATED: + return EdgeEventActionType.UPDATED; + case ALARM_ACK: + return EdgeEventActionType.ALARM_ACK; + case ALARM_CLEAR: + return EdgeEventActionType.ALARM_CLEAR; + case DELETED: + return EdgeEventActionType.DELETED; + default: + return null; + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java index e86f068203..181a1f41eb 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.entitiy; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; @@ -78,4 +79,8 @@ public interface TbNotificationEntityService { void notifyEdge(TenantId tenantId, EdgeId edgeId, CustomerId customerId, Edge edge, ActionType actionType, SecurityUser user, Object... additionalInfo); + void notifyCreateOrUpdateAlarm(Alarm alarm, + ActionType actionType, SecurityUser user, Object... additionalInfo); + + void notifyDeleteAlarm(Alarm alarm, SecurityUser user, List relatedEdgeIds); } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java new file mode 100644 index 0000000000..f0ac516393 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java @@ -0,0 +1,85 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.entitiy.alarm; + + +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.AbstractTbEntityService; +import org.thingsboard.server.service.security.model.SecurityUser; + +import java.util.List; + +@Service +@TbCoreComponent +@AllArgsConstructor +public class DefaultTbAlarmService extends AbstractTbEntityService implements TbAlarmService { + + @Override + public Alarm save(Alarm alarm, SecurityUser user) throws ThingsboardException { + ActionType actionType = alarm.getId() == null ? ActionType.ADDED : ActionType.UPDATED; + TenantId tenantId = alarm.getTenantId(); + try { + Alarm savedAlarm = checkNotNull(alarmService.createOrUpdateAlarm(alarm).getAlarm()); + notificationEntityService.notifyCreateOrUpdateAlarm(savedAlarm, actionType, user); + return savedAlarm; + } catch (Exception e) { + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.ALARM), alarm, null, actionType, user, e); + throw handleException(e); + } + } + + @Override + public void ack(Alarm alarm, SecurityUser user) throws ThingsboardException { + try { + long ackTs = System.currentTimeMillis(); + alarmService.ackAlarm(user.getTenantId(), alarm.getId(), ackTs).get(); + alarm.setAckTs(ackTs); + alarm.setStatus(alarm.getStatus().isCleared() ? AlarmStatus.CLEARED_ACK : AlarmStatus.ACTIVE_ACK); + notificationEntityService.notifyCreateOrUpdateAlarm(alarm, ActionType.ALARM_ACK, user); + } catch (Exception e) { + throw handleException(e); + } + } + + @Override + public void clear(Alarm alarm, SecurityUser user) throws ThingsboardException { + try { + long clearTs = System.currentTimeMillis(); + alarmService.clearAlarm(user.getTenantId(), alarm.getId(), null, clearTs).get(); + alarm.setClearTs(clearTs); + alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK); + notificationEntityService.notifyCreateOrUpdateAlarm(alarm, ActionType.ALARM_CLEAR, user); + } catch (Exception e) { + throw handleException(e); + } + } + + @Override + public Boolean delete(Alarm alarm, SecurityUser user) throws ThingsboardException { + List relatedEdgeIds = findRelatedEdgeIds(alarm.getTenantId(), alarm.getOriginator()); + notificationEntityService.notifyDeleteAlarm(alarm, user, relatedEdgeIds); + return alarmService.deleteAlarm(alarm.getTenantId(), alarm.getId()).isSuccessful(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java new file mode 100644 index 0000000000..7c3613d730 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.entitiy.alarm; + +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.service.security.model.SecurityUser; + +public interface TbAlarmService { + + Alarm save (Alarm alarm, SecurityUser user) throws ThingsboardException; + + void ack (Alarm alarm, SecurityUser user) throws ThingsboardException; + + void clear (Alarm alarm, SecurityUser user) throws ThingsboardException; + + Boolean delete (Alarm alarm, SecurityUser user) throws ThingsboardException; +} From 913e52abbc0da9c6bcc6ef77f949b5f0de6fb4e3 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Mon, 9 May 2022 23:15:15 +0300 Subject: [PATCH 108/276] refactoring: 01 - Alarm only --- .../server/controller/AlarmController.java | 76 ++++--------------- 1 file changed, 16 insertions(+), 60 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java index bdeec8d0ae..939e9da8a0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java @@ -17,6 +17,7 @@ package org.thingsboard.server.controller; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; +import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -29,29 +30,24 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; -import org.thingsboard.server.common.data.audit.ActionType; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AlarmId; -import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.alarm.TbAlarmService; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; -import java.util.List; - import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ALARM_INFO_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ALARM_SORT_PROPERTY_ALLOWABLE_VALUES; @@ -70,9 +66,12 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI @RestController @TbCoreComponent +@RequiredArgsConstructor @RequestMapping("/api") public class AlarmController extends BaseController { + private final TbAlarmService tbAlarmService; + public static final String ALARM_ID = "alarmId"; private static final String ALARM_SECURITY_CHECK = "If the user has the authority of 'Tenant Administrator', the server checks that the originator of alarm is owned by the same tenant. " + "If the user has the authority of 'Customer User', the server checks that the originator of alarm belongs to the customer. "; @@ -133,24 +132,9 @@ public class AlarmController extends BaseController { @RequestMapping(value = "/alarm", method = RequestMethod.POST) @ResponseBody public Alarm saveAlarm(@ApiParam(value = "A JSON value representing the alarm.") @RequestBody Alarm alarm) throws ThingsboardException { - try { - alarm.setTenantId(getCurrentUser().getTenantId()); - - checkEntity(alarm.getId(), alarm, Resource.ALARM); - - Alarm savedAlarm = checkNotNull(alarmService.createOrUpdateAlarm(alarm)); - logEntityAction(savedAlarm.getOriginator(), savedAlarm, - getCurrentUser().getCustomerId(), - alarm.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); - - sendEntityNotificationMsg(getTenantId(), savedAlarm.getId(), alarm.getId() == null ? EdgeEventActionType.ADDED : EdgeEventActionType.UPDATED); - - return savedAlarm; - } catch (Exception e) { - logEntityAction(emptyId(EntityType.ALARM), alarm, - null, alarm.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e); - throw handleException(e); - } + alarm.setTenantId(getCurrentUser().getTenantId()); + checkEntity(alarm.getId(), alarm, Resource.ALARM); + return tbAlarmService.save(alarm, getCurrentUser()); } @ApiOperation(value = "Delete Alarm (deleteAlarm)", @@ -163,16 +147,7 @@ public class AlarmController extends BaseController { try { AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); - - List relatedEdgeIds = findRelatedEdgeIds(getTenantId(), alarm.getOriginator()); - - logEntityAction(alarm.getOriginator(), alarm, - getCurrentUser().getCustomerId(), - ActionType.ALARM_DELETE, null); - - sendAlarmDeleteNotificationMsg(getTenantId(), alarmId, relatedEdgeIds, alarm); - - return alarmService.deleteAlarm(getTenantId(), alarmId); + return tbAlarmService.delete(alarm, getCurrentUser()); } catch (Exception e) { throw handleException(e); } @@ -187,19 +162,9 @@ public class AlarmController extends BaseController { @ResponseStatus(value = HttpStatus.OK) public void ackAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { checkParameter(ALARM_ID, strAlarmId); - try { - AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); - Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); - long ackTs = System.currentTimeMillis(); - alarmService.ackAlarm(getCurrentUser().getTenantId(), alarmId, ackTs).get(); - alarm.setAckTs(ackTs); - alarm.setStatus(alarm.getStatus().isCleared() ? AlarmStatus.CLEARED_ACK : AlarmStatus.ACTIVE_ACK); - logEntityAction(alarm.getOriginator(), alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_ACK, null); - - sendEntityNotificationMsg(getTenantId(), alarmId, EdgeEventActionType.ALARM_ACK); - } catch (Exception e) { - throw handleException(e); - } + AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); + Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); + tbAlarmService.ack(alarm, getCurrentUser()); } @ApiOperation(value = "Clear Alarm (clearAlarm)", @@ -211,19 +176,9 @@ public class AlarmController extends BaseController { @ResponseStatus(value = HttpStatus.OK) public void clearAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { checkParameter(ALARM_ID, strAlarmId); - try { - AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); - Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); - long clearTs = System.currentTimeMillis(); - alarmService.clearAlarm(getCurrentUser().getTenantId(), alarmId, null, clearTs).get(); - alarm.setClearTs(clearTs); - alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK); - logEntityAction(alarm.getOriginator(), alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_CLEAR, null); - - sendEntityNotificationMsg(getTenantId(), alarmId, EdgeEventActionType.ALARM_CLEAR); - } catch (Exception e) { - throw handleException(e); - } + AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); + Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); + tbAlarmService.clear(alarm, getCurrentUser()); } @ApiOperation(value = "Get Alarms (getAlarms)", @@ -276,6 +231,7 @@ public class AlarmController extends BaseController { throw handleException(e); } } + @ApiOperation(value = "Get All Alarms (getAllAlarms)", notes = "Returns a page of alarms that belongs to the current user owner. " + "If the user has the authority of 'Tenant Administrator', the server returns alarms that belongs to the tenant of current user. " + From 3382ac480778e77b34e5f88648efe80cec79483e Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 9 May 2022 23:28:04 +0200 Subject: [PATCH 109/276] created Queue validator --- .../dao/device/DeviceProfileServiceImpl.java | 3 - .../server/dao/queue/BaseQueueService.java | 92 +------------ .../dao/service/validator/QueueValidator.java | 121 ++++++++++++++++++ 3 files changed, 124 insertions(+), 92 deletions(-) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/service/validator/QueueValidator.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index f33a78ebd1..05551f885b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -71,9 +71,6 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D @Autowired private CacheManager cacheManager; - @Autowired(required = false) - private QueueService queueService; - @Autowired private DataValidator deviceProfileValidator; diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java index a908ab8cdc..a0254a4923 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java @@ -17,7 +17,6 @@ package org.thingsboard.server.dao.queue; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -26,10 +25,7 @@ import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.queue.ProcessingStrategy; import org.thingsboard.server.common.data.queue.Queue; -import org.thingsboard.server.common.data.queue.SubmitStrategy; -import org.thingsboard.server.common.data.queue.SubmitStrategyType; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; @@ -50,6 +46,9 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ @Autowired private TbTenantProfileCache tenantProfileCache; + @Autowired + private DataValidator queueValidator; + // @Autowired // private QueueStatsService queueStatsService; @@ -118,91 +117,6 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ tenantQueuesRemover.removeEntities(tenantId, tenantId); } - private DataValidator queueValidator = - new DataValidator<>() { - - @Override - protected void validateCreate(TenantId tenantId, Queue queue) { - if (queueDao.findQueueByTenantIdAndTopic(tenantId, queue.getTopic()) != null) { - throw new DataValidationException(String.format("Queue with topic: %s already exists!", queue.getTopic())); - } - if (queueDao.findQueueByTenantIdAndName(tenantId, queue.getName()) != null) { - throw new DataValidationException(String.format("Queue with name: %s already exists!", queue.getName())); - } - } - - @Override - protected void validateUpdate(TenantId tenantId, Queue queue) { - Queue foundQueue = queueDao.findById(tenantId, queue.getUuidId()); - if (queueDao.findById(tenantId, queue.getUuidId()) == null) { - throw new DataValidationException(String.format("Queue with id: %s does not exists!", queue.getId())); - } - if (!foundQueue.getName().equals(queue.getName())) { - throw new DataValidationException("Queue name can't be changed!"); - } - if (!foundQueue.getTopic().equals(queue.getTopic())) { - throw new DataValidationException("Queue topic can't be changed!"); - } - } - - @Override - protected void validateDataImpl(TenantId tenantId, Queue queue) { - if (!tenantId.equals(TenantId.SYS_TENANT_ID)) { - TenantProfile tenantProfile = tenantProfileCache.get(tenantId); - - if (!tenantProfile.isIsolatedTbRuleEngine()) { - throw new DataValidationException("Tenant should be isolated!"); - } - } - - if (StringUtils.isEmpty(queue.getName())) { - throw new DataValidationException("Queue name should be specified!"); - } - if (StringUtils.isBlank(queue.getTopic())) { - throw new DataValidationException("Queue topic should be non empty and without spaces!"); - } - if (queue.getPollInterval() < 1) { - throw new DataValidationException("Queue poll interval should be more then 0!"); - } - if (queue.getPartitions() < 1) { - throw new DataValidationException("Queue partitions should be more then 0!"); - } - if (queue.getPackProcessingTimeout() < 1) { - throw new DataValidationException("Queue pack processing timeout should be more then 0!"); - } - - SubmitStrategy submitStrategy = queue.getSubmitStrategy(); - if (submitStrategy == null) { - throw new DataValidationException("Queue submit strategy can't be null!"); - } - if (submitStrategy.getType() == null) { - throw new DataValidationException("Queue submit strategy type can't be null!"); - } - if (submitStrategy.getType() == SubmitStrategyType.BATCH && submitStrategy.getBatchSize() < 1) { - throw new DataValidationException("Queue submit strategy batch size should be more then 0!"); - } - ProcessingStrategy processingStrategy = queue.getProcessingStrategy(); - if (processingStrategy == null) { - throw new DataValidationException("Queue processing strategy can't be null!"); - } - if (processingStrategy.getType() == null) { - throw new DataValidationException("Queue processing strategy type can't be null!"); - } - if (processingStrategy.getRetries() < 0) { - throw new DataValidationException("Queue processing strategy retries can't be less then 0!"); - } - if (processingStrategy.getFailurePercentage() < 0 || processingStrategy.getFailurePercentage() > 100) { - throw new DataValidationException("Queue processing strategy failure percentage should be in a range from 0 to 100!"); - } - if (processingStrategy.getPauseBetweenRetries() < 0) { - throw new DataValidationException("Queue processing strategy pause between retries can't be less then 0!"); - } - if (processingStrategy.getMaxPauseBetweenRetries() < processingStrategy.getPauseBetweenRetries()) { - throw new DataValidationException("Queue processing strategy MAX pause between retries can't be less then pause between retries!"); - } - } - }; - private PaginatedRemover tenantQueuesRemover = new PaginatedRemover<>() { diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/QueueValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/QueueValidator.java new file mode 100644 index 0000000000..a9f9838d1c --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/QueueValidator.java @@ -0,0 +1,121 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.service.validator; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.queue.ProcessingStrategy; +import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.common.data.queue.SubmitStrategy; +import org.thingsboard.server.common.data.queue.SubmitStrategyType; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.queue.QueueDao; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; + +@Component +public class QueueValidator extends DataValidator { + + @Autowired + private QueueDao queueDao; + + @Autowired + private TbTenantProfileCache tenantProfileCache; + + @Override + protected void validateCreate(TenantId tenantId, Queue queue) { + if (queueDao.findQueueByTenantIdAndTopic(tenantId, queue.getTopic()) != null) { + throw new DataValidationException(String.format("Queue with topic: %s already exists!", queue.getTopic())); + } + if (queueDao.findQueueByTenantIdAndName(tenantId, queue.getName()) != null) { + throw new DataValidationException(String.format("Queue with name: %s already exists!", queue.getName())); + } + } + + @Override + protected void validateUpdate(TenantId tenantId, Queue queue) { + Queue foundQueue = queueDao.findById(tenantId, queue.getUuidId()); + if (queueDao.findById(tenantId, queue.getUuidId()) == null) { + throw new DataValidationException(String.format("Queue with id: %s does not exists!", queue.getId())); + } + if (!foundQueue.getName().equals(queue.getName())) { + throw new DataValidationException("Queue name can't be changed!"); + } + if (!foundQueue.getTopic().equals(queue.getTopic())) { + throw new DataValidationException("Queue topic can't be changed!"); + } + } + + @Override + protected void validateDataImpl(TenantId tenantId, Queue queue) { + if (!tenantId.equals(TenantId.SYS_TENANT_ID)) { + TenantProfile tenantProfile = tenantProfileCache.get(tenantId); + + if (!tenantProfile.isIsolatedTbRuleEngine()) { + throw new DataValidationException("Tenant should be isolated!"); + } + } + + if (StringUtils.isEmpty(queue.getName())) { + throw new DataValidationException("Queue name should be specified!"); + } + if (StringUtils.isBlank(queue.getTopic())) { + throw new DataValidationException("Queue topic should be non empty and without spaces!"); + } + if (queue.getPollInterval() < 1) { + throw new DataValidationException("Queue poll interval should be more then 0!"); + } + if (queue.getPartitions() < 1) { + throw new DataValidationException("Queue partitions should be more then 0!"); + } + if (queue.getPackProcessingTimeout() < 1) { + throw new DataValidationException("Queue pack processing timeout should be more then 0!"); + } + + SubmitStrategy submitStrategy = queue.getSubmitStrategy(); + if (submitStrategy == null) { + throw new DataValidationException("Queue submit strategy can't be null!"); + } + if (submitStrategy.getType() == null) { + throw new DataValidationException("Queue submit strategy type can't be null!"); + } + if (submitStrategy.getType() == SubmitStrategyType.BATCH && submitStrategy.getBatchSize() < 1) { + throw new DataValidationException("Queue submit strategy batch size should be more then 0!"); + } + ProcessingStrategy processingStrategy = queue.getProcessingStrategy(); + if (processingStrategy == null) { + throw new DataValidationException("Queue processing strategy can't be null!"); + } + if (processingStrategy.getType() == null) { + throw new DataValidationException("Queue processing strategy type can't be null!"); + } + if (processingStrategy.getRetries() < 0) { + throw new DataValidationException("Queue processing strategy retries can't be less then 0!"); + } + if (processingStrategy.getFailurePercentage() < 0 || processingStrategy.getFailurePercentage() > 100) { + throw new DataValidationException("Queue processing strategy failure percentage should be in a range from 0 to 100!"); + } + if (processingStrategy.getPauseBetweenRetries() < 0) { + throw new DataValidationException("Queue processing strategy pause between retries can't be less then 0!"); + } + if (processingStrategy.getMaxPauseBetweenRetries() < processingStrategy.getPauseBetweenRetries()) { + throw new DataValidationException("Queue processing strategy MAX pause between retries can't be less then pause between retries!"); + } + } +} From d9d68a068287e44ca6d702699dc5152fcf0f0510 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Tue, 10 May 2022 09:42:36 +0300 Subject: [PATCH 110/276] Asset Cache implementation --- .../dao/asset/AssetCacheEvictEvent.java | 33 ++++++++++++ .../server/dao/asset/AssetCacheKey.java | 42 +++++++++++++++ .../server/dao/asset/AssetCaffeineCache.java | 35 +++++++++++++ .../server/dao/asset/AssetRedisCache.java | 51 +++++++++++++++++++ .../server/dao/asset/BaseAssetService.java | 44 ++++++++++------ ...eEvent.java => DeviceCacheEvictEvent.java} | 6 +-- .../server/dao/device/DeviceServiceImpl.java | 34 +++++-------- 7 files changed, 207 insertions(+), 38 deletions(-) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/asset/AssetCacheEvictEvent.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/asset/AssetCacheKey.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/asset/AssetCaffeineCache.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/asset/AssetRedisCache.java rename dao/src/main/java/org/thingsboard/server/dao/device/{DeviceEvent.java => DeviceCacheEvictEvent.java} (89%) diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCacheEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCacheEvictEvent.java new file mode 100644 index 0000000000..faaf3945a0 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCacheEvictEvent.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.asset; + +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +@RequiredArgsConstructor +class AssetCacheEvictEvent { + + private final TenantId tenantId; + private final String newName; + private final String oldName; + + public AssetCacheEvictEvent(TenantId tenantId, String newName) { + this(tenantId, newName, null); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCacheKey.java new file mode 100644 index 0000000000..70e17e28ff --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCacheKey.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.asset; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.thingsboard.server.cache.CacheKeyUtil; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.io.Serializable; + +@Getter +@EqualsAndHashCode +@RequiredArgsConstructor +@Builder +public class AssetCacheKey implements Serializable { + + private final TenantId tenantId; + private final String name; + + @Override + public String toString() { + return CacheKeyUtil.toString(tenantId, name); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCaffeineCache.java new file mode 100644 index 0000000000..3d64db46b0 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCaffeineCache.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.asset; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.dao.device.DeviceCacheKey; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("AssetCache") +public class AssetCaffeineCache extends CaffeineTbTransactionalCache { + + public AssetCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.ASSET_CACHE); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetRedisCache.java new file mode 100644 index 0000000000..864d5cf1e5 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetRedisCache.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.asset; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; +import org.thingsboard.server.dao.device.DeviceCacheKey; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("AssetCache") +public class AssetRedisCache extends RedisTbTransactionalCache { + + public AssetRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.ASSET_CACHE, cacheSpecsMap, connectionFactory, configuration, new RedisSerializer<>() { + + private final RedisSerializer java = RedisSerializer.java(); + + @Override + public byte[] serialize(Asset attributeKvEntry) throws SerializationException { + return java.serialize(attributeKvEntry); + } + + @Override + public Asset deserialize(byte[] bytes) throws SerializationException { + return (Asset) java.deserialize(bytes); + } + }); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java index bf8d75d27a..a7b408b47e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java @@ -22,12 +22,14 @@ import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cache.annotation.CacheEvict; -import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetSearchQuery; @@ -42,8 +44,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationTypeGroup; -import org.thingsboard.server.dao.cache.EntitiesCacheManager; -import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; @@ -55,7 +56,6 @@ import java.util.List; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; -import static org.thingsboard.server.common.data.CacheConstants.ASSET_CACHE; import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateIds; @@ -64,7 +64,7 @@ import static org.thingsboard.server.dao.service.Validator.validateString; @Service @Slf4j -public class BaseAssetService extends AbstractEntityService implements AssetService { +public class BaseAssetService extends AbstractCachedEntityService implements AssetService { public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId "; @@ -74,12 +74,20 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ @Autowired private AssetDao assetDao; - @Autowired - private EntitiesCacheManager cacheManager; - @Autowired private DataValidator assetValidator; + @TransactionalEventListener(classes = AssetCacheEvictEvent.class) + @Override + public void handleEvictEvent(AssetCacheEvictEvent event) { + List keys = new ArrayList<>(2); + keys.add(new AssetCacheKey(event.getTenantId(), event.getNewName())); + if (StringUtils.isNotEmpty(event.getOldName()) && !event.getOldName().equals(event.getNewName())) { + keys.add(new AssetCacheKey(event.getTenantId(), event.getOldName())); + } + cache.evict(keys); + } + @Override public AssetInfo findAssetInfoById(TenantId tenantId, AssetId assetId) { log.trace("Executing findAssetInfoById [{}]", assetId); @@ -101,23 +109,26 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ return assetDao.findByIdAsync(tenantId, assetId.getId()); } - @Cacheable(cacheNames = ASSET_CACHE, key = "{#tenantId, #name}") @Override public Asset findAssetByTenantIdAndName(TenantId tenantId, String name) { log.trace("Executing findAssetByTenantIdAndName [{}][{}]", tenantId, name); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - return assetDao.findAssetsByTenantIdAndName(tenantId.getId(), name) - .orElse(null); + return cache.getAndPutInTransaction(new AssetCacheKey(tenantId, name), + () -> assetDao.findAssetsByTenantIdAndName(tenantId.getId(), name) + .orElse(null), true); + } - @CacheEvict(cacheNames = ASSET_CACHE, key = "{#asset.tenantId, #asset.name}") + @Transactional(propagation = Propagation.SUPPORTS) @Override public Asset saveAsset(Asset asset) { log.trace("Executing saveAsset [{}]", asset); - assetValidator.validate(asset, Asset::getTenantId); + Asset oldAsset = assetValidator.validate(asset, Asset::getTenantId); Asset savedAsset; + AssetCacheEvictEvent evictEvent = new AssetCacheEvictEvent(asset.getTenantId(), asset.getName(), oldAsset != null ? oldAsset.getName() : null); try { savedAsset = assetDao.save(asset.getTenantId(), asset); + publishEvictEvent(evictEvent); } catch (Exception t) { ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("asset_name_unq_key")) { @@ -129,6 +140,7 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ return savedAsset; } + @Transactional(propagation = Propagation.SUPPORTS) @Override public Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, CustomerId customerId) { Asset asset = findAssetById(tenantId, assetId); @@ -136,6 +148,7 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ return saveAsset(asset); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public Asset unassignAssetFromCustomer(TenantId tenantId, AssetId assetId) { Asset asset = findAssetById(tenantId, assetId); @@ -143,6 +156,7 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ return saveAsset(asset); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public void deleteAsset(TenantId tenantId, AssetId assetId) { log.trace("Executing deleteAsset [{}]", assetId); @@ -160,7 +174,7 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ throw new RuntimeException("Exception while finding entity views for assetId [" + assetId + "]", e); } - cacheManager.removeAssetFromCacheByName(asset.getTenantId(), asset.getName()); + publishEvictEvent(new AssetCacheEvictEvent(asset.getTenantId(), asset.getName(), null)); assetDao.removeById(tenantId, assetId.getId()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceEvent.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCacheEvictEvent.java similarity index 89% rename from dao/src/main/java/org/thingsboard/server/dao/device/DeviceEvent.java rename to dao/src/main/java/org/thingsboard/server/dao/device/DeviceCacheEvictEvent.java index 51ad354ea3..b6f8ab4beb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceEvent.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCacheEvictEvent.java @@ -20,11 +20,11 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; @Data -public class DeviceEvent { +class DeviceCacheEvictEvent { private final TenantId tenantId; private final DeviceId deviceId; - private final String newDeviceName; - private final String oldDeviceName; + private final String newName; + private final String oldName; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index 7d12d505d5..e537c1d71b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -23,15 +23,12 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.RandomStringUtils; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cache.annotation.CacheEvict; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; -import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.CollectionUtils; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.DeviceProfile; @@ -69,7 +66,6 @@ import org.thingsboard.server.dao.device.provision.ProvisionFailedException; import org.thingsboard.server.dao.device.provision.ProvisionRequest; import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; -import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; @@ -84,7 +80,6 @@ import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; -import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CACHE; import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateIds; @@ -93,7 +88,7 @@ import static org.thingsboard.server.dao.service.Validator.validateString; @Service @Slf4j -public class DeviceServiceImpl extends AbstractCachedEntityService implements DeviceService { +public class DeviceServiceImpl extends AbstractCachedEntityService implements DeviceService { public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; public static final String INCORRECT_DEVICE_PROFILE_ID = "Incorrect deviceProfileId "; @@ -222,7 +217,7 @@ public class DeviceServiceImpl extends AbstractCachedEntityService keys = new ArrayList<>(3); - keys.add(new DeviceCacheKey(event.getTenantId(), event.getNewDeviceName())); + keys.add(new DeviceCacheKey(event.getTenantId(), event.getNewName())); if (event.getDeviceId() != null) { keys.add(new DeviceCacheKey(event.getTenantId(), event.getDeviceId())); } - if (StringUtils.isNotEmpty(event.getOldDeviceName()) && !event.getOldDeviceName().equals(event.getNewDeviceName())) { - keys.add(new DeviceCacheKey(event.getTenantId(), event.getOldDeviceName())); + if (StringUtils.isNotEmpty(event.getOldName()) && !event.getOldName().equals(event.getNewName())) { + keys.add(new DeviceCacheKey(event.getTenantId(), event.getOldName())); } cache.evict(keys); } @@ -323,7 +318,7 @@ public class DeviceServiceImpl extends AbstractCachedEntityService entityViews = entityViewService.findEntityViewsByTenantIdAndEntityIdAsync(device.getTenantId(), deviceId).get(); if (entityViews != null && !entityViews.isEmpty()) { @@ -342,7 +337,7 @@ public class DeviceServiceImpl extends AbstractCachedEntityService Date: Tue, 10 May 2022 10:20:12 +0300 Subject: [PATCH 111/276] Tenant Profile Cache --- .../dao/tenant/TenantProfileCacheKey.java | 38 +++++++++ .../tenant/TenantProfileCaffeineCache.java | 35 ++++++++ .../dao/tenant/TenantProfileEvictEvent.java | 10 +++ .../dao/tenant/TenantProfileRedisCache.java | 51 ++++++++++++ .../dao/tenant/TenantProfileServiceImpl.java | 80 +++++++++---------- 5 files changed, 171 insertions(+), 43 deletions(-) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCacheKey.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCaffeineCache.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileEvictEvent.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileRedisCache.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCacheKey.java new file mode 100644 index 0000000000..719c7daa0d --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCacheKey.java @@ -0,0 +1,38 @@ +package org.thingsboard.server.dao.tenant; + +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantProfileId; + +import java.io.Serializable; + +@Data +public class TenantProfileCacheKey implements Serializable { + + private static final long serialVersionUID = 8220455917177676472L; + + private final TenantProfileId tenantProfileId; + private final boolean defaultProfile; + + private TenantProfileCacheKey(TenantProfileId tenantProfileId, boolean defaultProfile) { + this.tenantProfileId = tenantProfileId; + this.defaultProfile = defaultProfile; + } + + public static TenantProfileCacheKey fromId(TenantProfileId id) { + return new TenantProfileCacheKey(id, false); + } + + public static TenantProfileCacheKey defaultProfile() { + return new TenantProfileCacheKey(null, true); + } + + + @Override + public String toString() { + if (defaultProfile) { + return "default"; + } else { + return tenantProfileId.getId().toString(); + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCaffeineCache.java new file mode 100644 index 0000000000..b9a9426172 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCaffeineCache.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.tenant; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.dao.asset.AssetCacheKey; +import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("TenantProfileCache") +public class TenantProfileCaffeineCache extends CaffeineTbTransactionalCache { + + public TenantProfileCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.TENANT_PROFILE_CACHE); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileEvictEvent.java new file mode 100644 index 0000000000..cb80987f65 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileEvictEvent.java @@ -0,0 +1,10 @@ +package org.thingsboard.server.dao.tenant; + +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantProfileId; + +@Data +public class TenantProfileEvictEvent { + private final TenantProfileId tenantProfileId; + private final boolean defaultProfile; +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileRedisCache.java new file mode 100644 index 0000000000..2348d405ff --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileRedisCache.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.tenant; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.dao.asset.AssetCacheKey; +import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("TenantProfileCache") +public class TenantProfileRedisCache extends RedisTbTransactionalCache { + + public TenantProfileRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.TENANT_PROFILE_CACHE, cacheSpecsMap, connectionFactory, configuration, new RedisSerializer<>() { + + private final RedisSerializer java = RedisSerializer.java(); + + @Override + public byte[] serialize(TenantProfile attributeKvEntry) throws SerializationException { + return java.serialize(attributeKvEntry); + } + + @Override + public TenantProfile deserialize(byte[] bytes) throws SerializationException { + return (TenantProfile) java.deserialize(bytes); + } + }); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java index 0976c49410..e726b33865 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -18,10 +18,11 @@ package org.thingsboard.server.dao.tenant; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cache.Cache; -import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.TenantId; @@ -30,49 +31,57 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; -import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; -import java.util.Arrays; -import java.util.Collections; +import java.util.ArrayList; +import java.util.List; import static org.thingsboard.server.common.data.CacheConstants.TENANT_PROFILE_CACHE; import static org.thingsboard.server.dao.service.Validator.validateId; @Service @Slf4j -public class TenantProfileServiceImpl extends AbstractEntityService implements TenantProfileService { +public class TenantProfileServiceImpl extends AbstractCachedEntityService implements TenantProfileService { private static final String INCORRECT_TENANT_PROFILE_ID = "Incorrect tenantProfileId "; @Autowired private TenantProfileDao tenantProfileDao; - @Autowired - private CacheManager cacheManager; - @Autowired private DataValidator tenantProfileValidator; - @Cacheable(cacheNames = TENANT_PROFILE_CACHE, key = "{#tenantProfileId.id}") + @TransactionalEventListener(classes = TenantProfileEvictEvent.class) + @Override + public void handleEvictEvent(TenantProfileEvictEvent event) { + List keys = new ArrayList<>(2); + keys.add(TenantProfileCacheKey.fromId(event.getTenantProfileId())); + if (event.isDefaultProfile()) { + keys.add(TenantProfileCacheKey.defaultProfile()); + } + cache.evict(keys); + } + @Override public TenantProfile findTenantProfileById(TenantId tenantId, TenantProfileId tenantProfileId) { log.trace("Executing findTenantProfileById [{}]", tenantProfileId); Validator.validateId(tenantProfileId, INCORRECT_TENANT_PROFILE_ID + tenantProfileId); - return tenantProfileDao.findById(tenantId, tenantProfileId.getId()); + return cache.getAndPutInTransaction(TenantProfileCacheKey.fromId(tenantProfileId), + () -> tenantProfileDao.findById(tenantId, tenantProfileId.getId()), true); } - @Cacheable(cacheNames = TENANT_PROFILE_CACHE, key = "{'info', #tenantProfileId.id}") @Override public EntityInfo findTenantProfileInfoById(TenantId tenantId, TenantProfileId tenantProfileId) { log.trace("Executing findTenantProfileInfoById [{}]", tenantProfileId); - Validator.validateId(tenantProfileId, INCORRECT_TENANT_PROFILE_ID + tenantProfileId); - return tenantProfileDao.findTenantProfileInfoById(tenantId, tenantProfileId.getId()); + TenantProfile profile = findTenantProfileById(tenantId, tenantProfileId); + return profile == null ? null : new EntityInfo(profile.getId(), profile.getName()); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public TenantProfile saveTenantProfile(TenantId tenantId, TenantProfile tenantProfile) { log.trace("Executing saveTenantProfile [{}]", tenantProfile); @@ -80,6 +89,7 @@ public class TenantProfileServiceImpl extends AbstractEntityService implements T TenantProfile savedTenantProfile; try { savedTenantProfile = tenantProfileDao.save(tenantId, tenantProfile); + publishEvictEvent(new TenantProfileEvictEvent(savedTenantProfile.getId(), savedTenantProfile.isDefault())); } catch (Exception t) { ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("tenant_profile_name_unq_key")) { @@ -88,16 +98,10 @@ public class TenantProfileServiceImpl extends AbstractEntityService implements T throw t; } } - Cache cache = cacheManager.getCache(TENANT_PROFILE_CACHE); - cache.evict(Collections.singletonList(savedTenantProfile.getId().getId())); - cache.evict(Arrays.asList("info", savedTenantProfile.getId().getId())); - if (savedTenantProfile.isDefault()) { - cache.evict(Collections.singletonList("default")); - cache.evict(Arrays.asList("default", "info")); - } return savedTenantProfile; } + @Transactional(propagation = Propagation.SUPPORTS) @Override public void deleteTenantProfile(TenantId tenantId, TenantProfileId tenantProfileId) { log.trace("Executing deleteTenantProfile [{}]", tenantProfileId); @@ -121,13 +125,7 @@ public class TenantProfileServiceImpl extends AbstractEntityService implements T } } deleteEntityRelations(tenantId, tenantProfileId); - Cache cache = cacheManager.getCache(TENANT_PROFILE_CACHE); - cache.evict(Collections.singletonList(tenantProfileId.getId())); - cache.evict(Arrays.asList("info", tenantProfileId.getId())); - if (isDefault) { - cache.evict(Collections.singletonList("default")); - cache.evict(Arrays.asList("default", "info")); - } + publishEvictEvent(new TenantProfileEvictEvent(tenantProfileId, isDefault)); } @Override @@ -163,47 +161,43 @@ public class TenantProfileServiceImpl extends AbstractEntityService implements T return defaultTenantProfile; } - @Cacheable(cacheNames = TENANT_PROFILE_CACHE, key = "{'default'}") @Override public TenantProfile findDefaultTenantProfile(TenantId tenantId) { log.trace("Executing findDefaultTenantProfile"); - return tenantProfileDao.findDefaultTenantProfile(tenantId); + return cache.getAndPutInTransaction(TenantProfileCacheKey.defaultProfile(), + () -> tenantProfileDao.findDefaultTenantProfile(tenantId), true); + } - @Cacheable(cacheNames = TENANT_PROFILE_CACHE, key = "{'default', 'info'}") @Override public EntityInfo findDefaultTenantProfileInfo(TenantId tenantId) { log.trace("Executing findDefaultTenantProfileInfo"); - return tenantProfileDao.findDefaultTenantProfileInfo(tenantId); + var tenantProfile = findDefaultTenantProfile(tenantId); + return tenantProfile == null ? null : new EntityInfo(tenantProfile.getId(), tenantProfile.getName()); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public boolean setDefaultTenantProfile(TenantId tenantId, TenantProfileId tenantProfileId) { log.trace("Executing setDefaultTenantProfile [{}]", tenantProfileId); validateId(tenantId, INCORRECT_TENANT_PROFILE_ID + tenantProfileId); TenantProfile tenantProfile = tenantProfileDao.findById(tenantId, tenantProfileId.getId()); if (!tenantProfile.isDefault()) { - Cache cache = cacheManager.getCache(TENANT_PROFILE_CACHE); tenantProfile.setDefault(true); TenantProfile previousDefaultTenantProfile = findDefaultTenantProfile(tenantId); boolean changed = false; if (previousDefaultTenantProfile == null) { tenantProfileDao.save(tenantId, tenantProfile); + publishEvictEvent(new TenantProfileEvictEvent(tenantProfileId, true)); changed = true; } else if (!previousDefaultTenantProfile.getId().equals(tenantProfile.getId())) { previousDefaultTenantProfile.setDefault(false); tenantProfileDao.save(tenantId, previousDefaultTenantProfile); tenantProfileDao.save(tenantId, tenantProfile); - cache.evict(Collections.singletonList(previousDefaultTenantProfile.getId().getId())); - cache.evict(Arrays.asList("info", previousDefaultTenantProfile.getId().getId())); + publishEvictEvent(new TenantProfileEvictEvent(previousDefaultTenantProfile.getId(), false)); + publishEvictEvent(new TenantProfileEvictEvent(tenantProfileId, true)); changed = true; } - if (changed) { - cache.evict(Collections.singletonList(tenantProfile.getId().getId())); - cache.evict(Arrays.asList("info", tenantProfile.getId().getId())); - cache.evict(Collections.singletonList("default")); - cache.evict(Arrays.asList("default", "info")); - } return changed; } return false; @@ -216,7 +210,7 @@ public class TenantProfileServiceImpl extends AbstractEntityService implements T } private final PaginatedRemover tenantProfilesRemover = - new PaginatedRemover() { + new PaginatedRemover<>() { @Override protected PageData findEntities(TenantId tenantId, String id, PageLink pageLink) { From 0ff53a928dd528c42a04aa82cd93f8e4ded59884 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Tue, 10 May 2022 10:56:08 +0300 Subject: [PATCH 112/276] Entity View Cache --- .../dao/entityview/EntityViewCacheKey.java | 63 +++++++++++ .../dao/entityview/EntityViewCacheValue.java | 40 +++++++ .../entityview/EntityViewCaffeineCache.java | 34 ++++++ .../server/dao/entityview/EntityViewDao.java | 2 +- .../dao/entityview/EntityViewEvictEvent.java | 35 ++++++ .../dao/entityview/EntityViewRedisCache.java | 50 +++++++++ .../dao/entityview/EntityViewServiceImpl.java | 104 ++++++++---------- .../dao/sql/entityview/JpaEntityViewDao.java | 6 +- .../dao/tenant/TenantProfileCacheKey.java | 15 +++ .../dao/tenant/TenantProfileEvictEvent.java | 15 +++ .../dao/tenant/TenantProfileRedisCache.java | 2 +- .../dao/tenant/TenantProfileServiceImpl.java | 2 +- 12 files changed, 303 insertions(+), 65 deletions(-) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheKey.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheValue.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCaffeineCache.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewEvictEvent.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewRedisCache.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheKey.java new file mode 100644 index 0000000000..6365fb6b45 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheKey.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.entityview; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.thingsboard.server.cache.CacheKeyUtil; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.io.Serializable; + +@Getter +@EqualsAndHashCode +@Builder +public class EntityViewCacheKey implements Serializable { + + private final TenantId tenantId; + private final String name; + private final EntityId entityId; + private final EntityViewId entityViewId; + + private EntityViewCacheKey(TenantId tenantId, String name, EntityId entityId, EntityViewId entityViewId) { + this.tenantId = tenantId; + this.name = name; + this.entityId = entityId; + this.entityViewId = entityViewId; + } + + public static EntityViewCacheKey byName(TenantId tenantId, String name) { + return new EntityViewCacheKey(tenantId, name, null, null); + } + + public static EntityViewCacheKey byEntityId(TenantId tenantId, EntityId entityId) { + return new EntityViewCacheKey(tenantId, null, entityId, null); + } + + public static EntityViewCacheKey byId(EntityViewId id) { + return new EntityViewCacheKey(null, null, null, id); + } + + @Override + public String toString() { + return CacheKeyUtil.toString(tenantId, name, entityId, entityViewId); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheValue.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheValue.java new file mode 100644 index 0000000000..ea0f85040d --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheValue.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.entityview; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.thingsboard.server.cache.CacheKeyUtil; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.io.Serializable; +import java.util.List; + +@Getter +@EqualsAndHashCode +@Builder +public class EntityViewCacheValue implements Serializable { + + private static final long serialVersionUID = 1959004642076413174L; + + private final EntityView entityView; + private final List entityViews; + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCaffeineCache.java new file mode 100644 index 0000000000..dd05d7078a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCaffeineCache.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.entityview; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.dao.asset.AssetCacheKey; +import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("EntityViewCache") +public class EntityViewCaffeineCache extends CaffeineTbTransactionalCache { + + public EntityViewCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.ENTITY_VIEW_CACHE); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java index e6020824cb..f44bafe937 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java @@ -144,7 +144,7 @@ public interface EntityViewDao extends Dao { */ PageData findEntityViewInfosByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink); - ListenableFuture> findEntityViewsByTenantIdAndEntityIdAsync(UUID tenantId, UUID entityId); + List findEntityViewsByTenantIdAndEntityId(UUID tenantId, UUID entityId); /** * Find tenants entity view types. diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewEvictEvent.java new file mode 100644 index 0000000000..012377b67d --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewEvictEvent.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.entityview; + +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +@RequiredArgsConstructor +class EntityViewEvictEvent { + + private final TenantId tenantId; + private final EntityViewId id; + private final EntityId newEntityId; + private final EntityId oldEntityId; + private final String newName; + private final String oldName; + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewRedisCache.java new file mode 100644 index 0000000000..21da2337f9 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewRedisCache.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.entityview; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.dao.asset.AssetCacheKey; +import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("EntityViewCache") +public class EntityViewRedisCache extends RedisTbTransactionalCache { + + public EntityViewRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.ENTITY_VIEW_CACHE, cacheSpecsMap, connectionFactory, configuration, new RedisSerializer<>() { + + private final RedisSerializer java = RedisSerializer.java(); + + @Override + public byte[] serialize(EntityViewCacheValue attributeKvEntry) throws SerializationException { + return java.serialize(attributeKvEntry); + } + + @Override + public EntityViewCacheValue deserialize(byte[] bytes) throws SerializationException { + return (EntityViewCacheValue) java.deserialize(bytes); + } + }); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java index 5705c463cb..98eb827837 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java @@ -16,20 +16,20 @@ package org.thingsboard.server.dao.entityview; import com.google.common.base.Function; -import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cache.Cache; -import org.springframework.cache.CacheManager; -import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityViewInfo; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.id.CustomerId; @@ -42,22 +42,20 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationTypeGroup; -import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; +import org.thingsboard.server.dao.sql.JpaExecutorService; import javax.annotation.Nullable; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; -import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; -import static org.thingsboard.server.common.data.CacheConstants.ENTITY_VIEW_CACHE; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validatePageLink; import static org.thingsboard.server.dao.service.Validator.validateString; @@ -67,7 +65,7 @@ import static org.thingsboard.server.dao.service.Validator.validateString; */ @Service @Slf4j -public class EntityViewServiceImpl extends AbstractEntityService implements EntityViewService { +public class EntityViewServiceImpl extends AbstractCachedEntityService implements EntityViewService { public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; public static final String INCORRECT_PAGE_LINK = "Incorrect page link "; @@ -79,40 +77,54 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti private EntityViewDao entityViewDao; @Autowired - private CacheManager cacheManager; + private DataValidator entityViewValidator; @Autowired - private DataValidator entityViewValidator; + protected JpaExecutorService service; + @TransactionalEventListener(classes = EntityViewEvictEvent.class) @Override - public EntityView saveEntityView(EntityView entityView) { - log.trace("Executing save entity view [{}]", entityView); - entityViewValidator.validate(entityView, EntityView::getTenantId); - - if (entityView.getId() != null) { - EntityView oldEntityView = entityViewDao.findById(entityView.getTenantId(), entityView.getUuidId()); - evictCache(oldEntityView); + public void handleEvictEvent(EntityViewEvictEvent event) { + List keys = new ArrayList<>(5); + keys.add(EntityViewCacheKey.byName(event.getTenantId(), event.getNewName())); + keys.add(EntityViewCacheKey.byId(event.getId())); + keys.add(EntityViewCacheKey.byEntityId(event.getTenantId(), event.getNewEntityId())); + if (event.getOldEntityId() != null && !event.getOldEntityId().equals(event.getNewEntityId())) { + keys.add(EntityViewCacheKey.byEntityId(event.getTenantId(), event.getOldEntityId())); + } + if (StringUtils.isNotEmpty(event.getOldName()) && !event.getOldName().equals(event.getNewName())) { + keys.add(EntityViewCacheKey.byName(event.getTenantId(), event.getOldName())); } + cache.evict(keys); + } - return entityViewDao.save(entityView.getTenantId(), entityView); + @Transactional(propagation = Propagation.SUPPORTS) + @Override + public EntityView saveEntityView(EntityView entityView) { + log.trace("Executing save entity view [{}]", entityView); + EntityView old = entityViewValidator.validate(entityView, EntityView::getTenantId); + EntityView saved = entityViewDao.save(entityView.getTenantId(), entityView); + publishEvictEvent(new EntityViewEvictEvent(saved.getTenantId(), saved.getId(), saved.getEntityId(), old != null ? old.getEntityId() : null, saved.getName(), old != null ? old.getName() : null)); + return saved; } + @Transactional(propagation = Propagation.SUPPORTS) @Override public EntityView assignEntityViewToCustomer(TenantId tenantId, EntityViewId entityViewId, CustomerId customerId) { EntityView entityView = findEntityViewById(tenantId, entityViewId); - evictCache(entityView); entityView.setCustomerId(customerId); return saveEntityView(entityView); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public EntityView unassignEntityViewFromCustomer(TenantId tenantId, EntityViewId entityViewId) { EntityView entityView = findEntityViewById(tenantId, entityViewId); - evictCache(entityView); entityView.setCustomerId(null); return saveEntityView(entityView); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public void unassignCustomerEntityViews(TenantId tenantId, CustomerId customerId) { log.trace("Executing unassignCustomerEntityViews, tenantId [{}], customerId [{}]", tenantId, customerId); @@ -128,21 +140,23 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti return entityViewDao.findEntityViewInfoById(tenantId, entityViewId.getId()); } - @Cacheable(cacheNames = ENTITY_VIEW_CACHE, key = "{#entityViewId}") @Override public EntityView findEntityViewById(TenantId tenantId, EntityViewId entityViewId) { log.trace("Executing findEntityViewById [{}]", entityViewId); validateId(entityViewId, INCORRECT_ENTITY_VIEW_ID + entityViewId); - return entityViewDao.findById(tenantId, entityViewId.getId()); + return cache.getAndPutInTransaction(EntityViewCacheKey.byId(entityViewId), + () -> entityViewDao.findById(tenantId, entityViewId.getId()) + , EntityViewCacheValue::getEntityView, v -> new EntityViewCacheValue(v, null), true); } - @Cacheable(cacheNames = ENTITY_VIEW_CACHE, key = "{#tenantId, #name}") @Override public EntityView findEntityViewByTenantIdAndName(TenantId tenantId, String name) { log.trace("Executing findEntityViewByTenantIdAndName [{}][{}]", tenantId, name); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - Optional entityViewOpt = entityViewDao.findEntityViewByTenantIdAndName(tenantId.getId(), name); - return entityViewOpt.orElse(null); + return cache.getAndPutInTransaction(EntityViewCacheKey.byName(tenantId, name), + () -> entityViewDao.findEntityViewByTenantIdAndName(tenantId.getId(), name).orElse(null) + , EntityViewCacheValue::getEntityView, v -> new EntityViewCacheValue(v, null), true); + } @Override @@ -265,41 +279,20 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti validateId(tenantId, INCORRECT_TENANT_ID + tenantId); validateId(entityId.getId(), "Incorrect entityId" + entityId); - List tenantIdAndEntityId = new ArrayList<>(); - tenantIdAndEntityId.add(tenantId); - tenantIdAndEntityId.add(entityId); - - Cache cache = cacheManager.getCache(ENTITY_VIEW_CACHE); - @SuppressWarnings("unchecked") - List fromCache = cache.get(tenantIdAndEntityId, List.class); - if (fromCache != null) { - return Futures.immediateFuture(fromCache); - } else { - ListenableFuture> entityViewsFuture = entityViewDao.findEntityViewsByTenantIdAndEntityIdAsync(tenantId.getId(), entityId.getId()); - Futures.addCallback(entityViewsFuture, - new FutureCallback>() { - @Override - public void onSuccess(@Nullable List result) { - cache.putIfAbsent(tenantIdAndEntityId, result); - } - - @Override - public void onFailure(Throwable t) { - log.error("Error while finding entity views by tenantId and entityId", t); - } - }, MoreExecutors.directExecutor()); - return entityViewsFuture; - } + return service.submit(() -> cache.getAndPutInTransaction(EntityViewCacheKey.byEntityId(tenantId, entityId), + () -> entityViewDao.findEntityViewsByTenantIdAndEntityId(tenantId.getId(), entityId.getId()), + EntityViewCacheValue::getEntityViews, v -> new EntityViewCacheValue(null, v), true)); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public void deleteEntityView(TenantId tenantId, EntityViewId entityViewId) { log.trace("Executing deleteEntityView [{}]", entityViewId); validateId(entityViewId, INCORRECT_ENTITY_VIEW_ID + entityViewId); deleteEntityRelations(tenantId, entityViewId); EntityView entityView = entityViewDao.findById(tenantId, entityViewId.getId()); - evictCache(entityView); entityViewDao.removeById(tenantId, entityViewId.getId()); + publishEvictEvent(new EntityViewEvictEvent(entityView.getTenantId(), entityView.getId(), entityView.getEntityId(), null, entityView.getName(), null)); } @Override @@ -410,11 +403,4 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti unassignEntityViewFromCustomer(tenantId, new EntityViewId(entity.getUuidId())); } }; - - private void evictCache(EntityView entityView) { - Cache cache = cacheManager.getCache(ENTITY_VIEW_CACHE); - cache.evict(Arrays.asList(entityView.getTenantId(), entityView.getEntityId())); - cache.evict(Arrays.asList(entityView.getTenantId(), entityView.getName())); - cache.evict(Arrays.asList(entityView.getId())); - } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java index 8c515f13c0..28bfe52e75 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java @@ -157,9 +157,9 @@ public class JpaEntityViewDao extends JpaAbstractSearchTextDao> findEntityViewsByTenantIdAndEntityIdAsync(UUID tenantId, UUID entityId) { - return service.submit(() -> DaoUtil.convertDataList( - entityViewRepository.findAllByTenantIdAndEntityId(tenantId, entityId))); + public List findEntityViewsByTenantIdAndEntityId(UUID tenantId, UUID entityId) { + return DaoUtil.convertDataList( + entityViewRepository.findAllByTenantIdAndEntityId(tenantId, entityId)); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCacheKey.java index 719c7daa0d..2c921d2f2c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCacheKey.java @@ -1,3 +1,18 @@ +/** + * Copyright © 2016-2022 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. + */ package org.thingsboard.server.dao.tenant; import lombok.Data; diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileEvictEvent.java index cb80987f65..be5e2cf64d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileEvictEvent.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileEvictEvent.java @@ -1,3 +1,18 @@ +/** + * Copyright © 2016-2022 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. + */ package org.thingsboard.server.dao.tenant; import lombok.Data; diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileRedisCache.java index 2348d405ff..6ed750b480 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileRedisCache.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java index e726b33865..97226df0fc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java @@ -5,7 +5,7 @@ * 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 + * 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, From 29396864f25d80cab793a1b63569c20e51cfcf53 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Tue, 10 May 2022 11:47:09 +0300 Subject: [PATCH 113/276] Edge cache --- .../dao/asset/AssetCacheEvictEvent.java | 3 -- .../server/dao/asset/AssetRedisCache.java | 16 +------ .../server/dao/asset/BaseAssetService.java | 1 - .../dao/attributes/AttributeRedisCache.java | 16 +------ .../server/dao/cache/TbRedisSerializer.java | 35 ++++++++++++++ .../server/dao/device/DeviceRedisCache.java | 16 +------ .../server/dao/edge/EdgeCacheEvictEvent.java | 30 ++++++++++++ .../server/dao/edge/EdgeCacheKey.java | 41 +++++++++++++++++ .../server/dao/edge/EdgeCaffeineCache.java | 35 ++++++++++++++ .../server/dao/edge/EdgeRedisCache.java | 39 ++++++++++++++++ .../server/dao/edge/EdgeServiceImpl.java | 46 +++++++++++++------ .../dao/entityview/EntityViewRedisCache.java | 16 +------ .../dao/relation/RelationRedisCache.java | 16 +------ .../dao/tenant/TenantProfileRedisCache.java | 16 +------ 14 files changed, 224 insertions(+), 102 deletions(-) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/cache/TbRedisSerializer.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCacheEvictEvent.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCacheKey.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCaffeineCache.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/edge/EdgeRedisCache.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCacheEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCacheEvictEvent.java index faaf3945a0..ce81e047aa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCacheEvictEvent.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCacheEvictEvent.java @@ -27,7 +27,4 @@ class AssetCacheEvictEvent { private final String newName; private final String oldName; - public AssetCacheEvictEvent(TenantId tenantId, String newName) { - this(tenantId, newName, null); - } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetRedisCache.java index 864d5cf1e5..996756c7eb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetRedisCache.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; +import org.thingsboard.server.dao.cache.TbRedisSerializer; import org.thingsboard.server.dao.device.DeviceCacheKey; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @@ -33,19 +34,6 @@ import org.thingsboard.server.dao.device.DeviceCacheKey; public class AssetRedisCache extends RedisTbTransactionalCache { public AssetRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { - super(CacheConstants.ASSET_CACHE, cacheSpecsMap, connectionFactory, configuration, new RedisSerializer<>() { - - private final RedisSerializer java = RedisSerializer.java(); - - @Override - public byte[] serialize(Asset attributeKvEntry) throws SerializationException { - return java.serialize(attributeKvEntry); - } - - @Override - public Asset deserialize(byte[] bytes) throws SerializationException { - return (Asset) java.deserialize(bytes); - } - }); + super(CacheConstants.ASSET_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>()); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java index a7b408b47e..25d4029a0c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java @@ -116,7 +116,6 @@ public class BaseAssetService extends AbstractCachedEntityService assetDao.findAssetsByTenantIdAndName(tenantId.getId(), name) .orElse(null), true); - } @Transactional(propagation = Propagation.SUPPORTS) diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java index 8d2b8deb1f..3fc4a98075 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java @@ -27,25 +27,13 @@ import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; +import org.thingsboard.server.dao.cache.TbRedisSerializer; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("AttributeCache") public class AttributeRedisCache extends RedisTbTransactionalCache { public AttributeRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { - super(CacheConstants.ATTRIBUTES_CACHE, cacheSpecsMap, connectionFactory, configuration, new RedisSerializer<>() { - - private final RedisSerializer java = RedisSerializer.java(); - - @Override - public byte[] serialize(AttributeKvEntry attributeKvEntry) throws SerializationException { - return java.serialize(attributeKvEntry); - } - - @Override - public AttributeKvEntry deserialize(byte[] bytes) throws SerializationException { - return (AttributeKvEntry) java.deserialize(bytes); - } - }); + super(CacheConstants.ATTRIBUTES_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>()); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/TbRedisSerializer.java b/dao/src/main/java/org/thingsboard/server/dao/cache/TbRedisSerializer.java new file mode 100644 index 0000000000..cf0fac914a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/cache/TbRedisSerializer.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.cache; + +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; +import org.thingsboard.server.common.data.edge.Edge; + +public class TbRedisSerializer implements RedisSerializer { + + private final RedisSerializer java = RedisSerializer.java(); + + @Override + public byte[] serialize(T t) throws SerializationException { + return java.serialize(t); + } + + @Override + public T deserialize(byte[] bytes) throws SerializationException { + return (T) java.deserialize(bytes); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceRedisCache.java index a3c63b49f9..72a7273ed5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceRedisCache.java @@ -25,25 +25,13 @@ import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; +import org.thingsboard.server.dao.cache.TbRedisSerializer; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("DeviceCache") public class DeviceRedisCache extends RedisTbTransactionalCache { public DeviceRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { - super(CacheConstants.DEVICE_CACHE, cacheSpecsMap, connectionFactory, configuration, new RedisSerializer<>() { - - private final RedisSerializer java = RedisSerializer.java(); - - @Override - public byte[] serialize(Device attributeKvEntry) throws SerializationException { - return java.serialize(attributeKvEntry); - } - - @Override - public Device deserialize(byte[] bytes) throws SerializationException { - return (Device) java.deserialize(bytes); - } - }); + super(CacheConstants.DEVICE_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>()); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCacheEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCacheEvictEvent.java new file mode 100644 index 0000000000..44f4cdb25d --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCacheEvictEvent.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.edge; + +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +@RequiredArgsConstructor +class EdgeCacheEvictEvent { + + private final TenantId tenantId; + private final String newName; + private final String oldName; + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCacheKey.java new file mode 100644 index 0000000000..152528f936 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCacheKey.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.edge; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.thingsboard.server.cache.CacheKeyUtil; +import org.thingsboard.server.common.data.id.TenantId; + +import java.io.Serializable; + +@Getter +@EqualsAndHashCode +@RequiredArgsConstructor +@Builder +public class EdgeCacheKey implements Serializable { + + private final TenantId tenantId; + private final String name; + + @Override + public String toString() { + return CacheKeyUtil.toString(tenantId, name); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCaffeineCache.java new file mode 100644 index 0000000000..c4cff90b7f --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCaffeineCache.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.edge; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.dao.asset.AssetCacheKey; +import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("EdgeCache") +public class EdgeCaffeineCache extends CaffeineTbTransactionalCache { + + public EdgeCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.EDGE_CACHE); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeRedisCache.java new file mode 100644 index 0000000000..9a18f9d855 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeRedisCache.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.edge; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.dao.asset.AssetCacheKey; +import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; +import org.thingsboard.server.dao.cache.TbRedisSerializer; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("EdgeCache") +public class EdgeRedisCache extends RedisTbTransactionalCache { + + public EdgeRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.EDGE_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>()); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index 80d3a1bb03..2e386c4c31 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -25,11 +25,14 @@ import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeInfo; @@ -47,8 +50,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo; -import org.thingsboard.server.dao.cache.EntitiesCacheManager; -import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; @@ -74,7 +76,7 @@ import static org.thingsboard.server.dao.service.Validator.validateString; @Service @Slf4j -public class EdgeServiceImpl extends AbstractEntityService implements EdgeService { +public class EdgeServiceImpl extends AbstractCachedEntityService implements EdgeService { public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId "; @@ -90,9 +92,6 @@ public class EdgeServiceImpl extends AbstractEntityService implements EdgeServic @Autowired private UserService userService; - @Autowired - private EntitiesCacheManager cacheManager; - @Autowired private RuleChainService ruleChainService; @@ -102,6 +101,17 @@ public class EdgeServiceImpl extends AbstractEntityService implements EdgeServic @Autowired private DataValidator edgeValidator; + @TransactionalEventListener(classes = EdgeCacheEvictEvent.class) + @Override + public void handleEvictEvent(EdgeCacheEvictEvent event) { + List keys = new ArrayList<>(2); + keys.add(new EdgeCacheKey(event.getTenantId(), event.getNewName())); + if (StringUtils.isNotEmpty(event.getOldName()) && !event.getOldName().equals(event.getNewName())) { + keys.add(new EdgeCacheKey(event.getTenantId(), event.getOldName())); + } + cache.evict(keys); + } + @Override public Edge findEdgeById(TenantId tenantId, EdgeId edgeId) { log.trace("Executing findEdgeById [{}]", edgeId); @@ -128,8 +138,9 @@ public class EdgeServiceImpl extends AbstractEntityService implements EdgeServic public Edge findEdgeByTenantIdAndName(TenantId tenantId, String name) { log.trace("Executing findEdgeByTenantIdAndName [{}][{}]", tenantId, name); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - Optional edgeOpt = edgeDao.findEdgeByTenantIdAndName(tenantId.getId(), name); - return edgeOpt.orElse(null); + return cache.getAndPutInTransaction(new EdgeCacheKey(tenantId, name), + () -> edgeDao.findEdgeByTenantIdAndName(tenantId.getId(), name) + .orElse(null), true); } @Override @@ -139,13 +150,16 @@ public class EdgeServiceImpl extends AbstractEntityService implements EdgeServic return edgeDao.findByRoutingKey(tenantId.getId(), routingKey); } - @CacheEvict(cacheNames = EDGE_CACHE, key = "{#edge.tenantId, #edge.name}") + @Transactional(propagation = Propagation.SUPPORTS) @Override public Edge saveEdge(Edge edge) { log.trace("Executing saveEdge [{}]", edge); - edgeValidator.validate(edge, Edge::getTenantId); + Edge oldEdge = edgeValidator.validate(edge, Edge::getTenantId); + EdgeCacheEvictEvent evictEvent = new EdgeCacheEvictEvent(edge.getTenantId(), edge.getName(), oldEdge != null ? oldEdge.getName() : null); try { - return edgeDao.save(edge.getTenantId(), edge); + var savedEdge = edgeDao.save(edge.getTenantId(), edge); + publishEvictEvent(evictEvent); + return savedEdge; } catch (Exception t) { ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); if (e != null && e.getConstraintName() != null @@ -157,6 +171,7 @@ public class EdgeServiceImpl extends AbstractEntityService implements EdgeServic } } + @Transactional(propagation = Propagation.SUPPORTS) @Override public Edge assignEdgeToCustomer(TenantId tenantId, EdgeId edgeId, CustomerId customerId) { log.trace("[{}] Executing assignEdgeToCustomer [{}][{}]", tenantId, edgeId, customerId); @@ -165,6 +180,7 @@ public class EdgeServiceImpl extends AbstractEntityService implements EdgeServic return saveEdge(edge); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public Edge unassignEdgeFromCustomer(TenantId tenantId, EdgeId edgeId) { log.trace("[{}] Executing unassignEdgeFromCustomer [{}]", tenantId, edgeId); @@ -173,6 +189,7 @@ public class EdgeServiceImpl extends AbstractEntityService implements EdgeServic return saveEdge(edge); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public void deleteEdge(TenantId tenantId, EdgeId edgeId) { log.trace("Executing deleteEdge [{}]", edgeId); @@ -182,9 +199,9 @@ public class EdgeServiceImpl extends AbstractEntityService implements EdgeServic deleteEntityRelations(tenantId, edgeId); - cacheManager.removeEdgeFromCacheByName(edge.getTenantId(), edge.getName()); - edgeDao.removeById(tenantId, edgeId.getId()); + + publishEvictEvent(new EdgeCacheEvictEvent(edge.getTenantId(), edge.getName(), null)); } @Override @@ -284,6 +301,7 @@ public class EdgeServiceImpl extends AbstractEntityService implements EdgeServic customerId.getId(), toUUIDs(edgeIds)); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public void unassignCustomerEdges(TenantId tenantId, CustomerId customerId) { log.trace("Executing unassignCustomerEdges, tenantId [{}], customerId [{}]", tenantId, customerId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewRedisCache.java index 21da2337f9..9fcc8383f9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewRedisCache.java @@ -26,25 +26,13 @@ import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.dao.asset.AssetCacheKey; import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; +import org.thingsboard.server.dao.cache.TbRedisSerializer; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("EntityViewCache") public class EntityViewRedisCache extends RedisTbTransactionalCache { public EntityViewRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { - super(CacheConstants.ENTITY_VIEW_CACHE, cacheSpecsMap, connectionFactory, configuration, new RedisSerializer<>() { - - private final RedisSerializer java = RedisSerializer.java(); - - @Override - public byte[] serialize(EntityViewCacheValue attributeKvEntry) throws SerializationException { - return java.serialize(attributeKvEntry); - } - - @Override - public EntityViewCacheValue deserialize(byte[] bytes) throws SerializationException { - return (EntityViewCacheValue) java.deserialize(bytes); - } - }); + super(CacheConstants.ENTITY_VIEW_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>()); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationRedisCache.java index 0e0dc261fb..e433379474 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationRedisCache.java @@ -26,25 +26,13 @@ import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.dao.attributes.AttributeCacheKey; import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; +import org.thingsboard.server.dao.cache.TbRedisSerializer; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("RelationCache") public class RelationRedisCache extends RedisTbTransactionalCache { public RelationRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { - super(CacheConstants.RELATIONS_CACHE, cacheSpecsMap, connectionFactory, configuration, new RedisSerializer<>() { - - private final RedisSerializer java = RedisSerializer.java(); - - @Override - public byte[] serialize(RelationCacheValue attributeKvEntry) throws SerializationException { - return java.serialize(attributeKvEntry); - } - - @Override - public RelationCacheValue deserialize(byte[] bytes) throws SerializationException { - return (RelationCacheValue) java.deserialize(bytes); - } - }); + super(CacheConstants.RELATIONS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>()); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileRedisCache.java index 6ed750b480..e6cc0a254c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileRedisCache.java @@ -27,25 +27,13 @@ import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.dao.asset.AssetCacheKey; import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; +import org.thingsboard.server.dao.cache.TbRedisSerializer; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("TenantProfileCache") public class TenantProfileRedisCache extends RedisTbTransactionalCache { public TenantProfileRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { - super(CacheConstants.TENANT_PROFILE_CACHE, cacheSpecsMap, connectionFactory, configuration, new RedisSerializer<>() { - - private final RedisSerializer java = RedisSerializer.java(); - - @Override - public byte[] serialize(TenantProfile attributeKvEntry) throws SerializationException { - return java.serialize(attributeKvEntry); - } - - @Override - public TenantProfile deserialize(byte[] bytes) throws SerializationException { - return (TenantProfile) java.deserialize(bytes); - } - }); + super(CacheConstants.TENANT_PROFILE_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>()); } } From ffe5b5ee91a470be38afee5d8702a59e4dcfeeeb Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Tue, 10 May 2022 12:07:19 +0300 Subject: [PATCH 114/276] OTA Package cache --- .../dao/device/DeviceProfileServiceImpl.java | 2 +- .../server/dao/edge/EdgeServiceImpl.java | 1 - .../server/dao/ota/BaseOtaPackageService.java | 45 ++++++++++--------- .../dao/ota/OtaPackageCacheEvictEvent.java | 29 ++++++++++++ .../server/dao/ota/OtaPackageCacheKey.java | 41 +++++++++++++++++ .../dao/ota/OtaPackageCaffeineCache.java | 35 +++++++++++++++ .../server/dao/ota/OtaPackageRedisCache.java | 37 +++++++++++++++ 7 files changed, 168 insertions(+), 22 deletions(-) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCacheEvictEvent.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCacheKey.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCaffeineCache.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageRedisCache.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index 602128818b..97061d9bc7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -292,7 +292,7 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D } private PaginatedRemover tenantDeviceProfilesRemover = - new PaginatedRemover() { + new PaginatedRemover<>() { @Override protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index 2e386c4c31..414b33d3c2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -133,7 +133,6 @@ public class EdgeServiceImpl extends AbstractCachedEntityService implements OtaPackageService { public static final String INCORRECT_OTA_PACKAGE_ID = "Incorrect otaPackageId "; public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; private final OtaPackageDao otaPackageDao; private final OtaPackageInfoDao otaPackageInfoDao; - private final CacheManager cacheManager; private final OtaPackageDataCache otaPackageDataCache; private final DataValidator otaPackageInfoValidator; private final DataValidator otaPackageValidator; + @TransactionalEventListener(classes = OtaPackageCacheEvictEvent.class) + @Override + public void handleEvictEvent(OtaPackageCacheEvictEvent event) { + cache.evict(new OtaPackageCacheKey(event.getId())); + otaPackageDataCache.evict(event.getId().toString()); + } + + @Transactional(propagation = Propagation.SUPPORTS) @Override public OtaPackageInfo saveOtaPackageInfo(OtaPackageInfo otaPackageInfo, boolean isUrl) { log.trace("Executing saveOtaPackageInfo [{}]", otaPackageInfo); - if(isUrl && (StringUtils.isEmpty(otaPackageInfo.getUrl()) || otaPackageInfo.getUrl().trim().length() == 0)) { + if (isUrl && (StringUtils.isEmpty(otaPackageInfo.getUrl()) || otaPackageInfo.getUrl().trim().length() == 0)) { throw new DataValidationException("Ota package URL should be specified!"); } otaPackageInfoValidator.validate(otaPackageInfo, OtaPackageInfo::getTenantId); try { OtaPackageId otaPackageId = otaPackageInfo.getId(); + var result = otaPackageInfoDao.save(otaPackageInfo.getTenantId(), otaPackageInfo); if (otaPackageId != null) { - Cache cache = cacheManager.getCache(OTA_PACKAGE_CACHE); - cache.evict(toOtaPackageInfoKey(otaPackageId)); - otaPackageDataCache.evict(otaPackageId.toString()); + publishEvictEvent(new OtaPackageCacheEvictEvent(otaPackageId)); } - return otaPackageInfoDao.save(otaPackageInfo.getTenantId(), otaPackageInfo); + return result; } catch (Exception t) { ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("ota_package_tenant_title_version_unq_key")) { @@ -88,18 +94,18 @@ public class BaseOtaPackageService implements OtaPackageService { } } + @Transactional(propagation = Propagation.SUPPORTS) @Override public OtaPackage saveOtaPackage(OtaPackage otaPackage) { log.trace("Executing saveOtaPackage [{}]", otaPackage); otaPackageValidator.validate(otaPackage, OtaPackageInfo::getTenantId); try { OtaPackageId otaPackageId = otaPackage.getId(); + var result = otaPackageDao.save(otaPackage.getTenantId(), otaPackage); if (otaPackageId != null) { - Cache cache = cacheManager.getCache(OTA_PACKAGE_CACHE); - cache.evict(toOtaPackageInfoKey(otaPackageId)); - otaPackageDataCache.evict(otaPackageId.toString()); + publishEvictEvent(new OtaPackageCacheEvictEvent(otaPackageId)); } - return otaPackageDao.save(otaPackage.getTenantId(), otaPackage); + return result; } catch (Exception t) { ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("ota_package_tenant_title_version_unq_key")) { @@ -149,11 +155,11 @@ public class BaseOtaPackageService implements OtaPackageService { } @Override - @Cacheable(cacheNames = OTA_PACKAGE_CACHE, key = "{#otaPackageId}") public OtaPackageInfo findOtaPackageInfoById(TenantId tenantId, OtaPackageId otaPackageId) { log.trace("Executing findOtaPackageInfoById [{}]", otaPackageId); validateId(otaPackageId, INCORRECT_OTA_PACKAGE_ID + otaPackageId); - return otaPackageInfoDao.findById(tenantId, otaPackageId.getId()); + return cache.getAndPutInTransaction(new OtaPackageCacheKey(otaPackageId), + () -> otaPackageInfoDao.findById(tenantId, otaPackageId.getId()), true); } @Override @@ -179,15 +185,14 @@ public class BaseOtaPackageService implements OtaPackageService { return otaPackageInfoDao.findOtaPackageInfoByTenantIdAndDeviceProfileIdAndTypeAndHasData(tenantId, deviceProfileId, otaPackageType, pageLink); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public void deleteOtaPackage(TenantId tenantId, OtaPackageId otaPackageId) { log.trace("Executing deleteOtaPackage [{}]", otaPackageId); validateId(otaPackageId, INCORRECT_OTA_PACKAGE_ID + otaPackageId); try { - Cache cache = cacheManager.getCache(OTA_PACKAGE_CACHE); - cache.evict(toOtaPackageInfoKey(otaPackageId)); - otaPackageDataCache.evict(otaPackageId.toString()); otaPackageDao.removeById(tenantId, otaPackageId.getId()); + publishEvictEvent(new OtaPackageCacheEvictEvent(otaPackageId)); } catch (Exception t) { ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_firmware_device")) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCacheEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCacheEvictEvent.java new file mode 100644 index 0000000000..cea1c1afcb --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCacheEvictEvent.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.ota; + +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.thingsboard.server.common.data.id.OtaPackageId; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +@RequiredArgsConstructor +class OtaPackageCacheEvictEvent { + + private final OtaPackageId id; + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCacheKey.java new file mode 100644 index 0000000000..b3acbea6bd --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCacheKey.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.ota; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.thingsboard.server.cache.CacheKeyUtil; +import org.thingsboard.server.common.data.id.OtaPackageId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.io.Serializable; + +@Getter +@EqualsAndHashCode +@RequiredArgsConstructor +@Builder +public class OtaPackageCacheKey implements Serializable { + + private final OtaPackageId id; + + @Override + public String toString() { + return id.getId().toString(); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCaffeineCache.java new file mode 100644 index 0000000000..03f2401f42 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCaffeineCache.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.ota; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.OtaPackageInfo; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.dao.asset.AssetCacheKey; +import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("OtaPackageCache") +public class OtaPackageCaffeineCache extends CaffeineTbTransactionalCache { + + public OtaPackageCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.OTA_PACKAGE_CACHE); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageRedisCache.java new file mode 100644 index 0000000000..7605f24f3c --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageRedisCache.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.ota; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.OtaPackageInfo; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.dao.asset.AssetCacheKey; +import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; +import org.thingsboard.server.dao.cache.TbRedisSerializer; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("OtaPackageCache") +public class OtaPackageRedisCache extends RedisTbTransactionalCache { + + public OtaPackageRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.OTA_PACKAGE_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>()); + } +} From c69ba6d02d10ae1655239a8bbebd3fa15551157d Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Tue, 10 May 2022 13:27:02 +0300 Subject: [PATCH 115/276] Device Profile Cache --- .../server/cache/CacheKeyUtil.java | 41 -------- .../server/dao/asset/AssetCacheKey.java | 6 +- .../server/dao/device/DeviceCacheKey.java | 7 +- .../dao/device/DeviceProfileCacheKey.java | 63 ++++++++++++ .../device/DeviceProfileCaffeineCache.java | 34 +++++++ .../dao/device/DeviceProfileEvictEvent.java | 31 ++++++ .../dao/device/DeviceProfileRedisCache.java | 36 +++++++ .../dao/device/DeviceProfileServiceImpl.java | 96 +++++++++++-------- .../server/dao/edge/EdgeCacheKey.java | 3 +- .../dao/entityview/EntityViewCacheKey.java | 10 +- .../dao/entityview/EntityViewCacheValue.java | 4 - .../server/dao/ota/OtaPackageCacheKey.java | 4 +- .../server/dao/relation/RelationCacheKey.java | 20 +++- .../dao/tenant/TenantProfileCacheKey.java | 2 +- .../server/dao/sql/alarm/JpaAlarmDaoTest.java | 2 + 15 files changed, 256 insertions(+), 103 deletions(-) delete mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/CacheKeyUtil.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCacheKey.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCaffeineCache.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileEvictEvent.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileRedisCache.java diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/CacheKeyUtil.java b/common/cache/src/main/java/org/thingsboard/server/cache/CacheKeyUtil.java deleted file mode 100644 index 1c1a096f59..0000000000 --- a/common/cache/src/main/java/org/thingsboard/server/cache/CacheKeyUtil.java +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.cache; - -public class CacheKeyUtil { - - public static String toString(Object... keyElements) { - StringBuilder sb = new StringBuilder(); - boolean first = true; - for (Object keyElement : keyElements) { - first = addElement(sb, first, keyElement); - } - return sb.toString(); - } - - private static boolean addElement(StringBuilder sb, boolean first, Object element) { - if (element != null) { - if (!first) { - sb.append("_"); - } - sb.append(element); - return false; - } else { - return first; - } - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCacheKey.java index 70e17e28ff..c4a6d5b7fb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCacheKey.java @@ -19,8 +19,6 @@ import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; -import org.thingsboard.server.cache.CacheKeyUtil; -import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import java.io.Serializable; @@ -31,12 +29,14 @@ import java.io.Serializable; @Builder public class AssetCacheKey implements Serializable { + private static final long serialVersionUID = 4196610233744512673L; + private final TenantId tenantId; private final String name; @Override public String toString() { - return CacheKeyUtil.toString(tenantId, name); + return tenantId + "_" + name; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCacheKey.java index 574a5c2617..6e62a07e1b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCacheKey.java @@ -19,7 +19,6 @@ import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; -import org.thingsboard.server.cache.CacheKeyUtil; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; @@ -45,7 +44,11 @@ public class DeviceCacheKey implements Serializable { @Override public String toString() { - return CacheKeyUtil.toString(tenantId, deviceId, deviceName); + if (deviceId != null) { + return tenantId + "_" + deviceId; + } else { + return tenantId + "_n_" + deviceName; + } } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCacheKey.java new file mode 100644 index 0000000000..79f997e22c --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCacheKey.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.device; + +import lombok.Data; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.io.Serializable; + +@Data +public class DeviceProfileCacheKey implements Serializable { + + private static final long serialVersionUID = 8220455917177676472L; + + private final TenantId tenantId; + private final String name; + private final DeviceProfileId deviceProfileId; + private final boolean defaultProfile; + + private DeviceProfileCacheKey(TenantId tenantId, String name, DeviceProfileId deviceProfileId, boolean defaultProfile) { + this.tenantId = tenantId; + this.name = name; + this.deviceProfileId = deviceProfileId; + this.defaultProfile = defaultProfile; + } + + public static DeviceProfileCacheKey fromName(TenantId tenantId, String name) { + return new DeviceProfileCacheKey(tenantId, name, null, false); + } + + public static DeviceProfileCacheKey fromId(DeviceProfileId id) { + return new DeviceProfileCacheKey(null, null, id, false); + } + + public static DeviceProfileCacheKey defaultProfile(TenantId tenantId) { + return new DeviceProfileCacheKey(tenantId, null, null, true); + } + + @Override + public String toString() { + if (deviceProfileId != null) { + return deviceProfileId.toString(); + } else if (defaultProfile) { + return tenantId.toString(); + } else { + return tenantId + "_" + name; + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCaffeineCache.java new file mode 100644 index 0000000000..03c5e43f94 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCaffeineCache.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.device; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("DeviceProfileCache") +public class DeviceProfileCaffeineCache extends CaffeineTbTransactionalCache { + + public DeviceProfileCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.DEVICE_PROFILE_CACHE); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileEvictEvent.java new file mode 100644 index 0000000000..b5b4b27a6b --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileEvictEvent.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.device; + +import lombok.Data; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +public class DeviceProfileEvictEvent { + + private final TenantId tenantId; + private final String newName; + private final String oldName; + private final DeviceProfileId deviceProfileId; + private final boolean defaultProfile; + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileRedisCache.java new file mode 100644 index 0000000000..8d0c4eded0 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileRedisCache.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.device; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; +import org.thingsboard.server.dao.cache.TbRedisSerializer; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("DeviceProfileCache") +public class DeviceProfileRedisCache extends RedisTbTransactionalCache { + + public DeviceProfileRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.DEVICE_PROFILE_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>()); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index 97061d9bc7..229e9c316f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -17,17 +17,23 @@ package org.thingsboard.server.dao.device; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; +import org.jetbrains.annotations.Nullable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileInfo; import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; @@ -36,14 +42,20 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.asset.AssetCacheKey; +import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; +import org.thingsboard.server.dao.tenant.TenantProfileCacheKey; +import org.thingsboard.server.dao.tenant.TenantProfileEvictEvent; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -52,7 +64,7 @@ import static org.thingsboard.server.dao.service.Validator.validateId; @Service @Slf4j -public class DeviceProfileServiceImpl extends AbstractEntityService implements DeviceProfileService { +public class DeviceProfileServiceImpl extends AbstractCachedEntityService implements DeviceProfileService { private static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; private static final String INCORRECT_DEVICE_PROFILE_ID = "Incorrect deviceProfileId "; @@ -67,51 +79,60 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D @Autowired private DeviceService deviceService; - @Autowired - private CacheManager cacheManager; - @Autowired private DataValidator deviceProfileValidator; private final Lock findOrCreateLock = new ReentrantLock(); - @Cacheable(cacheNames = DEVICE_PROFILE_CACHE, key = "{#deviceProfileId.id}") + @TransactionalEventListener(classes = DeviceProfileEvictEvent.class) + @Override + public void handleEvictEvent(DeviceProfileEvictEvent event) { + List keys = new ArrayList<>(2); + keys.add(DeviceProfileCacheKey.fromId(event.getDeviceProfileId())); + keys.add(DeviceProfileCacheKey.fromName(event.getTenantId(), event.getNewName())); + if (event.isDefaultProfile()) { + keys.add(DeviceProfileCacheKey.defaultProfile(event.getTenantId())); + } + if (StringUtils.isNotEmpty(event.getOldName()) && !event.getOldName().equals(event.getNewName())) { + keys.add(DeviceProfileCacheKey.fromName(event.getTenantId(), event.getOldName())); + } + cache.evict(keys); + } + @Override public DeviceProfile findDeviceProfileById(TenantId tenantId, DeviceProfileId deviceProfileId) { log.trace("Executing findDeviceProfileById [{}]", deviceProfileId); Validator.validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId); - return deviceProfileDao.findById(tenantId, deviceProfileId.getId()); + return cache.getAndPutInTransaction(DeviceProfileCacheKey.fromId(deviceProfileId), + () -> deviceProfileDao.findById(tenantId, deviceProfileId.getId()), true); } @Override public DeviceProfile findDeviceProfileByName(TenantId tenantId, String profileName) { log.trace("Executing findDeviceProfileByName [{}][{}]", tenantId, profileName); Validator.validateString(profileName, INCORRECT_DEVICE_PROFILE_NAME + profileName); - return deviceProfileDao.findByName(tenantId, profileName); + return cache.getAndPutInTransaction(DeviceProfileCacheKey.fromName(tenantId, profileName), + () -> deviceProfileDao.findByName(tenantId, profileName), true); } - @Cacheable(cacheNames = DEVICE_PROFILE_CACHE, key = "{'info', #deviceProfileId.id}") @Override public DeviceProfileInfo findDeviceProfileInfoById(TenantId tenantId, DeviceProfileId deviceProfileId) { log.trace("Executing findDeviceProfileById [{}]", deviceProfileId); Validator.validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId); - return deviceProfileDao.findDeviceProfileInfoById(tenantId, deviceProfileId.getId()); + return toDeviceProfileInfo(findDeviceProfileById(tenantId, deviceProfileId)); } @Override public DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile) { log.trace("Executing saveDeviceProfile [{}]", deviceProfile); - deviceProfileValidator.validate(deviceProfile, DeviceProfile::getTenantId); - DeviceProfile oldDeviceProfile = null; - if (deviceProfile.getId() != null) { - oldDeviceProfile = deviceProfileDao.findById(deviceProfile.getTenantId(), deviceProfile.getId().getId()); - } + DeviceProfile oldDeviceProfile = deviceProfileValidator.validate(deviceProfile, DeviceProfile::getTenantId); DeviceProfile savedDeviceProfile; try { savedDeviceProfile = deviceProfileDao.saveAndFlush(deviceProfile.getTenantId(), deviceProfile); } catch (Exception t) { ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_profile_name_unq_key")) { + //TODO: refactor this to return existing device profile. If they are equal - no need to throw exception. Then we can make this call @Transactional and tests will not fail. throw new DataValidationException("Device profile with such name already exists!"); } else if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_provision_key_unq_key")) { throw new DataValidationException("Device profile with such provision device key already exists!"); @@ -119,14 +140,8 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D throw t; } } - Cache cache = cacheManager.getCache(DEVICE_PROFILE_CACHE); - cache.evict(Collections.singletonList(savedDeviceProfile.getId().getId())); - cache.evict(Arrays.asList("info", savedDeviceProfile.getId().getId())); - cache.evict(Arrays.asList(deviceProfile.getTenantId().getId(), deviceProfile.getName())); - if (savedDeviceProfile.isDefault()) { - cache.evict(Arrays.asList("default", savedDeviceProfile.getTenantId().getId())); - cache.evict(Arrays.asList("default", "info", savedDeviceProfile.getTenantId().getId())); - } + publishEvictEvent(new DeviceProfileEvictEvent(savedDeviceProfile.getTenantId(), savedDeviceProfile.getName(), + oldDeviceProfile != null ? oldDeviceProfile.getName() : null, savedDeviceProfile.getId(), savedDeviceProfile.isDefault())); if (oldDeviceProfile != null && !oldDeviceProfile.getName().equals(deviceProfile.getName())) { PageLink pageLink = new PageLink(100); PageData pageData; @@ -142,6 +157,7 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D return savedDeviceProfile; } + @Transactional(propagation = Propagation.SUPPORTS) @Override public void deleteDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId) { log.trace("Executing deleteDeviceProfile [{}]", deviceProfileId); @@ -157,6 +173,8 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D DeviceProfileId deviceProfileId = deviceProfile.getId(); try { deviceProfileDao.removeById(tenantId, deviceProfileId.getId()); + publishEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(), + null, deviceProfile.getId(), deviceProfile.isDefault())); } catch (Exception t) { ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_device_profile")) { @@ -166,10 +184,6 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D } } deleteEntityRelations(tenantId, deviceProfileId); - Cache cache = cacheManager.getCache(DEVICE_PROFILE_CACHE); - cache.evict(Collections.singletonList(deviceProfileId.getId())); - cache.evict(Arrays.asList("info", deviceProfileId.getId())); - cache.evict(Arrays.asList(tenantId.getId(), deviceProfile.getName())); } @Override @@ -188,7 +202,6 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D return deviceProfileDao.findDeviceProfileInfos(tenantId, pageLink, transportType); } - @Cacheable(cacheNames = DEVICE_PROFILE_CACHE, key = "{#tenantId.id, #name}") @Override public DeviceProfile findOrCreateDeviceProfile(TenantId tenantId, String name) { log.trace("Executing findOrCreateDefaultDeviceProfile"); @@ -207,6 +220,7 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D return deviceProfile; } + @Transactional(propagation = Propagation.SUPPORTS) @Override public DeviceProfile createDefaultDeviceProfile(TenantId tenantId) { log.trace("Executing createDefaultDeviceProfile tenantId [{}]", tenantId); @@ -234,56 +248,49 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D return saveDeviceProfile(deviceProfile); } - @Cacheable(cacheNames = DEVICE_PROFILE_CACHE, key = "{'default', #tenantId.id}") @Override public DeviceProfile findDefaultDeviceProfile(TenantId tenantId) { log.trace("Executing findDefaultDeviceProfile tenantId [{}]", tenantId); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - return deviceProfileDao.findDefaultDeviceProfile(tenantId); + return cache.getAndPutInTransaction(DeviceProfileCacheKey.defaultProfile(tenantId), + () -> deviceProfileDao.findDefaultDeviceProfile(tenantId), true); } - @Cacheable(cacheNames = DEVICE_PROFILE_CACHE, key = "{'default', 'info', #tenantId.id}") @Override public DeviceProfileInfo findDefaultDeviceProfileInfo(TenantId tenantId) { log.trace("Executing findDefaultDeviceProfileInfo tenantId [{}]", tenantId); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - return deviceProfileDao.findDefaultDeviceProfileInfo(tenantId); + return toDeviceProfileInfo(findDefaultDeviceProfile(tenantId)); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public boolean setDefaultDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId) { log.trace("Executing setDefaultDeviceProfile [{}]", deviceProfileId); Validator.validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId); DeviceProfile deviceProfile = deviceProfileDao.findById(tenantId, deviceProfileId.getId()); if (!deviceProfile.isDefault()) { - Cache cache = cacheManager.getCache(DEVICE_PROFILE_CACHE); deviceProfile.setDefault(true); DeviceProfile previousDefaultDeviceProfile = findDefaultDeviceProfile(tenantId); boolean changed = false; if (previousDefaultDeviceProfile == null) { deviceProfileDao.save(tenantId, deviceProfile); + publishEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(), null, deviceProfile.getId(), true)); changed = true; } else if (!previousDefaultDeviceProfile.getId().equals(deviceProfile.getId())) { previousDefaultDeviceProfile.setDefault(false); deviceProfileDao.save(tenantId, previousDefaultDeviceProfile); deviceProfileDao.save(tenantId, deviceProfile); - cache.evict(Collections.singletonList(previousDefaultDeviceProfile.getId().getId())); - cache.evict(Arrays.asList("info", previousDefaultDeviceProfile.getId().getId())); - cache.evict(Arrays.asList(tenantId.getId(), previousDefaultDeviceProfile.getName())); + publishEvictEvent(new DeviceProfileEvictEvent(previousDefaultDeviceProfile.getTenantId(), previousDefaultDeviceProfile.getName(), null, previousDefaultDeviceProfile.getId(), false)); + publishEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(), null, deviceProfile.getId(), true)); changed = true; } - if (changed) { - cache.evict(Collections.singletonList(deviceProfile.getId().getId())); - cache.evict(Arrays.asList("info", deviceProfile.getId().getId())); - cache.evict(Arrays.asList("default", tenantId.getId())); - cache.evict(Arrays.asList("default", "info", tenantId.getId())); - cache.evict(Arrays.asList(tenantId.getId(), deviceProfile.getName())); - } return changed; } return false; } + @Transactional(propagation = Propagation.SUPPORTS) @Override public void deleteDeviceProfilesByTenantId(TenantId tenantId) { log.trace("Executing deleteDeviceProfilesByTenantId, tenantId [{}]", tenantId); @@ -305,4 +312,9 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D } }; + private DeviceProfileInfo toDeviceProfileInfo(DeviceProfile profile) { + return profile == null ? null : new DeviceProfileInfo(profile.getId(), profile.getName(), profile.getImage(), + profile.getDefaultDashboardId(), profile.getType(), profile.getTransportType()); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCacheKey.java index 152528f936..e348e4a854 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCacheKey.java @@ -19,7 +19,6 @@ import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; -import org.thingsboard.server.cache.CacheKeyUtil; import org.thingsboard.server.common.data.id.TenantId; import java.io.Serializable; @@ -35,7 +34,7 @@ public class EdgeCacheKey implements Serializable { @Override public String toString() { - return CacheKeyUtil.toString(tenantId, name); + return tenantId + "_" + name; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheKey.java index 6365fb6b45..20e9de06f6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheKey.java @@ -18,8 +18,6 @@ package org.thingsboard.server.dao.entityview; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.thingsboard.server.cache.CacheKeyUtil; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; @@ -57,7 +55,13 @@ public class EntityViewCacheKey implements Serializable { @Override public String toString() { - return CacheKeyUtil.toString(tenantId, name, entityId, entityViewId); + if (entityViewId != null) { + return entityViewId.toString(); + } else if (entityId != null) { + return tenantId + "_" + entityId; + } else { + return tenantId + "_n_" + name; + } } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheValue.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheValue.java index ea0f85040d..2d086cf1d6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheValue.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheValue.java @@ -18,11 +18,7 @@ package org.thingsboard.server.dao.entityview; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; -import org.thingsboard.server.cache.CacheKeyUtil; import org.thingsboard.server.common.data.EntityView; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.EntityViewId; -import org.thingsboard.server.common.data.id.TenantId; import java.io.Serializable; import java.util.List; diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCacheKey.java index b3acbea6bd..e238c70f2c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCacheKey.java @@ -19,9 +19,7 @@ import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; -import org.thingsboard.server.cache.CacheKeyUtil; import org.thingsboard.server.common.data.id.OtaPackageId; -import org.thingsboard.server.common.data.id.TenantId; import java.io.Serializable; @@ -35,7 +33,7 @@ public class OtaPackageCacheKey implements Serializable { @Override public String toString() { - return id.getId().toString(); + return id.toString(); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheKey.java index b60aa84b9b..f45c8a1344 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheKey.java @@ -19,7 +19,6 @@ import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; -import org.thingsboard.server.cache.CacheKeyUtil; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationTypeGroup; @@ -46,7 +45,24 @@ public class RelationCacheKey implements Serializable { @Override public String toString() { - return CacheKeyUtil.toString(from, to, typeGroup, type, direction); + StringBuilder sb = new StringBuilder(); + boolean first = add(sb, true, from); + first = add(sb, first, to); + first = add(sb, first, type); + first = add(sb, first, typeGroup); + add(sb, first, direction); + return sb.toString(); + } + + private boolean add(StringBuilder sb, boolean first, Object param) { + if (param != null) { + if (!first) { + sb.append("_"); + } + first = false; + sb.append(param); + } + return first; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCacheKey.java index 2c921d2f2c..a9bfcbe2ca 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCacheKey.java @@ -47,7 +47,7 @@ public class TenantProfileCacheKey implements Serializable { if (defaultProfile) { return "default"; } else { - return tenantProfileId.getId().toString(); + return tenantProfileId.toString(); } } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java index aa99a88ac7..941c2d01ac 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java @@ -56,6 +56,8 @@ public class JpaAlarmDaoTest extends AbstractJpaDaoTest { UUID alarm3Id = UUID.fromString("d4b68f45-3e96-11e7-a884-898080180d6b"); int alarmCountBeforeSave = alarmDao.find(TenantId.fromUUID(tenantId)).size(); saveAlarm(alarm1Id, tenantId, originator1Id, "TEST_ALARM"); + //The timestamp of the startTime should be different in order for test to always work + Thread.sleep(1); saveAlarm(alarm2Id, tenantId, originator1Id, "TEST_ALARM"); saveAlarm(alarm3Id, tenantId, originator2Id, "TEST_ALARM"); int alarmCountAfterSave = alarmDao.find(TenantId.fromUUID(tenantId)).size(); From 5243b873e09cbe159bd7ef959e696d4f9cd06955 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Tue, 10 May 2022 13:35:19 +0300 Subject: [PATCH 116/276] No more redundant EntityCacheManager --- .../cache/ota/CaffeineOtaPackageCache.java | 1 - .../server/dao/asset/AssetCaffeineCache.java | 2 - .../dao/attributes/AttributeRedisCache.java | 4 -- .../dao/cache/EntitiesCacheManager.java | 25 ---------- .../dao/cache/EntitiesCacheManagerImpl.java | 47 ------------------- .../device/DeviceCredentialsServiceImpl.java | 1 - .../dao/device/DeviceProfileServiceImpl.java | 12 ----- .../service/validator/AssetDataValidator.java | 7 --- .../validator/DeviceDataValidator.java | 1 - .../service/validator/EdgeDataValidator.java | 8 +--- .../dao/tenant/TenantProfileServiceImpl.java | 2 - 11 files changed, 1 insertion(+), 109 deletions(-) delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/cache/EntitiesCacheManager.java delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/cache/EntitiesCacheManagerImpl.java diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/ota/CaffeineOtaPackageCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/ota/CaffeineOtaPackageCache.java index 7ea7b18f43..aaea36abae 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/ota/CaffeineOtaPackageCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/ota/CaffeineOtaPackageCache.java @@ -20,7 +20,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; -import static org.thingsboard.server.common.data.CacheConstants.OTA_PACKAGE_CACHE; import static org.thingsboard.server.common.data.CacheConstants.OTA_PACKAGE_DATA_CACHE; @Service diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCaffeineCache.java index 3d64db46b0..daced88f34 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCaffeineCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCaffeineCache.java @@ -19,10 +19,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; -import org.thingsboard.server.dao.device.DeviceCacheKey; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) @Service("AssetCache") diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java index 3fc4a98075..1f10027fda 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java @@ -15,12 +15,8 @@ */ package org.thingsboard.server.dao.attributes; -import lombok.Getter; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.cache.CacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.serializer.RedisSerializer; -import org.springframework.data.redis.serializer.SerializationException; import org.springframework.stereotype.Service; import org.thingsboard.server.cache.CacheSpecsMap; import org.thingsboard.server.cache.TBRedisCacheConfiguration; diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/EntitiesCacheManager.java b/dao/src/main/java/org/thingsboard/server/dao/cache/EntitiesCacheManager.java deleted file mode 100644 index 06dcd51e61..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/EntitiesCacheManager.java +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.dao.cache; - -import org.thingsboard.server.common.data.id.TenantId; - -public interface EntitiesCacheManager { - - void removeAssetFromCacheByName(TenantId tenantId, String name); - - void removeEdgeFromCacheByName(TenantId tenantId, String name); -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/EntitiesCacheManagerImpl.java b/dao/src/main/java/org/thingsboard/server/dao/cache/EntitiesCacheManagerImpl.java deleted file mode 100644 index d24bd98822..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/EntitiesCacheManagerImpl.java +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.dao.cache; - -import lombok.AllArgsConstructor; -import org.springframework.cache.Cache; -import org.springframework.cache.CacheManager; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.id.TenantId; - -import java.util.Arrays; - -import static org.thingsboard.server.common.data.CacheConstants.ASSET_CACHE; -import static org.thingsboard.server.common.data.CacheConstants.EDGE_CACHE; - -@Component -@AllArgsConstructor -public class EntitiesCacheManagerImpl implements EntitiesCacheManager { - - private final CacheManager cacheManager; - - @Override - public void removeAssetFromCacheByName(TenantId tenantId, String name) { - Cache cache = cacheManager.getCache(ASSET_CACHE); - cache.evict(Arrays.asList(tenantId, name)); - } - - @Override - public void removeEdgeFromCacheByName(TenantId tenantId, String name) { - Cache cache = cacheManager.getCache(EDGE_CACHE); - cache.evict(Arrays.asList(tenantId, name)); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java index ff92c330e0..72ec300ab0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.device; - import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.SecurityMode; import org.eclipse.leshan.core.util.SecurityUtil; diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index 229e9c316f..be42372a78 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -17,11 +17,7 @@ package org.thingsboard.server.dao.device; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; -import org.jetbrains.annotations.Nullable; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cache.Cache; -import org.springframework.cache.CacheManager; -import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -33,7 +29,6 @@ import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; @@ -42,24 +37,17 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.dao.asset.AssetCacheKey; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; -import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; -import org.thingsboard.server.dao.tenant.TenantProfileCacheKey; -import org.thingsboard.server.dao.tenant.TenantProfileEvictEvent; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -import static org.thingsboard.server.common.data.CacheConstants.DEVICE_PROFILE_CACHE; import static org.thingsboard.server.dao.service.Validator.validateId; @Service diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java index bc03a2131b..a2204491f9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java @@ -28,7 +28,6 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.asset.AssetDao; import org.thingsboard.server.dao.asset.BaseAssetService; -import org.thingsboard.server.dao.cache.EntitiesCacheManager; import org.thingsboard.server.dao.customer.CustomerDao; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; @@ -53,9 +52,6 @@ public class AssetDataValidator extends DataValidator { @Lazy private TbTenantProfileCache tenantProfileCache; - @Autowired - private EntitiesCacheManager cacheManager; - @Override protected void validateCreate(TenantId tenantId, Asset asset) { DefaultTenantProfileConfiguration profileConfiguration = @@ -72,9 +68,6 @@ public class AssetDataValidator extends DataValidator { if (old == null) { throw new DataValidationException("Can't update non existing asset!"); } - if (!old.getName().equals(asset.getName())) { - cacheManager.removeAssetFromCacheByName(tenantId, old.getName()); - } return old; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceDataValidator.java index 30f6e84ee6..e9d8b82e1c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceDataValidator.java @@ -29,7 +29,6 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; -import org.thingsboard.server.dao.cache.EntitiesCacheManager; import org.thingsboard.server.dao.customer.CustomerDao; import org.thingsboard.server.dao.device.DeviceDao; import org.thingsboard.server.dao.exception.DataValidationException; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/EdgeDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/EdgeDataValidator.java index 490da123e6..8289d21b23 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/EdgeDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/EdgeDataValidator.java @@ -23,7 +23,6 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.cache.EntitiesCacheManager; import org.thingsboard.server.dao.customer.CustomerDao; import org.thingsboard.server.dao.edge.EdgeDao; import org.thingsboard.server.dao.exception.DataValidationException; @@ -39,7 +38,6 @@ public class EdgeDataValidator extends DataValidator { private final EdgeDao edgeDao; private final TenantDao tenantDao; private final CustomerDao customerDao; - private final EntitiesCacheManager cacheManager; @Override protected void validateCreate(TenantId tenantId, Edge edge) { @@ -47,11 +45,7 @@ public class EdgeDataValidator extends DataValidator { @Override protected Edge validateUpdate(TenantId tenantId, Edge edge) { - Edge old = edgeDao.findById(edge.getTenantId(), edge.getId().getId()); - if (!old.getName().equals(edge.getName())) { - cacheManager.removeEdgeFromCacheByName(tenantId, old.getName()); - } - return old; + return edgeDao.findById(edge.getTenantId(), edge.getId().getId()); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java index 97226df0fc..4cdfed4944 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java @@ -18,7 +18,6 @@ package org.thingsboard.server.dao.tenant; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -40,7 +39,6 @@ import org.thingsboard.server.dao.service.Validator; import java.util.ArrayList; import java.util.List; -import static org.thingsboard.server.common.data.CacheConstants.TENANT_PROFILE_CACHE; import static org.thingsboard.server.dao.service.Validator.validateId; @Service From ee05935a498c779643ff46e1aeb65294016933c8 Mon Sep 17 00:00:00 2001 From: ShvaykaD Date: Tue, 10 May 2022 13:42:48 +0300 Subject: [PATCH 117/276] added new MqttTestClient & MqttTestCallback & refactored credentials and attribute updates/request tests --- .../controller/TbTestWebSocketClient.java | 6 +- .../transport/mqtt/MqttTestCallback.java | 60 ++ .../server/transport/mqtt/MqttTestClient.java | 118 ++++ ...AbstractMqttAttributesIntegrationTest.java | 654 ++++++++---------- ...tBackwardCompatibilityIntegrationTest.java | 2 - ...AttributesRequestProtoIntegrationTest.java | 1 - ...sBackwardCompatibilityIntegrationTest.java | 1 - .../credentials/BasicMqttCredentialsTest.java | 41 +- 8 files changed, 510 insertions(+), 373 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestCallback.java create mode 100644 application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestClient.java diff --git a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java index 6af6176b06..f7f8c73d34 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java @@ -79,8 +79,12 @@ public class TbTestWebSocketClient extends WebSocketClient { } public void registerWaitForUpdate() { + registerWaitForUpdate(1); + } + + public void registerWaitForUpdate(int count) { lastMsg = null; - update = new CountDownLatch(1); + update = new CountDownLatch(count); } @Override diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestCallback.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestCallback.java new file mode 100644 index 0000000000..a17d3bd2a4 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestCallback.java @@ -0,0 +1,60 @@ +package org.thingsboard.server.transport.mqtt; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttMessage; + +import java.util.concurrent.CountDownLatch; + +@Slf4j +@Data +public class MqttTestCallback implements MqttCallback { + + private final CountDownLatch subscribeLatch; + private final CountDownLatch deliveryLatch; + private int qoS; + private byte[] payloadBytes; + private String awaitSubTopic; + + public MqttTestCallback(String awaitSubTopic) { + this.subscribeLatch = new CountDownLatch(1); + this.deliveryLatch = new CountDownLatch(1); + this.awaitSubTopic = awaitSubTopic; + } + + public MqttTestCallback() { + this.subscribeLatch = new CountDownLatch(1); + this.deliveryLatch = new CountDownLatch(1); + } + + @Override + public void connectionLost(Throwable throwable) { + log.warn("connectionLost: ", throwable); + } + + @Override + public void messageArrived(String requestTopic, MqttMessage mqttMessage) { + if (awaitSubTopic == null) { + log.warn("messageArrived on topic: {}", requestTopic); + qoS = mqttMessage.getQos(); + payloadBytes = mqttMessage.getPayload(); + subscribeLatch.countDown(); + } else { + log.warn("messageArrived on topic: {}, awaitSubTopic: {}", requestTopic, awaitSubTopic); + if (awaitSubTopic.equals(requestTopic)) { + qoS = mqttMessage.getQos(); + payloadBytes = mqttMessage.getPayload(); + subscribeLatch.countDown(); + } + } + } + + @Override + public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { + log.warn("delivery complete: {}", iMqttDeliveryToken.getResponse()); + deliveryLatch.countDown(); + + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestClient.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestClient.java new file mode 100644 index 0000000000..8afa41b160 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestClient.java @@ -0,0 +1,118 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.transport.mqtt; + +import io.netty.handler.codec.mqtt.MqttQoS; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.IMqttToken; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; +import org.thingsboard.server.common.data.StringUtils; + +import java.util.concurrent.TimeUnit; + +public class MqttTestClient { + + private static final String MQTT_URL = "tcp://localhost:1883"; + private static final int TIMEOUT = 30; // seconds + private static final long TIMEOUT_MS = TimeUnit.SECONDS.toMillis(TIMEOUT); + + private final MqttAsyncClient client; + + public void setCallback(MqttTestCallback callback) { + client.setCallback(callback); + } + + public MqttTestClient() throws MqttException { + this.client = createClient(); + } + + public MqttTestClient(String clientId) throws MqttException { + this.client = createClient(clientId); + } + + public void connectAndWait(String userName, String password) throws MqttException { + IMqttToken connect = connect(userName, password); + connect.waitForCompletion(TIMEOUT_MS); + } + + public void connectAndWait(String userName) throws MqttException { + connectAndWait(userName, null); + } + + public void connectAndWait() throws MqttException { + connectAndWait(null, null); + } + + private IMqttToken connect(String userName, String password) throws MqttException { + if (client == null) { + throw new RuntimeException("Failed to connect! MqttAsyncClient is not initialized!"); + } + MqttConnectOptions options = new MqttConnectOptions(); + if (StringUtils.isNotEmpty(userName)) { + options.setUserName(userName); + } + if (StringUtils.isNotEmpty(password)) { + options.setPassword(password.toCharArray()); + } + return client.connect(options); + } + + public void disconnectAndWait() throws MqttException { + disconnect().waitForCompletion(TIMEOUT_MS); + } + + public IMqttToken disconnect() throws MqttException { + return client.disconnect(); + } + + public void disconnectForcibly() throws MqttException { + client.disconnectForcibly(TIMEOUT_MS); + } + + public void publishAndWait(String topic, byte[] payload) throws MqttException { + publish(topic, payload).waitForCompletion(TIMEOUT_MS); + } + + public IMqttDeliveryToken publish(String topic, byte[] payload) throws MqttException { + MqttMessage message = new MqttMessage(); + message.setPayload(payload); + return client.publish(topic, message); + } + + public void subscribeAndWait(String topic, MqttQoS qoS) throws MqttException { + subscribe(topic, qoS).waitForCompletion(TIMEOUT_MS); + } + + public IMqttToken subscribe(String topic, MqttQoS qoS) throws MqttException { + return client.subscribe(topic, qoS.value()); + } + + private MqttAsyncClient createClient(String clientId) throws MqttException { + if (StringUtils.isEmpty(clientId)) { + clientId = MqttAsyncClient.generateClientId(); + } + return new MqttAsyncClient(MQTT_URL, clientId, new MemoryPersistence()); + } + + private MqttAsyncClient createClient() throws MqttException { + return createClient(null); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java index 530e67a625..261f904e2d 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java @@ -22,36 +22,47 @@ import com.google.protobuf.InvalidProtocolBufferException; import com.squareup.wire.schema.internal.parser.ProtoFileElement; import io.netty.handler.codec.mqtt.MqttQoS; import lombok.extern.slf4j.Slf4j; -import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; -import org.eclipse.paho.client.mqttv3.MqttAsyncClient; -import org.eclipse.paho.client.mqttv3.MqttCallback; -import org.eclipse.paho.client.mqttv3.MqttException; -import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; -import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.DeviceTypeFilter; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.gen.transport.TransportApiProtos; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; +import org.thingsboard.server.transport.mqtt.MqttTestCallback; +import org.thingsboard.server.transport.mqtt.MqttTestClient; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - +import static org.thingsboard.server.common.data.device.profile.MqttTopics.GATEWAY_ATTRIBUTES_REQUEST_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.GATEWAY_ATTRIBUTES_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.GATEWAY_CONNECT_TOPIC; +import static org.thingsboard.server.common.data.query.EntityKeyType.CLIENT_ATTRIBUTE; +import static org.thingsboard.server.common.data.query.EntityKeyType.SHARED_ATTRIBUTE; + +@TestPropertySource(properties = { + "js.evaluator=mock", +}) @Slf4j public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqttIntegrationTest { @@ -60,11 +71,11 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt "package test;\n" + "\n" + "message PostAttributes {\n" + - " string attribute1 = 1;\n" + - " bool attribute2 = 2;\n" + - " double attribute3 = 3;\n" + - " int32 attribute4 = 4;\n" + - " JsonObject attribute5 = 5;\n" + + " string clientStr = 1;\n" + + " bool clientBool = 2;\n" + + " double clientDbl = 3;\n" + + " int32 clientLong = 4;\n" + + " JsonObject clientJson = 5;\n" + "\n" + " message JsonObject {\n" + " int32 someNumber = 6;\n" + @@ -76,17 +87,20 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt " }\n" + "}"; - protected static final String POST_ATTRIBUTES_PAYLOAD = "{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73," + - "\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}"; + private static final String CLIENT_ATTRIBUTES_PAYLOAD = "{\"clientStr\":\"value1\",\"clientBool\":true,\"clientDbl\":42.0,\"clientLong\":73," + + "\"clientJson\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}"; + + private static final String SHARED_ATTRIBUTES_PAYLOAD = "{\"sharedStr\":\"value1\",\"sharedBool\":true,\"sharedDbl\":42.0,\"sharedLong\":73," + + "\"sharedJson\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}"; - private static final String RESPONSE_ATTRIBUTES_PAYLOAD_DELETED = "{\"deleted\":[\"attribute5\"]}"; + private static final String SHARED_ATTRIBUTES_DELETED_RESPONSE = "{\"deleted\":[\"sharedJson\"]}"; - protected List getTsKvProtoList() { - TransportProtos.TsKvProto tsKvProtoAttribute1 = getTsKvProto("attribute1", "value1", TransportProtos.KeyValueType.STRING_V); - TransportProtos.TsKvProto tsKvProtoAttribute2 = getTsKvProto("attribute2", "true", TransportProtos.KeyValueType.BOOLEAN_V); - TransportProtos.TsKvProto tsKvProtoAttribute3 = getTsKvProto("attribute3", "42.0", TransportProtos.KeyValueType.DOUBLE_V); - TransportProtos.TsKvProto tsKvProtoAttribute4 = getTsKvProto("attribute4", "73", TransportProtos.KeyValueType.LONG_V); - TransportProtos.TsKvProto tsKvProtoAttribute5 = getTsKvProto("attribute5", "{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}", TransportProtos.KeyValueType.JSON_V); + private List getTsKvProtoList(String attributePrefix) { + TransportProtos.TsKvProto tsKvProtoAttribute1 = getTsKvProto(attributePrefix + "Str", "value1", TransportProtos.KeyValueType.STRING_V); + TransportProtos.TsKvProto tsKvProtoAttribute2 = getTsKvProto(attributePrefix + "Bool", "true", TransportProtos.KeyValueType.BOOLEAN_V); + TransportProtos.TsKvProto tsKvProtoAttribute3 = getTsKvProto(attributePrefix + "Dbl", "42.0", TransportProtos.KeyValueType.DOUBLE_V); + TransportProtos.TsKvProto tsKvProtoAttribute4 = getTsKvProto(attributePrefix + "Long", "73", TransportProtos.KeyValueType.LONG_V); + TransportProtos.TsKvProto tsKvProtoAttribute5 = getTsKvProto(attributePrefix + "Json", "{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}", TransportProtos.KeyValueType.JSON_V); List tsKvProtoList = new ArrayList<>(); tsKvProtoList.add(tsKvProtoAttribute1); tsKvProtoList.add(tsKvProtoAttribute2); @@ -103,118 +117,56 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt return tsKvProtoBuilder.build(); } - protected TestMqttCallback getTestMqttCallback() { - CountDownLatch latch = new CountDownLatch(1); - return new TestMqttCallback(latch); - } - - protected static class TestMqttCallback implements MqttCallback { - - private final CountDownLatch latch; - private Integer qoS; - private byte[] payloadBytes; - - TestMqttCallback(CountDownLatch latch) { - this.latch = latch; - } - - public int getQoS() { - return qoS; - } - - public byte[] getPayloadBytes() { - return payloadBytes; - } - - public CountDownLatch getLatch() { - return latch; - } - - @Override - public void connectionLost(Throwable throwable) { - } - - @Override - public void messageArrived(String requestTopic, MqttMessage mqttMessage) throws Exception { - qoS = mqttMessage.getQos(); - payloadBytes = mqttMessage.getPayload(); - latch.countDown(); - } - - @Override - public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { - - } - } - // subscribe to attributes updates from server methods protected void processJsonTestSubscribeToAttributesUpdates(String attrSubTopic) throws Exception { - - MqttAsyncClient client = getMqttAsyncClient(accessToken); - - TestMqttCallback onUpdateCallback = getTestMqttCallback(); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(accessToken); + MqttTestCallback onUpdateCallback = new MqttTestCallback(); client.setCallback(onUpdateCallback); + client.subscribeAndWait(attrSubTopic, MqttQoS.AT_MOST_ONCE); - client.subscribe(attrSubTopic, MqttQoS.AT_MOST_ONCE.value()); + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + onUpdateCallback.getSubscribeLatch().await(3, TimeUnit.SECONDS); - Thread.sleep(1000); + validateUpdateAttributesJsonResponse(onUpdateCallback, SHARED_ATTRIBUTES_PAYLOAD); - doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); - onUpdateCallback.getLatch().await(3, TimeUnit.SECONDS); - - validateUpdateAttributesJsonResponse(onUpdateCallback); - - TestMqttCallback onDeleteCallback = getTestMqttCallback(); + MqttTestCallback onDeleteCallback = new MqttTestCallback(); client.setCallback(onDeleteCallback); - - doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=attribute5", String.class); - onDeleteCallback.getLatch().await(3, TimeUnit.SECONDS); - - validateDeleteAttributesJsonResponse(onDeleteCallback); + doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=sharedJson", String.class); + onDeleteCallback.getSubscribeLatch().await(3, TimeUnit.SECONDS); + validateUpdateAttributesJsonResponse(onDeleteCallback, SHARED_ATTRIBUTES_DELETED_RESPONSE); + client.disconnect(); } protected void processProtoTestSubscribeToAttributesUpdates(String attrSubTopic) throws Exception { - - MqttAsyncClient client = getMqttAsyncClient(accessToken); - - TestMqttCallback onUpdateCallback = getTestMqttCallback(); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(accessToken); + MqttTestCallback onUpdateCallback = new MqttTestCallback(); client.setCallback(onUpdateCallback); + client.subscribeAndWait(attrSubTopic, MqttQoS.AT_MOST_ONCE); - client.subscribe(attrSubTopic, MqttQoS.AT_MOST_ONCE.value()); - - Thread.sleep(1000); - - doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); - onUpdateCallback.getLatch().await(3, TimeUnit.SECONDS); - + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + onUpdateCallback.getSubscribeLatch().await(3, TimeUnit.SECONDS); validateUpdateAttributesProtoResponse(onUpdateCallback); - TestMqttCallback onDeleteCallback = getTestMqttCallback(); + MqttTestCallback onDeleteCallback = new MqttTestCallback(); client.setCallback(onDeleteCallback); - - doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=attribute5", String.class); - onDeleteCallback.getLatch().await(3, TimeUnit.SECONDS); - + doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=sharedJson", String.class); + onDeleteCallback.getSubscribeLatch().await(3, TimeUnit.SECONDS); validateDeleteAttributesProtoResponse(onDeleteCallback); + client.disconnect(); } - protected void validateUpdateAttributesJsonResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { - assertNotNull(callback.getPayloadBytes()); - String response = new String(callback.getPayloadBytes(), StandardCharsets.UTF_8); - assertEquals(JacksonUtil.toJsonNode(POST_ATTRIBUTES_PAYLOAD), JacksonUtil.toJsonNode(response)); - } - - protected void validateDeleteAttributesJsonResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + protected void validateUpdateAttributesJsonResponse(MqttTestCallback callback, String expectedResponse) { assertNotNull(callback.getPayloadBytes()); - String response = new String(callback.getPayloadBytes(), StandardCharsets.UTF_8); - assertEquals(JacksonUtil.toJsonNode(RESPONSE_ATTRIBUTES_PAYLOAD_DELETED), JacksonUtil.toJsonNode(response)); + assertEquals(JacksonUtil.toJsonNode(expectedResponse), JacksonUtil.fromBytes(callback.getPayloadBytes())); } - protected void validateUpdateAttributesProtoResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + protected void validateUpdateAttributesProtoResponse(MqttTestCallback callback) throws InvalidProtocolBufferException { assertNotNull(callback.getPayloadBytes()); TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder(); - List tsKvProtoList = getTsKvProtoList(); + List tsKvProtoList = getTsKvProtoList("shared"); attributeUpdateNotificationMsgBuilder.addAllSharedUpdated(tsKvProtoList); TransportProtos.AttributeUpdateNotificationMsg expectedAttributeUpdateNotificationMsg = attributeUpdateNotificationMsgBuilder.build(); @@ -227,134 +179,99 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt assertTrue(actualSharedUpdatedList.containsAll(expectedSharedUpdatedList)); } - protected void validateDeleteAttributesProtoResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + protected void validateDeleteAttributesProtoResponse(MqttTestCallback callback) throws InvalidProtocolBufferException { assertNotNull(callback.getPayloadBytes()); TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder(); - attributeUpdateNotificationMsgBuilder.addSharedDeleted("attribute5"); + attributeUpdateNotificationMsgBuilder.addSharedDeleted("sharedJson"); TransportProtos.AttributeUpdateNotificationMsg expectedAttributeUpdateNotificationMsg = attributeUpdateNotificationMsgBuilder.build(); TransportProtos.AttributeUpdateNotificationMsg actualAttributeUpdateNotificationMsg = TransportProtos.AttributeUpdateNotificationMsg.parseFrom(callback.getPayloadBytes()); assertEquals(expectedAttributeUpdateNotificationMsg.getSharedDeletedList().size(), actualAttributeUpdateNotificationMsg.getSharedDeletedList().size()); - assertEquals("attribute5", actualAttributeUpdateNotificationMsg.getSharedDeletedList().get(0)); + assertEquals("sharedJson", actualAttributeUpdateNotificationMsg.getSharedDeletedList().get(0)); } protected void processJsonGatewayTestSubscribeToAttributesUpdates() throws Exception { - - MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); - - TestMqttCallback onUpdateCallback = getTestMqttCallback(); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(gatewayAccessToken); + MqttTestCallback onUpdateCallback = new MqttTestCallback(); client.setCallback(onUpdateCallback); - Device device = new Device(); - device.setName("Gateway Device Subscribe to attribute updates"); - device.setType("default"); - - byte[] connectPayloadBytes = getJsonConnectPayloadBytes(); + String deviceName = "Gateway Device Subscribe to attribute updates"; + byte[] connectPayloadBytes = getJsonConnectPayloadBytes(deviceName, deviceProfile.getTransportType().name()); - publishMqttMsg(client, connectPayloadBytes, MqttTopics.GATEWAY_CONNECT_TOPIC); + client.publishAndWait(GATEWAY_CONNECT_TOPIC, connectPayloadBytes); - Device savedDevice = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + "Gateway Device Subscribe to attribute updates", Device.class), + Device savedDevice = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class), 20, 100); assertNotNull(savedDevice); - client.subscribe(MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, MqttQoS.AT_MOST_ONCE.value()); + client.subscribeAndWait(GATEWAY_ATTRIBUTES_TOPIC, MqttQoS.AT_MOST_ONCE); - Thread.sleep(1000); + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + onUpdateCallback.getSubscribeLatch().await(3, TimeUnit.SECONDS); - doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); - onUpdateCallback.getLatch().await(3, TimeUnit.SECONDS); + validateJsonGatewayUpdateAttributesResponse(onUpdateCallback, deviceName, SHARED_ATTRIBUTES_PAYLOAD); - validateJsonGatewayUpdateAttributesResponse(onUpdateCallback); - - TestMqttCallback onDeleteCallback = getTestMqttCallback(); + MqttTestCallback onDeleteCallback = new MqttTestCallback(); client.setCallback(onDeleteCallback); - doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=attribute5", String.class); - onDeleteCallback.getLatch().await(3, TimeUnit.SECONDS); - - validateJsonGatewayDeleteAttributesResponse(onDeleteCallback); + doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=sharedJson", String.class); + onDeleteCallback.getSubscribeLatch().await(3, TimeUnit.SECONDS); + validateJsonGatewayUpdateAttributesResponse(onDeleteCallback, deviceName, SHARED_ATTRIBUTES_DELETED_RESPONSE); + client.disconnect(); } protected void processProtoGatewayTestSubscribeToAttributesUpdates() throws Exception { - - MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); - - TestMqttCallback onUpdateCallback = getTestMqttCallback(); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(gatewayAccessToken); + MqttTestCallback onUpdateCallback = new MqttTestCallback(); client.setCallback(onUpdateCallback); - - Device device = new Device(); - device.setName("Gateway Device Subscribe to attribute updates"); - device.setType("default"); - - byte[] connectPayloadBytes = getProtoConnectPayloadBytes(); - - publishMqttMsg(client, connectPayloadBytes, MqttTopics.GATEWAY_CONNECT_TOPIC); - - Device savedDevice = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + "Gateway Device Subscribe to attribute updates", Device.class), + String deviceName = "Gateway Device Subscribe to attribute updates"; + byte[] connectPayloadBytes = getProtoConnectPayloadBytes(deviceName, TransportPayloadType.PROTOBUF.name()); + client.publishAndWait(GATEWAY_CONNECT_TOPIC, connectPayloadBytes); + Device device = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class), 20, 100); - - assertNotNull(savedDevice); - - client.subscribe(MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, MqttQoS.AT_MOST_ONCE.value()); - - Thread.sleep(1000); - - doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); - onUpdateCallback.getLatch().await(3, TimeUnit.SECONDS); - - validateProtoGatewayUpdateAttributesResponse(onUpdateCallback); - - TestMqttCallback onDeleteCallback = getTestMqttCallback(); + assertNotNull(device); + client.subscribeAndWait(GATEWAY_ATTRIBUTES_TOPIC, MqttQoS.AT_MOST_ONCE); + doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + validateProtoGatewayUpdateAttributesResponse(onUpdateCallback, deviceName); + MqttTestCallback onDeleteCallback = new MqttTestCallback(); client.setCallback(onDeleteCallback); - - doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=attribute5", String.class); - onDeleteCallback.getLatch().await(3, TimeUnit.SECONDS); - - validateProtoGatewayDeleteAttributesResponse(onDeleteCallback); - - } - - protected void validateJsonGatewayUpdateAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { - assertNotNull(callback.getPayloadBytes()); - String s = new String(callback.getPayloadBytes(), StandardCharsets.UTF_8); - assertEquals(getJsonResponseGatewayAttributesUpdatedPayload(), s); + doDelete("/api/plugins/telemetry/DEVICE/" + device.getId().getId() + "/SHARED_SCOPE?keys=sharedJson", String.class); + validateProtoGatewayDeleteAttributesResponse(onDeleteCallback, deviceName); + client.disconnect(); } - protected void validateJsonGatewayDeleteAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + protected void validateJsonGatewayUpdateAttributesResponse(MqttTestCallback callback, String deviceName, String expectResultData) { assertNotNull(callback.getPayloadBytes()); - String s = new String(callback.getPayloadBytes(), StandardCharsets.UTF_8); - assertEquals(s, getJsonResponseGatewayAttributesDeletedPayload()); + assertEquals(JacksonUtil.toJsonNode(getGatewayAttributesResponseJson(deviceName, expectResultData)), JacksonUtil.fromBytes(callback.getPayloadBytes())); } - protected byte[] getJsonConnectPayloadBytes() { - String connectPayload = "{\"device\": \"Gateway Device Subscribe to attribute updates\", \"type\": \"" + TransportPayloadType.JSON.name() + "\"}"; + protected byte[] getJsonConnectPayloadBytes(String deviceName, String deviceType) { + String connectPayload = "{\"device\":\"" + deviceName + "\", \"type\": \"" + deviceType + "\"}"; return connectPayload.getBytes(); } - private static String getJsonResponseGatewayAttributesUpdatedPayload() { - return "{\"device\":\"" + "Gateway Device Subscribe to attribute updates" + "\"," + - "\"data\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}"; - } - - private static String getJsonResponseGatewayAttributesDeletedPayload() { - return "{\"device\":\"" + "Gateway Device Subscribe to attribute updates" + "\",\"data\":{\"deleted\":[\"attribute5\"]}}"; + private static String getGatewayAttributesResponseJson(String deviceName, String expectResultData) { + return "{\"device\":\"" + deviceName + "\"," + "\"data\":" + expectResultData + "}"; } - protected void validateProtoGatewayUpdateAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + protected void validateProtoGatewayUpdateAttributesResponse(MqttTestCallback callback, String deviceName) throws InvalidProtocolBufferException, InterruptedException { + callback.getSubscribeLatch().await(3, TimeUnit.SECONDS); assertNotNull(callback.getPayloadBytes()); TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder(); - List tsKvProtoList = getTsKvProtoList(); + List tsKvProtoList = getTsKvProtoList("shared"); attributeUpdateNotificationMsgBuilder.addAllSharedUpdated(tsKvProtoList); TransportProtos.AttributeUpdateNotificationMsg expectedAttributeUpdateNotificationMsg = attributeUpdateNotificationMsgBuilder.build(); TransportApiProtos.GatewayAttributeUpdateNotificationMsg.Builder gatewayAttributeUpdateNotificationMsgBuilder = TransportApiProtos.GatewayAttributeUpdateNotificationMsg.newBuilder(); - gatewayAttributeUpdateNotificationMsgBuilder.setDeviceName("Gateway Device Subscribe to attribute updates"); + gatewayAttributeUpdateNotificationMsgBuilder.setDeviceName(deviceName); gatewayAttributeUpdateNotificationMsgBuilder.setNotificationMsg(expectedAttributeUpdateNotificationMsg); TransportApiProtos.GatewayAttributeUpdateNotificationMsg expectedGatewayAttributeUpdateNotificationMsg = gatewayAttributeUpdateNotificationMsgBuilder.build(); @@ -367,17 +284,17 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt assertEquals(expectedSharedUpdatedList.size(), actualSharedUpdatedList.size()); assertTrue(actualSharedUpdatedList.containsAll(expectedSharedUpdatedList)); - } - protected void validateProtoGatewayDeleteAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + protected void validateProtoGatewayDeleteAttributesResponse(MqttTestCallback callback, String deviceName) throws InvalidProtocolBufferException, InterruptedException { + callback.getSubscribeLatch().await(3, TimeUnit.SECONDS); assertNotNull(callback.getPayloadBytes()); TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder(); - attributeUpdateNotificationMsgBuilder.addSharedDeleted("attribute5"); + attributeUpdateNotificationMsgBuilder.addSharedDeleted("sharedJson"); TransportProtos.AttributeUpdateNotificationMsg attributeUpdateNotificationMsg = attributeUpdateNotificationMsgBuilder.build(); TransportApiProtos.GatewayAttributeUpdateNotificationMsg.Builder gatewayAttributeUpdateNotificationMsgBuilder = TransportApiProtos.GatewayAttributeUpdateNotificationMsg.newBuilder(); - gatewayAttributeUpdateNotificationMsgBuilder.setDeviceName("Gateway Device Subscribe to attribute updates"); + gatewayAttributeUpdateNotificationMsgBuilder.setDeviceName(deviceName); gatewayAttributeUpdateNotificationMsgBuilder.setNotificationMsg(attributeUpdateNotificationMsg); TransportApiProtos.GatewayAttributeUpdateNotificationMsg expectedGatewayAttributeUpdateNotificationMsg = gatewayAttributeUpdateNotificationMsgBuilder.build(); @@ -389,118 +306,187 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt TransportProtos.AttributeUpdateNotificationMsg actualAttributeUpdateNotificationMsg = actualGatewayAttributeUpdateNotificationMsg.getNotificationMsg(); assertEquals(expectedAttributeUpdateNotificationMsg.getSharedDeletedList().size(), actualAttributeUpdateNotificationMsg.getSharedDeletedList().size()); - assertEquals("attribute5", actualAttributeUpdateNotificationMsg.getSharedDeletedList().get(0)); - - } - - protected byte[] getProtoConnectPayloadBytes() { - TransportApiProtos.ConnectMsg connectProto = getConnectProto(); - return connectProto.toByteArray(); + assertEquals("sharedJson", actualAttributeUpdateNotificationMsg.getSharedDeletedList().get(0)); } - private TransportApiProtos.ConnectMsg getConnectProto() { - TransportApiProtos.ConnectMsg.Builder builder = TransportApiProtos.ConnectMsg.newBuilder(); - builder.setDeviceName("Gateway Device Subscribe to attribute updates"); - builder.setDeviceType(TransportPayloadType.PROTOBUF.name()); - return builder.build(); + private byte[] getProtoConnectPayloadBytes(String deviceName, String deviceType) { + TransportApiProtos.ConnectMsg connectMsg = TransportApiProtos.ConnectMsg.newBuilder() + .setDeviceName(deviceName) + .setDeviceType(deviceType) + .build(); + return connectMsg.toByteArray(); } // request attributes from server methods protected void processJsonTestRequestAttributesValuesFromTheServer(String attrPubTopic, String attrSubTopic, String attrReqTopicPrefix) throws Exception { - - MqttAsyncClient client = getMqttAsyncClient(accessToken); - - postJsonAttributesAndSubscribeToTopic(savedDevice, client, attrPubTopic, attrSubTopic); - - Thread.sleep(5000); - - TestMqttCallback callback = getTestMqttCallback(); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(accessToken); + DeviceTypeFilter dtf = new DeviceTypeFilter(savedDevice.getType(), savedDevice.getName()); + String clientKeysStr = "clientStr,clientBool,clientDbl,clientLong,clientJson"; + String sharedKeysStr = "sharedStr,sharedBool,sharedDbl,sharedLong,sharedJson"; + List clientKeysList = List.of(clientKeysStr.split(",")); + List sharedKeysList = List.of(sharedKeysStr.split(",")); + List csKeys = getEntityKeys(clientKeysList, CLIENT_ATTRIBUTE); + List shKeys = getEntityKeys(sharedKeysList, SHARED_ATTRIBUTE); + List keys = new ArrayList<>(); + keys.addAll(csKeys); + keys.addAll(shKeys); + getWsClient().subscribeLatestUpdate(keys, dtf); + getWsClient().registerWaitForUpdate(2); + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", + SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + client.publishAndWait(attrPubTopic, CLIENT_ATTRIBUTES_PAYLOAD.getBytes()); + client.subscribeAndWait(attrSubTopic, MqttQoS.AT_MOST_ONCE); + String update = getWsClient().waitForUpdate(); + assertThat(update).as("ws update received").isNotBlank(); + MqttTestCallback callback = new MqttTestCallback(attrSubTopic.replace("+", "1")); client.setCallback(callback); - - validateJsonResponse(client, callback.getLatch(), callback, attrReqTopicPrefix); + String payloadStr = "{\"clientKeys\":\"" + clientKeysStr + "\", \"sharedKeys\":\"" + sharedKeysStr + "\"}"; + client.publishAndWait(attrReqTopicPrefix + "1", payloadStr.getBytes()); + String expectedResponse = "{\"client\":" + CLIENT_ATTRIBUTES_PAYLOAD + ",\"shared\":" + SHARED_ATTRIBUTES_PAYLOAD + "}"; + validateJsonResponse(callback, expectedResponse); + client.disconnect(); } protected void processProtoTestRequestAttributesValuesFromTheServer(String attrPubTopic, String attrSubTopic, String attrReqTopicPrefix) throws Exception { - - MqttAsyncClient client = getMqttAsyncClient(accessToken); - - postProtoAttributesAndSubscribeToTopic(savedDevice, client, attrPubTopic, attrSubTopic); - - Thread.sleep(5000); - - TestMqttCallback callback = getTestMqttCallback(); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(accessToken); + DeviceTypeFilter dtf = new DeviceTypeFilter(savedDevice.getType(), savedDevice.getName()); + String clientKeysStr = "clientStr,clientBool,clientDbl,clientLong,clientJson"; + String sharedKeysStr = "sharedStr,sharedBool,sharedDbl,sharedLong,sharedJson"; + List clientKeysList = List.of(clientKeysStr.split(",")); + List sharedKeysList = List.of(sharedKeysStr.split(",")); + List csKeys = getEntityKeys(clientKeysList, CLIENT_ATTRIBUTE); + List shKeys = getEntityKeys(sharedKeysList, SHARED_ATTRIBUTE); + List keys = new ArrayList<>(); + keys.addAll(csKeys); + keys.addAll(shKeys); + getWsClient().subscribeLatestUpdate(keys, dtf); + getWsClient().registerWaitForUpdate(2); + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + client.publishAndWait(attrPubTopic, getAttributesProtoPayloadBytes()); + client.subscribeAndWait(attrSubTopic, MqttQoS.AT_MOST_ONCE); + String update = getWsClient().waitForUpdate(); + assertThat(update).as("ws update received").isNotBlank(); + MqttTestCallback callback = new MqttTestCallback(attrSubTopic.replace("+", "1")); client.setCallback(callback); - - validateProtoResponse(client, callback.getLatch(), callback, attrReqTopicPrefix); + TransportApiProtos.AttributesRequest.Builder attributesRequestBuilder = TransportApiProtos.AttributesRequest.newBuilder(); + attributesRequestBuilder.setClientKeys(clientKeysStr); + attributesRequestBuilder.setSharedKeys(sharedKeysStr); + TransportApiProtos.AttributesRequest attributesRequest = attributesRequestBuilder.build(); + client.publishAndWait(attrReqTopicPrefix + "1", attributesRequest.toByteArray()); + validateProtoResponse(callback, getExpectedAttributeResponseMsg()); + client.disconnect(); } protected void processJsonTestGatewayRequestAttributesValuesFromTheServer() throws Exception { + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(gatewayAccessToken); + String deviceName = "Gateway Device Request Attributes"; + String postClientAttributes = "{\"" + deviceName + "\":" + CLIENT_ATTRIBUTES_PAYLOAD + "}"; + client.publishAndWait(GATEWAY_ATTRIBUTES_TOPIC, postClientAttributes.getBytes()); - MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); - - postJsonGatewayDeviceClientAttributes(client); - - Device savedDevice = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + "Gateway Device Request Attributes", Device.class), + Device device = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class), 20, 100); - - assertNotNull(savedDevice); - - Thread.sleep(2000); - - doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); - - Thread.sleep(5000); - - client.subscribe(MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC, MqttQoS.AT_LEAST_ONCE.value()).waitForCompletion(TimeUnit.MINUTES.toMillis(1)); - - TestMqttCallback clientAttributesCallback = getTestMqttCallback(); + assertNotNull(device); + + DeviceTypeFilter dtf = new DeviceTypeFilter(device.getType(), device.getName()); + String clientKeysStr = "clientStr,clientBool,clientDbl,clientLong,clientJson"; + String sharedKeysStr = "sharedStr,sharedBool,sharedDbl,sharedLong,sharedJson"; + List clientKeysList = List.of(clientKeysStr.split(",")); + List sharedKeysList = List.of(sharedKeysStr.split(",")); + List csKeys = getEntityKeys(clientKeysList, CLIENT_ATTRIBUTE); + List shKeys = getEntityKeys(sharedKeysList, SHARED_ATTRIBUTE); + List keys = new ArrayList<>(); + keys.addAll(csKeys); + keys.addAll(shKeys); + EntityDataUpdate initUpdate = getWsClient().subscribeLatestUpdate(keys, dtf); + assertNotNull(initUpdate); + PageData data = initUpdate.getData(); + assertNotNull(data); + assertFalse(data.getData().isEmpty()); + getWsClient().registerWaitForUpdate(); + + doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + String update = getWsClient().waitForUpdate(); + assertThat(update).as("ws update received").isNotBlank(); + + client.subscribeAndWait(GATEWAY_ATTRIBUTES_RESPONSE_TOPIC, MqttQoS.AT_LEAST_ONCE); + + MqttTestCallback clientAttributesCallback = new MqttTestCallback(GATEWAY_ATTRIBUTES_RESPONSE_TOPIC); client.setCallback(clientAttributesCallback); - validateJsonClientResponseGateway(client, clientAttributesCallback); + String csKeysStr = "[\"clientStr\", \"clientBool\", \"clientDbl\", \"clientLong\", \"clientJson\"]"; + String csRequestPayloadStr = "{\"id\": 1, \"device\": \"" + deviceName + "\", \"client\": true, \"keys\": " + csKeysStr + "}"; + client.publishAndWait(GATEWAY_ATTRIBUTES_REQUEST_TOPIC, csRequestPayloadStr.getBytes()); + validateJsonResponseGateway(clientAttributesCallback, deviceName, CLIENT_ATTRIBUTES_PAYLOAD); - TestMqttCallback sharedAttributesCallback = getTestMqttCallback(); + MqttTestCallback sharedAttributesCallback = new MqttTestCallback(GATEWAY_ATTRIBUTES_RESPONSE_TOPIC); client.setCallback(sharedAttributesCallback); - validateJsonSharedResponseGateway(client, sharedAttributesCallback); + String shKeysStr = "[\"sharedStr\", \"sharedBool\", \"sharedDbl\", \"sharedLong\", \"sharedJson\"]"; + String shRequestPayloadStr = "{\"id\": 1, \"device\": \"" + deviceName + "\", \"client\": false, \"keys\": " + shKeysStr + "}"; + client.publishAndWait(GATEWAY_ATTRIBUTES_REQUEST_TOPIC, shRequestPayloadStr.getBytes()); + validateJsonResponseGateway(sharedAttributesCallback, deviceName, SHARED_ATTRIBUTES_PAYLOAD); + + client.disconnect(); } protected void processProtoTestGatewayRequestAttributesValuesFromTheServer() throws Exception { + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(gatewayAccessToken); - MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + String deviceName = "Gateway Device Request Attributes"; + String clientKeysStr = "clientStr,clientBool,clientDbl,clientLong,clientJson"; + List clientKeysList = List.of(clientKeysStr.split(",")); + client.publishAndWait(GATEWAY_ATTRIBUTES_TOPIC, getProtoGatewayDeviceClientAttributesPayload(deviceName, clientKeysList)); - postProtoGatewayDeviceClientAttributes(client); - - Device savedDevice = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + "Gateway Device Request Attributes", Device.class), + Device device = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class), 20, 100); - - assertNotNull(savedDevice); - - Thread.sleep(2000); - - doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); - - Thread.sleep(5000); - - client.subscribe(MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC, MqttQoS.AT_LEAST_ONCE.value()).waitForCompletion(TimeUnit.MINUTES.toMillis(1)); - - TestMqttCallback clientAttributesCallback = getTestMqttCallback(); + assertNotNull(device); + + DeviceTypeFilter dtf = new DeviceTypeFilter(device.getType(), device.getName()); + String sharedKeysStr = "sharedStr,sharedBool,sharedDbl,sharedLong,sharedJson"; + List sharedKeysList = List.of(sharedKeysStr.split(",")); + List csKeys = getEntityKeys(clientKeysList, CLIENT_ATTRIBUTE); + List shKeys = getEntityKeys(sharedKeysList, SHARED_ATTRIBUTE); + List keys = new ArrayList<>(); + keys.addAll(csKeys); + keys.addAll(shKeys); + EntityDataUpdate initUpdate = getWsClient().subscribeLatestUpdate(keys, dtf); + assertNotNull(initUpdate); + PageData data = initUpdate.getData(); + assertNotNull(data); + assertFalse(data.getData().isEmpty()); + getWsClient().registerWaitForUpdate(); + + doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + String update = getWsClient().waitForUpdate(); + assertThat(update).as("ws update received").isNotBlank(); + + client.subscribeAndWait(GATEWAY_ATTRIBUTES_RESPONSE_TOPIC, MqttQoS.AT_LEAST_ONCE); + + MqttTestCallback clientAttributesCallback = new MqttTestCallback(GATEWAY_ATTRIBUTES_RESPONSE_TOPIC); client.setCallback(clientAttributesCallback); - validateProtoClientResponseGateway(client, clientAttributesCallback); + TransportApiProtos.GatewayAttributesRequestMsg gatewayAttributesRequestMsg = getGatewayAttributesRequestMsg(deviceName, clientKeysList, true); + client.publishAndWait(GATEWAY_ATTRIBUTES_REQUEST_TOPIC, gatewayAttributesRequestMsg.toByteArray()); + validateProtoClientResponseGateway(clientAttributesCallback, deviceName); - TestMqttCallback sharedAttributesCallback = getTestMqttCallback(); + MqttTestCallback sharedAttributesCallback = new MqttTestCallback(GATEWAY_ATTRIBUTES_RESPONSE_TOPIC); client.setCallback(sharedAttributesCallback); - validateProtoSharedResponseGateway(client, sharedAttributesCallback); + gatewayAttributesRequestMsg = getGatewayAttributesRequestMsg(deviceName, sharedKeysList, false); + client.publishAndWait(GATEWAY_ATTRIBUTES_REQUEST_TOPIC, gatewayAttributesRequestMsg.toByteArray()); + validateProtoSharedResponseGateway(sharedAttributesCallback, deviceName); + + client.disconnect(); } - protected void postJsonAttributesAndSubscribeToTopic(Device savedDevice, MqttAsyncClient client, String attrPubTopic, String attrSubTopic) throws Exception { - doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); - client.publish(attrPubTopic, new MqttMessage(POST_ATTRIBUTES_PAYLOAD.getBytes())).waitForCompletion(TimeUnit.MINUTES.toMillis(1)); - client.subscribe(attrSubTopic, MqttQoS.AT_MOST_ONCE.value()).waitForCompletion(TimeUnit.MINUTES.toMillis(1)); + private List getEntityKeys(List keys, EntityKeyType scope) { + return keys.stream().map(key -> new EntityKey(scope, key)).collect(Collectors.toList()); } - protected void postProtoAttributesAndSubscribeToTopic(Device savedDevice, MqttAsyncClient client, String attrPubTopic, String attrSubTopic) throws Exception { - doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", AbstractMqttAttributesIntegrationTest.POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + private byte[] getAttributesProtoPayloadBytes() { DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); assertTrue(transportConfiguration instanceof MqttDeviceProfileTransportConfiguration); MqttDeviceProfileTransportConfiguration mqttTransportConfiguration = (MqttDeviceProfileTransportConfiguration) transportConfiguration; @@ -530,64 +516,39 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt Descriptors.Descriptor postAttributesMsgDescriptor = postAttributesBuilder.getDescriptorForType(); assertNotNull(postAttributesMsgDescriptor); DynamicMessage postAttributesMsg = postAttributesBuilder - .setField(postAttributesMsgDescriptor.findFieldByName("attribute1"), "value1") - .setField(postAttributesMsgDescriptor.findFieldByName("attribute2"), true) - .setField(postAttributesMsgDescriptor.findFieldByName("attribute3"), 42.0) - .setField(postAttributesMsgDescriptor.findFieldByName("attribute4"), 73) - .setField(postAttributesMsgDescriptor.findFieldByName("attribute5"), jsonObject) + .setField(postAttributesMsgDescriptor.findFieldByName("clientStr"), "value1") + .setField(postAttributesMsgDescriptor.findFieldByName("clientBool"), true) + .setField(postAttributesMsgDescriptor.findFieldByName("clientDbl"), 42.0) + .setField(postAttributesMsgDescriptor.findFieldByName("clientLong"), 73) + .setField(postAttributesMsgDescriptor.findFieldByName("clientJson"), jsonObject) .build(); - byte[] payload = postAttributesMsg.toByteArray(); - client.publish(attrPubTopic, new MqttMessage(payload)); - client.subscribe(attrSubTopic, MqttQoS.AT_MOST_ONCE.value()); + return postAttributesMsg.toByteArray(); } - protected void postJsonGatewayDeviceClientAttributes(MqttAsyncClient client) throws Exception { - String postClientAttributes = "{\"" + "Gateway Device Request Attributes" + "\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}"; - client.publish(MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, new MqttMessage(postClientAttributes.getBytes())).waitForCompletion(TimeUnit.MINUTES.toMillis(1)); - } - - protected void postProtoGatewayDeviceClientAttributes(MqttAsyncClient client) throws Exception { - String keys = "attribute1,attribute2,attribute3,attribute4,attribute5"; - List expectedKeys = Arrays.asList(keys.split(",")); - TransportProtos.PostAttributeMsg postAttributeMsg = getPostAttributeMsg(expectedKeys); + protected byte[] getProtoGatewayDeviceClientAttributesPayload(String deviceName, List clientKeysList) { + TransportProtos.PostAttributeMsg postAttributeMsg = getPostAttributeMsg(clientKeysList); TransportApiProtos.AttributesMsg.Builder attributesMsgBuilder = TransportApiProtos.AttributesMsg.newBuilder(); - attributesMsgBuilder.setDeviceName("Gateway Device Request Attributes"); + attributesMsgBuilder.setDeviceName(deviceName); attributesMsgBuilder.setMsg(postAttributeMsg); TransportApiProtos.AttributesMsg attributesMsg = attributesMsgBuilder.build(); TransportApiProtos.GatewayAttributesMsg.Builder gatewayAttributeMsgBuilder = TransportApiProtos.GatewayAttributesMsg.newBuilder(); gatewayAttributeMsgBuilder.addMsg(attributesMsg); - byte[] bytes = gatewayAttributeMsgBuilder.build().toByteArray(); - client.publish(MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, new MqttMessage(bytes)); + return gatewayAttributeMsgBuilder.build().toByteArray(); } - protected void validateJsonResponse(MqttAsyncClient client, CountDownLatch latch, TestMqttCallback callback, String attrReqTopicPrefix) throws MqttException, InterruptedException, InvalidProtocolBufferException { - String keys = "attribute1,attribute2,attribute3,attribute4,attribute5"; - String payloadStr = "{\"clientKeys\":\"" + keys + "\", \"sharedKeys\":\"" + keys + "\"}"; - MqttMessage mqttMessage = new MqttMessage(); - mqttMessage.setPayload(payloadStr.getBytes()); - client.publish(attrReqTopicPrefix + "1", mqttMessage).waitForCompletion(TimeUnit.MINUTES.toMillis(1)); - latch.await(1, TimeUnit.MINUTES); + protected void validateJsonResponse(MqttTestCallback callback, String expectedResponse) throws InterruptedException { + callback.getSubscribeLatch().await(3, TimeUnit.SECONDS); assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); - String expectedRequestPayload = "{\"client\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}},\"shared\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}"; - assertEquals(JacksonUtil.toJsonNode(expectedRequestPayload), JacksonUtil.toJsonNode(new String(callback.getPayloadBytes(), StandardCharsets.UTF_8))); + assertEquals(JacksonUtil.toJsonNode(expectedResponse), JacksonUtil.fromBytes(callback.getPayloadBytes())); } - protected void validateProtoResponse(MqttAsyncClient client, CountDownLatch latch, TestMqttCallback callback, String attrReqTopic) throws MqttException, InterruptedException, InvalidProtocolBufferException { - String keys = "attribute1,attribute2,attribute3,attribute4,attribute5"; - TransportApiProtos.AttributesRequest.Builder attributesRequestBuilder = TransportApiProtos.AttributesRequest.newBuilder(); - attributesRequestBuilder.setClientKeys(keys); - attributesRequestBuilder.setSharedKeys(keys); - TransportApiProtos.AttributesRequest attributesRequest = attributesRequestBuilder.build(); - MqttMessage mqttMessage = new MqttMessage(); - mqttMessage.setPayload(attributesRequest.toByteArray()); - client.publish(attrReqTopic + "1", mqttMessage); - latch.await(3, TimeUnit.SECONDS); + protected void validateProtoResponse(MqttTestCallback callback, TransportProtos.GetAttributeResponseMsg expectedResponse) throws InterruptedException, InvalidProtocolBufferException { + callback.getSubscribeLatch().await(3, TimeUnit.SECONDS); assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); - TransportProtos.GetAttributeResponseMsg expectedAttributesResponse = getExpectedAttributeResponseMsg(); TransportProtos.GetAttributeResponseMsg actualAttributesResponse = TransportProtos.GetAttributeResponseMsg.parseFrom(callback.getPayloadBytes()); - assertEquals(expectedAttributesResponse.getRequestId(), actualAttributesResponse.getRequestId()); - List expectedClientKeyValueProtos = expectedAttributesResponse.getClientAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); - List expectedSharedKeyValueProtos = expectedAttributesResponse.getSharedAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + assertEquals(expectedResponse.getRequestId(), actualAttributesResponse.getRequestId()); + List expectedClientKeyValueProtos = expectedResponse.getClientAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + List expectedSharedKeyValueProtos = expectedResponse.getSharedAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); List actualClientKeyValueProtos = actualAttributesResponse.getClientAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); List actualSharedKeyValueProtos = actualAttributesResponse.getSharedAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); assertTrue(actualClientKeyValueProtos.containsAll(expectedClientKeyValueProtos)); @@ -596,42 +557,25 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt private TransportProtos.GetAttributeResponseMsg getExpectedAttributeResponseMsg() { TransportProtos.GetAttributeResponseMsg.Builder result = TransportProtos.GetAttributeResponseMsg.newBuilder(); - List tsKvProtoList = getTsKvProtoList(); - result.addAllClientAttributeList(tsKvProtoList); - result.addAllSharedAttributeList(tsKvProtoList); + List csTsKvProtoList = getTsKvProtoList("client"); + List shTsKvProtoList = getTsKvProtoList("shared"); + result.addAllClientAttributeList(csTsKvProtoList); + result.addAllSharedAttributeList(shTsKvProtoList); result.setRequestId(1); return result.build(); } - protected void validateJsonClientResponseGateway(MqttAsyncClient client, TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException { - String payloadStr = "{\"id\": 1, \"device\": \"" + "Gateway Device Request Attributes" + "\", \"client\": true, \"keys\": [\"attribute1\", \"attribute2\", \"attribute3\", \"attribute4\", \"attribute5\"]}"; - MqttMessage mqttMessage = new MqttMessage(); - mqttMessage.setPayload(payloadStr.getBytes()); - client.publish(MqttTopics.GATEWAY_ATTRIBUTES_REQUEST_TOPIC, mqttMessage).waitForCompletion(TimeUnit.MINUTES.toMillis(1)); - callback.getLatch().await(1, TimeUnit.MINUTES); - assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS()); - String expectedRequestPayload = "{\"id\":1,\"device\":\"" + "Gateway Device Request Attributes" + "\",\"values\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}"; - assertEquals(JacksonUtil.toJsonNode(expectedRequestPayload), JacksonUtil.toJsonNode(new String(callback.getPayloadBytes(), StandardCharsets.UTF_8))); - } - - protected void validateJsonSharedResponseGateway(MqttAsyncClient client, TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException { - String payloadStr = "{\"id\": 1, \"device\": \"" + "Gateway Device Request Attributes" + "\", \"client\": false, \"keys\": [\"attribute1\", \"attribute2\", \"attribute3\", \"attribute4\", \"attribute5\"]}"; - MqttMessage mqttMessage = new MqttMessage(); - mqttMessage.setPayload(payloadStr.getBytes()); - client.publish(MqttTopics.GATEWAY_ATTRIBUTES_REQUEST_TOPIC, mqttMessage).waitForCompletion(TimeUnit.MINUTES.toMillis(1)); - callback.getLatch().await(1, TimeUnit.MINUTES); + protected void validateJsonResponseGateway(MqttTestCallback callback, String deviceName, String expectedValues) throws InterruptedException { + callback.getSubscribeLatch().await(3, TimeUnit.SECONDS); assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS()); - String expectedRequestPayload = "{\"id\":1,\"device\":\"" + "Gateway Device Request Attributes" + "\",\"values\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}"; - assertEquals(JacksonUtil.toJsonNode(expectedRequestPayload), JacksonUtil.toJsonNode(new String(callback.getPayloadBytes(), StandardCharsets.UTF_8))); + String expectedRequestPayload = "{\"id\":1,\"device\":\"" + deviceName + "\",\"values\":" + expectedValues + "}"; + assertEquals(JacksonUtil.toJsonNode(expectedRequestPayload), JacksonUtil.fromBytes(callback.getPayloadBytes())); } - protected void validateProtoClientResponseGateway(MqttAsyncClient client, AbstractMqttAttributesIntegrationTest.TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException { - String keys = "attribute1,attribute2,attribute3,attribute4,attribute5"; - TransportApiProtos.GatewayAttributesRequestMsg gatewayAttributesRequestMsg = getGatewayAttributesRequestMsg(keys, true); - client.publish(MqttTopics.GATEWAY_ATTRIBUTES_REQUEST_TOPIC, new MqttMessage(gatewayAttributesRequestMsg.toByteArray())); - callback.getLatch().await(3, TimeUnit.SECONDS); + protected void validateProtoClientResponseGateway(MqttTestCallback callback, String deviceName) throws InterruptedException, InvalidProtocolBufferException { + callback.getSubscribeLatch().await(3, TimeUnit.SECONDS); assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS()); - TransportApiProtos.GatewayAttributeResponseMsg expectedGatewayAttributeResponseMsg = getExpectedGatewayAttributeResponseMsg(true); + TransportApiProtos.GatewayAttributeResponseMsg expectedGatewayAttributeResponseMsg = getExpectedGatewayAttributeResponseMsg(deviceName, true); TransportApiProtos.GatewayAttributeResponseMsg actualGatewayAttributeResponseMsg = TransportApiProtos.GatewayAttributeResponseMsg.parseFrom(callback.getPayloadBytes()); assertEquals(expectedGatewayAttributeResponseMsg.getDeviceName(), actualGatewayAttributeResponseMsg.getDeviceName()); @@ -644,13 +588,10 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt assertTrue(actualClientKeyValueProtos.containsAll(expectedClientKeyValueProtos)); } - protected void validateProtoSharedResponseGateway(MqttAsyncClient client, AbstractMqttAttributesIntegrationTest.TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException { - String keys = "attribute1,attribute2,attribute3,attribute4,attribute5"; - TransportApiProtos.GatewayAttributesRequestMsg gatewayAttributesRequestMsg = getGatewayAttributesRequestMsg(keys, false); - client.publish(MqttTopics.GATEWAY_ATTRIBUTES_REQUEST_TOPIC, new MqttMessage(gatewayAttributesRequestMsg.toByteArray())); - callback.getLatch().await(3, TimeUnit.SECONDS); + protected void validateProtoSharedResponseGateway(MqttTestCallback callback, String deviceName) throws InterruptedException, InvalidProtocolBufferException { + callback.getSubscribeLatch().await(3, TimeUnit.SECONDS); assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS()); - TransportApiProtos.GatewayAttributeResponseMsg expectedGatewayAttributeResponseMsg = getExpectedGatewayAttributeResponseMsg(false); + TransportApiProtos.GatewayAttributeResponseMsg expectedGatewayAttributeResponseMsg = getExpectedGatewayAttributeResponseMsg(deviceName, false); TransportApiProtos.GatewayAttributeResponseMsg actualGatewayAttributeResponseMsg = TransportApiProtos.GatewayAttributeResponseMsg.parseFrom(callback.getPayloadBytes()); assertEquals(expectedGatewayAttributeResponseMsg.getDeviceName(), actualGatewayAttributeResponseMsg.getDeviceName()); @@ -664,27 +605,26 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt assertTrue(actualSharedKeyValueProtos.containsAll(expectedSharedKeyValueProtos)); } - private TransportApiProtos.GatewayAttributeResponseMsg getExpectedGatewayAttributeResponseMsg(boolean client) { + private TransportApiProtos.GatewayAttributeResponseMsg getExpectedGatewayAttributeResponseMsg(String deviceName, boolean client) { TransportApiProtos.GatewayAttributeResponseMsg.Builder gatewayAttributeResponseMsg = TransportApiProtos.GatewayAttributeResponseMsg.newBuilder(); TransportProtos.GetAttributeResponseMsg.Builder getAttributeResponseMsgBuilder = TransportProtos.GetAttributeResponseMsg.newBuilder(); - List tsKvProtoList = getTsKvProtoList(); if (client) { - getAttributeResponseMsgBuilder.addAllClientAttributeList(tsKvProtoList); + getAttributeResponseMsgBuilder.addAllClientAttributeList(getTsKvProtoList("client")); } else { - getAttributeResponseMsgBuilder.addAllSharedAttributeList(tsKvProtoList); + getAttributeResponseMsgBuilder.addAllSharedAttributeList(getTsKvProtoList("shared")); } getAttributeResponseMsgBuilder.setRequestId(1); TransportProtos.GetAttributeResponseMsg getAttributeResponseMsg = getAttributeResponseMsgBuilder.build(); - gatewayAttributeResponseMsg.setDeviceName("Gateway Device Request Attributes"); + gatewayAttributeResponseMsg.setDeviceName(deviceName); gatewayAttributeResponseMsg.setResponseMsg(getAttributeResponseMsg); return gatewayAttributeResponseMsg.build(); } - private TransportApiProtos.GatewayAttributesRequestMsg getGatewayAttributesRequestMsg(String keys, boolean client) { + private TransportApiProtos.GatewayAttributesRequestMsg getGatewayAttributesRequestMsg(String deviceName, List keysList, boolean client) { return TransportApiProtos.GatewayAttributesRequestMsg.newBuilder() + .setDeviceName(deviceName) + .addAllKeys(keysList) .setClient(client) - .addAllKeys(Arrays.asList(keys.split(","))) - .setDeviceName("Gateway Device Request Attributes") .setId(1).build(); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/MqttAttributesRequestBackwardCompatibilityIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/MqttAttributesRequestBackwardCompatibilityIntegrationTest.java index f2bacc66ed..2231cb89eb 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/MqttAttributesRequestBackwardCompatibilityIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/MqttAttributesRequestBackwardCompatibilityIntegrationTest.java @@ -85,7 +85,6 @@ public class MqttAttributesRequestBackwardCompatibilityIntegrationTest extends A @Test public void testRequestAttributesValuesFromTheServerGatewayWithEnabledJsonCompatibilityAndJsonDownlinks() throws Exception { MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder() - .deviceName("Test Request attribute values from the server proto") .gatewayName("Gateway Test Request attribute values from the server proto") .transportPayloadType(TransportPayloadType.PROTOBUF) .enableCompatibilityWithJsonPayloadFormat(true) @@ -99,7 +98,6 @@ public class MqttAttributesRequestBackwardCompatibilityIntegrationTest extends A public void testRequestAttributesValuesFromTheServerOnShortJsonTopicWithEnabledJsonCompatibilityAndJsonDownlinks() throws Exception { MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder() .deviceName("Test Request attribute values from the server proto") - .gatewayName("Gateway Test Request attribute values from the server proto") .transportPayloadType(TransportPayloadType.PROTOBUF) .enableCompatibilityWithJsonPayloadFormat(true) .useJsonPayloadFormatForDefaultDownlinkTopics(true) diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/MqttAttributesRequestProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/MqttAttributesRequestProtoIntegrationTest.java index 402d9a7263..d330cdbbaf 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/MqttAttributesRequestProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/MqttAttributesRequestProtoIntegrationTest.java @@ -67,7 +67,6 @@ public class MqttAttributesRequestProtoIntegrationTest extends AbstractMqttAttri @Test public void testRequestAttributesValuesFromTheServerGateway() throws Exception { MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder() - .deviceName("Test Request attribute values from the server proto") .gatewayName("Gateway Test Request attribute values from the server proto") .transportPayloadType(TransportPayloadType.PROTOBUF) .build(); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/MqttAttributesUpdatesBackwardCompatibilityIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/MqttAttributesUpdatesBackwardCompatibilityIntegrationTest.java index c79f695fb6..2d72af2526 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/MqttAttributesUpdatesBackwardCompatibilityIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/MqttAttributesUpdatesBackwardCompatibilityIntegrationTest.java @@ -90,7 +90,6 @@ public class MqttAttributesUpdatesBackwardCompatibilityIntegrationTest extends A @Test public void testProtoSubscribeToAttributesUpdatesFromTheServerGatewayWithEnabledJsonCompatibilityAndJsonDownlinks() throws Exception { MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder() - .deviceName("Test Subscribe to attribute updates") .gatewayName("Gateway Test Subscribe to attribute updates") .transportPayloadType(TransportPayloadType.PROTOBUF) .enableCompatibilityWithJsonPayloadFormat(true) diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/credentials/BasicMqttCredentialsTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/credentials/BasicMqttCredentialsTest.java index 107d8dd0fa..795ca7f660 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/credentials/BasicMqttCredentialsTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/credentials/BasicMqttCredentialsTest.java @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; +import org.thingsboard.server.transport.mqtt.MqttTestClient; import java.util.Arrays; import java.util.HashSet; @@ -93,33 +94,51 @@ public class BasicMqttCredentialsTest extends AbstractMqttIntegrationTest { @Test public void testCorrectCredentials() throws Exception { // Check that correct devices receive telemetry - testTelemetryIsDelivered(accessTokenDevice, getMqttAsyncClient(null, USER_NAME1, null)); - testTelemetryIsDelivered(clientIdDevice, getMqttAsyncClient(CLIENT_ID, null, null)); - testTelemetryIsDelivered(clientIdAndUserNameDevice1, getMqttAsyncClient(CLIENT_ID, USER_NAME1, null)); - testTelemetryIsDelivered(clientIdAndUserNameAndPasswordDevice2, getMqttAsyncClient(CLIENT_ID, USER_NAME2, PASSWORD)); + MqttTestClient mqttTestClient1 = new MqttTestClient(); + mqttTestClient1.connectAndWait(USER_NAME1); + + MqttTestClient mqttTestClient2 = new MqttTestClient(CLIENT_ID); + mqttTestClient2.connectAndWait(); + + MqttTestClient mqttTestClient3 = new MqttTestClient(CLIENT_ID); + mqttTestClient3.connectAndWait(USER_NAME1); + + MqttTestClient mqttTestClient4 = new MqttTestClient(CLIENT_ID); + mqttTestClient4.connectAndWait(USER_NAME2, PASSWORD); + + // Also correct. Random clientId and password, but matches access token + MqttTestClient mqttTestClient5 = new MqttTestClient(RandomStringUtils.randomAlphanumeric(10)); + mqttTestClient5.connectAndWait(USER_NAME2, RandomStringUtils.randomAlphanumeric(10)); + + testTelemetryIsDelivered(accessTokenDevice, mqttTestClient1); + testTelemetryIsDelivered(clientIdDevice, mqttTestClient2); + testTelemetryIsDelivered(clientIdAndUserNameDevice1, mqttTestClient3); + testTelemetryIsDelivered(clientIdAndUserNameAndPasswordDevice2, mqttTestClient4); // Also correct. Random clientId and password, but matches access token - testTelemetryIsDelivered(accessToken2Device, getMqttAsyncClient(RandomStringUtils.randomAlphanumeric(10), USER_NAME2, RandomStringUtils.randomAlphanumeric(10))); + testTelemetryIsDelivered(accessToken2Device, mqttTestClient5); } @Test(expected = MqttSecurityException.class) public void testCorrectClientIdAndUserNameButWrongPassword() throws Exception { // Not correct. Correct clientId and username, but wrong password - testTelemetryIsNotDelivered(clientIdAndUserNameAndPasswordDevice3, getMqttAsyncClient(CLIENT_ID, USER_NAME3, "WRONG PASSWORD")); + MqttTestClient mqttTestClient = new MqttTestClient(CLIENT_ID); + mqttTestClient.connectAndWait(USER_NAME3, "WRONG PASSWORD"); + testTelemetryIsNotDelivered(clientIdAndUserNameAndPasswordDevice3, mqttTestClient); } - private void testTelemetryIsDelivered(Device device, MqttAsyncClient client) throws Exception { + private void testTelemetryIsDelivered(Device device, MqttTestClient client) throws Exception { testTelemetryIsDelivered(device, client, true); } - private void testTelemetryIsNotDelivered(Device device, MqttAsyncClient client) throws Exception { + private void testTelemetryIsNotDelivered(Device device, MqttTestClient client) throws Exception { testTelemetryIsDelivered(device, client, false); } - private void testTelemetryIsDelivered(Device device, MqttAsyncClient client, boolean ok) throws Exception { + private void testTelemetryIsDelivered(Device device, MqttTestClient client, boolean ok) throws Exception { String randomKey = RandomStringUtils.randomAlphanumeric(10); List expectedKeys = Arrays.asList(randomKey); - publishMqttMsg(client, JacksonUtil.toString(JacksonUtil.newObjectNode().put(randomKey, true)).getBytes(), MqttTopics.DEVICE_TELEMETRY_TOPIC); + client.publishAndWait(MqttTopics.DEVICE_TELEMETRY_TOPIC, JacksonUtil.toString(JacksonUtil.newObjectNode().put(randomKey, true)).getBytes()); String deviceId = device.getId().getId().toString(); @@ -146,7 +165,7 @@ public class BasicMqttCredentialsTest extends AbstractMqttIntegrationTest { } else { assertNull(actualKeys); } - client.disconnect().waitForCompletion(); + client.disconnect(); } protected MqttAsyncClient getMqttAsyncClient(String clientId, String username, String password) throws MqttException { From 96a9b22a67b4d3b9e86555dda1d58d536ea252ec Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Tue, 10 May 2022 15:35:04 +0300 Subject: [PATCH 118/276] Device Credentials service --- .../DeviceCredentialsCaffeineCache.java | 34 +++++++++++++++ .../device/DeviceCredentialsEvictEvent.java | 28 +++++++++++++ .../device/DeviceCredentialsRedisCache.java | 36 ++++++++++++++++ .../device/DeviceCredentialsServiceImpl.java | 41 ++++++++++++++----- .../BaseDeviceCredentialsCacheTest.java | 6 ++- 5 files changed, 133 insertions(+), 12 deletions(-) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsCaffeineCache.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsEvictEvent.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsRedisCache.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsCaffeineCache.java new file mode 100644 index 0000000000..c24be4dc04 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsCaffeineCache.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.device; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("DeviceCredentialsCache") +public class DeviceCredentialsCaffeineCache extends CaffeineTbTransactionalCache { + + public DeviceCredentialsCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.DEVICE_CREDENTIALS_CACHE); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsEvictEvent.java new file mode 100644 index 0000000000..4c59dcacbf --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsEvictEvent.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.device; + +import lombok.Data; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +class DeviceCredentialsEvictEvent { + + private final String newCedentialsId; + private final String oldCredentialsId; + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsRedisCache.java new file mode 100644 index 0000000000..580b130d5e --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsRedisCache.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.dao.device; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; +import org.thingsboard.server.dao.cache.TbRedisSerializer; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("DeviceCredentialsCache") +public class DeviceCredentialsRedisCache extends RedisTbTransactionalCache { + + public DeviceCredentialsRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.DEVICE_CREDENTIALS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>()); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java index 72ec300ab0..0c068aad6e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java @@ -21,8 +21,10 @@ import org.eclipse.leshan.core.util.SecurityUtil; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; -import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; @@ -40,7 +42,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.msg.EncryptionUtil; -import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; import org.thingsboard.server.dao.service.DataValidator; @@ -51,7 +53,7 @@ import static org.thingsboard.server.dao.service.Validator.validateString; @Service @Slf4j -public class DeviceCredentialsServiceImpl extends AbstractEntityService implements DeviceCredentialsService { +public class DeviceCredentialsServiceImpl extends AbstractCachedEntityService implements DeviceCredentialsService { @Autowired private DeviceCredentialsDao deviceCredentialsDao; @@ -59,6 +61,15 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen @Autowired private DataValidator credentialsValidator; + @TransactionalEventListener(classes = DeviceCredentialsEvictEvent.class) + @Override + public void handleEvictEvent(DeviceCredentialsEvictEvent event) { + cache.evict(event.getNewCedentialsId()); + if (StringUtils.isNotEmpty(event.getOldCredentialsId()) && !event.getNewCedentialsId().equals(event.getOldCredentialsId())) { + cache.evict(event.getOldCredentialsId()); + } + } + @Override public DeviceCredentials findDeviceCredentialsByDeviceId(TenantId tenantId, DeviceId deviceId) { log.trace("Executing findDeviceCredentialsByDeviceId [{}]", deviceId); @@ -67,19 +78,21 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen } @Override - @Cacheable(cacheNames = DEVICE_CREDENTIALS_CACHE, key = "'deviceCredentials_' + #credentialsId", unless = "#result == null") public DeviceCredentials findDeviceCredentialsByCredentialsId(String credentialsId) { log.trace("Executing findDeviceCredentialsByCredentialsId [{}]", credentialsId); validateString(credentialsId, "Incorrect credentialsId " + credentialsId); - return deviceCredentialsDao.findByCredentialsId(TenantId.SYS_TENANT_ID, credentialsId); + return cache.getAndPutInTransaction(credentialsId, + () -> deviceCredentialsDao.findByCredentialsId(TenantId.SYS_TENANT_ID, credentialsId), + false); } + @Transactional(propagation = Propagation.SUPPORTS) @Override - @CacheEvict(cacheNames = DEVICE_CREDENTIALS_CACHE, keyGenerator = "previousDeviceCredentialsId", beforeInvocation = true) public DeviceCredentials updateDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials) { return saveOrUpdate(tenantId, deviceCredentials); } + @Transactional(propagation = Propagation.SUPPORTS) @Override public DeviceCredentials createDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials) { return saveOrUpdate(tenantId, deviceCredentials); @@ -93,7 +106,13 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen log.trace("Executing updateDeviceCredentials [{}]", deviceCredentials); credentialsValidator.validate(deviceCredentials, id -> tenantId); try { - return deviceCredentialsDao.saveAndFlush(tenantId, deviceCredentials); + DeviceCredentials oldDeviceCredentials = null; + if (deviceCredentials.getDeviceId() != null) { + oldDeviceCredentials = deviceCredentialsDao.findByDeviceId(tenantId, deviceCredentials.getDeviceId().getId()); + } + var value = deviceCredentialsDao.saveAndFlush(tenantId, deviceCredentials); + publishEvictEvent(new DeviceCredentialsEvictEvent(value.getCredentialsId(), oldDeviceCredentials != null ? oldDeviceCredentials.getCredentialsId() : null)); + return value; } catch (Exception t) { ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); if (e != null && e.getConstraintName() != null @@ -182,8 +201,7 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen deviceCredentials.setCredentialsValue(JacksonUtil.toString(lwM2MCredentials)); X509ClientCredential x509ClientConfig = (X509ClientCredential) clientCredentials; if ((StringUtils.isNotBlank(x509ClientConfig.getCert()))) { - String sha3Hash = EncryptionUtil.getSha3Hash(x509ClientConfig.getCert()); - credentialsId = sha3Hash; + credentialsId = EncryptionUtil.getSha3Hash(x509ClientConfig.getCert()); } else { credentialsId = x509ClientConfig.getEndpoint(); } @@ -251,7 +269,7 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen throw new DeviceCredentialsValidationException("LwM2M client PSK key must be random sequence in hex encoding!"); } - if (pskKey.length()% 32 != 0 || pskKey.length() > 128) { + if (pskKey.length() % 32 != 0 || pskKey.length() > 128) { throw new DeviceCredentialsValidationException("LwM2M client PSK key length = " + pskKey.length() + ". Key must be HexDec format: 32, 64, 128 characters!"); } @@ -366,11 +384,12 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen } } + @Transactional(propagation = Propagation.SUPPORTS) @Override - @CacheEvict(cacheNames = DEVICE_CREDENTIALS_CACHE, key = "'deviceCredentials_' + #deviceCredentials.credentialsId") public void deleteDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials) { log.trace("Executing deleteDeviceCredentials [{}]", deviceCredentials); deviceCredentialsDao.removeById(tenantId, deviceCredentials.getUuidId()); + publishEvictEvent(new DeviceCredentialsEvictEvent(deviceCredentials.getCredentialsId(), null)); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java index 689752f0fe..91becb8c0f 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.dao.device.DeviceCredentialsDao; import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceDao; import org.thingsboard.server.dao.device.DeviceService; import java.util.UUID; @@ -120,7 +121,10 @@ public abstract class BaseDeviceCredentialsCacheTest extends AbstractServiceTest when(deviceCredentialsDao.findById(SYSTEM_TENANT_ID, deviceCredentialsId)).thenReturn(createDummyDeviceCredentialsEntity(CREDENTIALS_ID_1)); when(deviceService.findDeviceById(SYSTEM_TENANT_ID, new DeviceId(deviceId))).thenReturn(new Device()); - deviceCredentialsService.updateDeviceCredentials(SYSTEM_TENANT_ID, createDummyDeviceCredentials(deviceCredentialsId, CREDENTIALS_ID_2, deviceId)); + var dummy = createDummyDeviceCredentials(deviceCredentialsId, CREDENTIALS_ID_2, deviceId); + when(deviceCredentialsDao.saveAndFlush(SYSTEM_TENANT_ID, dummy)).thenReturn(dummy); + + deviceCredentialsService.updateDeviceCredentials(SYSTEM_TENANT_ID, dummy); when(deviceCredentialsDao.findByCredentialsId(SYSTEM_TENANT_ID, CREDENTIALS_ID_1)).thenReturn(null); From 274cfa986da162c4cd322b8b3e70a4aa645ad5ba Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Tue, 10 May 2022 16:14:41 +0300 Subject: [PATCH 119/276] Session Cache --- .../device/DeviceActorMessageProcessor.java | 8 +-- .../DefaultDeviceSessionCacheService.java | 17 ++++-- .../session/DeviceSessionCacheService.java | 4 +- .../service/session/SessionCaffeineCache.java | 37 +++++++++++++ .../service/session/SessionRedisCache.java | 55 +++++++++++++++++++ .../server/cache/TbTransactionalCache.java | 2 + .../cache/CaffeineTbTransactionalCache.java | 11 ++++ .../dao/cache/RedisTbCacheTransaction.java | 3 +- .../dao/cache/RedisTbTransactionalCache.java | 14 +++-- 9 files changed, 135 insertions(+), 16 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/session/SessionCaffeineCache.java create mode 100644 application/src/main/java/org/thingsboard/server/service/session/SessionRedisCache.java diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index 0b28aca7b0..b0830317a7 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -884,10 +884,10 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { return; } log.debug("[{}] Restoring sessions from cache", deviceId); - DeviceSessionsCacheEntry sessionsDump = null; + DeviceSessionsCacheEntry sessionsDump; try { - sessionsDump = DeviceSessionsCacheEntry.parseFrom(systemContext.getDeviceSessionCacheService().get(deviceId)); - } catch (InvalidProtocolBufferException e) { + sessionsDump = systemContext.getDeviceSessionCacheService().get(deviceId); + } catch (Exception e) { log.warn("[{}] Failed to decode device sessions from cache", deviceId); return; } @@ -942,7 +942,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { }); systemContext.getDeviceSessionCacheService() .put(deviceId, DeviceSessionsCacheEntry.newBuilder() - .addAllSessions(sessionsList).build().toByteArray()); + .addAllSessions(sessionsList).build()); } void init(TbActorCtx ctx) { diff --git a/application/src/main/java/org/thingsboard/server/service/session/DefaultDeviceSessionCacheService.java b/application/src/main/java/org/thingsboard/server/service/session/DefaultDeviceSessionCacheService.java index bf8af38e5f..9e5667aa19 100644 --- a/application/src/main/java/org/thingsboard/server/service/session/DefaultDeviceSessionCacheService.java +++ b/application/src/main/java/org/thingsboard/server/service/session/DefaultDeviceSessionCacheService.java @@ -15,14 +15,18 @@ */ package org.thingsboard.server.service.session; +import com.google.protobuf.InvalidProtocolBufferException; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.gen.transport.TransportProtos.DeviceSessionsCacheEntry; import org.thingsboard.server.queue.util.TbCoreComponent; +import java.io.Serializable; import java.util.Collections; import static org.thingsboard.server.common.data.CacheConstants.SESSIONS_CACHE; @@ -35,17 +39,20 @@ import static org.thingsboard.server.common.data.CacheConstants.SESSIONS_CACHE; @Slf4j public class DefaultDeviceSessionCacheService implements DeviceSessionCacheService { + @Autowired + protected TbTransactionalCache cache; + @Override - @Cacheable(cacheNames = SESSIONS_CACHE, key = "#deviceId.toString()") - public byte[] get(DeviceId deviceId) { + public DeviceSessionsCacheEntry get(DeviceId deviceId) { log.debug("[{}] Fetching session data from cache", deviceId); - return DeviceSessionsCacheEntry.newBuilder().addAllSessions(Collections.emptyList()).build().toByteArray(); + return cache.getAndPutInTransaction(deviceId, () -> + DeviceSessionsCacheEntry.newBuilder().addAllSessions(Collections.emptyList()).build(), false); } @Override - @CachePut(cacheNames = SESSIONS_CACHE, key = "#deviceId.toString()") - public byte[] put(DeviceId deviceId, byte[] sessions) { + public DeviceSessionsCacheEntry put(DeviceId deviceId, DeviceSessionsCacheEntry sessions) { log.debug("[{}] Pushing session data to cache: {}", deviceId, sessions); + cache.putIfAbsent(deviceId, sessions); return sessions; } } diff --git a/application/src/main/java/org/thingsboard/server/service/session/DeviceSessionCacheService.java b/application/src/main/java/org/thingsboard/server/service/session/DeviceSessionCacheService.java index 158666048c..b01f94306a 100644 --- a/application/src/main/java/org/thingsboard/server/service/session/DeviceSessionCacheService.java +++ b/application/src/main/java/org/thingsboard/server/service/session/DeviceSessionCacheService.java @@ -23,8 +23,8 @@ import org.thingsboard.server.gen.transport.TransportProtos.DeviceSessionsCacheE */ public interface DeviceSessionCacheService { - byte[] get(DeviceId deviceId); + DeviceSessionsCacheEntry get(DeviceId deviceId); - byte[] put(DeviceId deviceId, byte[] sessions); + DeviceSessionsCacheEntry put(DeviceId deviceId, DeviceSessionsCacheEntry sessions); } diff --git a/application/src/main/java/org/thingsboard/server/service/session/SessionCaffeineCache.java b/application/src/main/java/org/thingsboard/server/service/session/SessionCaffeineCache.java new file mode 100644 index 0000000000..f8177c22db --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/session/SessionCaffeineCache.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.session; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.DeviceInfo; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.dao.asset.AssetCacheKey; +import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.gen.transport.TransportProtos; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("SessionCache") +public class SessionCaffeineCache extends CaffeineTbTransactionalCache { + + public SessionCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.SESSIONS_CACHE); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/session/SessionRedisCache.java b/application/src/main/java/org/thingsboard/server/service/session/SessionRedisCache.java new file mode 100644 index 0000000000..210410fe8e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/session/SessionRedisCache.java @@ -0,0 +1,55 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.session; + +import com.google.protobuf.InvalidProtocolBufferException; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.dao.asset.AssetCacheKey; +import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; +import org.thingsboard.server.dao.cache.TbRedisSerializer; +import org.thingsboard.server.gen.transport.TransportProtos; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("SessionCache") +public class SessionRedisCache extends RedisTbTransactionalCache { + + public SessionRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.ASSET_CACHE, cacheSpecsMap, connectionFactory, configuration, new RedisSerializer<>() { + @Override + public byte[] serialize(TransportProtos.DeviceSessionsCacheEntry deviceSessionsCacheEntry) throws SerializationException { + return deviceSessionsCacheEntry.toByteArray(); + } + + @Override + public TransportProtos.DeviceSessionsCacheEntry deserialize(byte[] bytes) throws SerializationException { + try { + return TransportProtos.DeviceSessionsCacheEntry.parseFrom(bytes); + } catch (InvalidProtocolBufferException e) { + throw new RuntimeException("Failed to deserialize session cache entry"); + } + } + }); + } +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java index cc43f0c537..92ab0489c7 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java @@ -27,6 +27,8 @@ public interface TbTransactionalCache get(K key); + void put(K key, V value); + void putIfAbsent(K key, V value); void evict(K key); diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbTransactionalCache.java b/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbTransactionalCache.java index 59e5b52f28..d9fb5cd774 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbTransactionalCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbTransactionalCache.java @@ -50,6 +50,17 @@ public abstract class CaffeineTbTransactionalCache Date: Tue, 10 May 2022 16:18:00 +0300 Subject: [PATCH 120/276] updated MqttTestCallback license header --- .../server/transport/mqtt/MqttTestCallback.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestCallback.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestCallback.java index a17d3bd2a4..49533aaf37 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestCallback.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestCallback.java @@ -1,3 +1,18 @@ +/** + * Copyright © 2016-2022 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. + */ package org.thingsboard.server.transport.mqtt; import lombok.Data; From a1596ac6479557a0a3cc7658365af3fb061ec1b9 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Tue, 10 May 2022 16:58:46 +0300 Subject: [PATCH 121/276] Clean all the cache during update to 3.4 --- .../service/install/update/DefaultCacheCleanupService.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java index c41eebdbb6..7a5bc08236 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java @@ -68,6 +68,10 @@ public class DefaultCacheCleanupService implements CacheCleanupService { log.info("Clear cache to upgrade from version 3.3.3 to 3.3.4 ..."); clearAll(); break; + case "3.3.4": + log.info("Clear cache to upgrade from version 3.3.4 to 3.4.0 ..."); + clearAll(); + break; default: //Do nothing, since cache cleanup is optional. } From d5ef34cfb826ece55cc903ee6de30fc3a353d0bf Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 10 May 2022 19:44:58 +0200 Subject: [PATCH 122/276] added additional info to Queue --- .../thingsboard/server/common/data/queue/Queue.java | 9 +++++++-- .../thingsboard/server/dao/model/ModelConstants.java | 2 ++ .../thingsboard/server/dao/model/sql/QueueEntity.java | 10 ++++++++-- dao/src/main/resources/sql/schema-entities.sql | 3 ++- ui-ngx/src/app/shared/models/queue.models.ts | 1 + 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java index 90caa9c03f..f65609beae 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java @@ -16,15 +16,15 @@ package org.thingsboard.server.common.data.queue; import lombok.Data; -import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; @Data -public class Queue extends BaseData implements HasName, HasTenantId { +public class Queue extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId { private TenantId tenantId; private String name; private String topic; @@ -53,4 +53,9 @@ public class Queue extends BaseData implements HasName, HasTenantId { this.submitStrategy = queueConfiguration.getSubmitStrategy(); this.processingStrategy = queueConfiguration.getProcessingStrategy(); } + + @Override + public String getSearchText() { + return getName(); + } } \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index d122f712ca..8bdbed8e5c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -595,6 +595,8 @@ public class ModelConstants { public static final String QUEUE_SUBMIT_STRATEGY_PROPERTY = "submit_strategy"; public static final String QUEUE_PROCESSING_STRATEGY_PROPERTY = "processing_strategy"; public static final String QUEUE_COLUMN_FAMILY_NAME = "queue"; + public static final String QUEUE_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; + protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN}; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/QueueEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/QueueEntity.java index fefe686c9e..ca7ab13c9b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/QueueEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/QueueEntity.java @@ -73,6 +73,10 @@ public class QueueEntity extends BaseSqlEntity { @Column(name = ModelConstants.QUEUE_PROCESSING_STRATEGY_PROPERTY) private JsonNode processingStrategy; + @Type(type = "json") + @Column(name = ModelConstants.QUEUE_ADDITIONAL_INFO_PROPERTY) + private JsonNode additionalInfo; + public QueueEntity() { } @@ -90,6 +94,7 @@ public class QueueEntity extends BaseSqlEntity { this.packProcessingTimeout = queue.getPackProcessingTimeout(); this.submitStrategy = mapper.valueToTree(queue.getSubmitStrategy()); this.processingStrategy = mapper.valueToTree(queue.getProcessingStrategy()); + this.additionalInfo = queue.getAdditionalInfo(); } @Override @@ -103,8 +108,9 @@ public class QueueEntity extends BaseSqlEntity { queue.setPartitions(partitions); queue.setConsumerPerPartition(consumerPerPartition); queue.setPackProcessingTimeout(packProcessingTimeout); - queue.setSubmitStrategy(mapper.convertValue(this.submitStrategy, SubmitStrategy.class)); - queue.setProcessingStrategy(mapper.convertValue(this.processingStrategy, ProcessingStrategy.class)); + queue.setSubmitStrategy(mapper.convertValue(submitStrategy, SubmitStrategy.class)); + queue.setProcessingStrategy(mapper.convertValue(processingStrategy, ProcessingStrategy.class)); + queue.setAdditionalInfo(additionalInfo); return queue; } } \ No newline at end of file diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index d71e2fb4dd..2affd12547 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -226,7 +226,8 @@ CREATE TABLE IF NOT EXISTS queue( consumer_per_partition boolean, pack_processing_timeout bigint, submit_strategy varchar(255), - processing_strategy varchar(255) + processing_strategy varchar(255), + additional_info varchar ); CREATE TABLE IF NOT EXISTS device_profile ( diff --git a/ui-ngx/src/app/shared/models/queue.models.ts b/ui-ngx/src/app/shared/models/queue.models.ts index 3224d7dc23..2b4891171b 100644 --- a/ui-ngx/src/app/shared/models/queue.models.ts +++ b/ui-ngx/src/app/shared/models/queue.models.ts @@ -61,4 +61,5 @@ export interface QueueInfo extends BaseData { }; tenantId?: TenantId; topic: string; + additionalInfo?: any; } From 94155d55fd0d456e5b9adb5e2efa5b642cde20da Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Wed, 11 May 2022 10:23:12 +0300 Subject: [PATCH 123/276] Organize imports --- .../service/session/SessionCaffeineCache.java | 3 --- .../server/cache/TBRedisCacheConfiguration.java | 2 -- .../cache/TbCaffeineCacheConfiguration.java | 1 - .../java/org/thingsboard/server/dao/DaoUtil.java | 1 - .../thingsboard/server/dao/asset/AssetDao.java | 1 - .../server/dao/asset/AssetRedisCache.java | 4 ---- .../dao/attributes/CachedAttributesService.java | 2 +- .../cache/CaffeineCacheTransactionStorage.java | 9 --------- .../server/dao/cache/TbRedisSerializer.java | 1 - .../device/DeviceCredentialsCaffeineCache.java | 1 - .../dao/device/DeviceCredentialsEvictEvent.java | 2 -- .../dao/device/DeviceCredentialsRedisCache.java | 1 - .../dao/device/DeviceCredentialsServiceImpl.java | 2 -- .../dao/device/DeviceProfileCaffeineCache.java | 1 - .../dao/device/DeviceProfileRedisCache.java | 1 - .../server/dao/device/DeviceRedisCache.java | 2 -- .../server/dao/edge/EdgeCaffeineCache.java | 2 -- .../server/dao/edge/EdgeRedisCache.java | 4 ---- .../server/dao/edge/EdgeServiceImpl.java | 2 -- .../server/dao/entity/AbstractEntityService.java | 3 --- .../dao/entityview/EntityViewCaffeineCache.java | 2 -- .../dao/entityview/EntityViewRedisCache.java | 4 ---- .../thingsboard/server/dao/event/EventDao.java | 2 -- .../server/dao/model/sql/AbstractEdgeEntity.java | 1 - .../dao/model/sql/AbstractEntityViewEntity.java | 1 - .../dao/model/sql/AbstractTenantEntity.java | 3 --- .../server/dao/model/sql/ApiUsageStateEntity.java | 2 +- .../OAuth2ClientRegistrationTemplateEntity.java | 15 ++++++++++----- .../server/dao/model/sql/OtaPackageEntity.java | 4 ++-- .../dao/model/sql/OtaPackageInfoEntity.java | 5 ++--- .../server/dao/model/sql/RuleNodeStateEntity.java | 2 -- .../server/dao/model/sql/TenantProfileEntity.java | 2 +- .../server/dao/model/sql/WidgetTypeEntity.java | 1 - .../server/dao/oauth2/OAuth2Configuration.java | 1 - .../server/dao/oauth2/OAuth2Utils.java | 14 ++++++++++++-- .../server/dao/ota/OtaPackageCacheEvictEvent.java | 1 - .../server/dao/ota/OtaPackageCaffeineCache.java | 2 -- .../thingsboard/server/dao/ota/OtaPackageDao.java | 1 - .../server/dao/ota/OtaPackageInfoDao.java | 2 +- .../server/dao/ota/OtaPackageRedisCache.java | 2 -- .../server/dao/relation/RelationCacheValue.java | 2 -- .../dao/relation/RelationCaffeineCache.java | 2 -- .../server/dao/relation/RelationRedisCache.java | 4 ---- .../thingsboard/server/dao/rule/RuleChainDao.java | 2 -- .../server/dao/rule/RuleNodeStateDao.java | 3 --- .../server/dao/sql/TbSqlBlockingQueueParams.java | 2 -- .../attributes/AttributeKvInsertRepository.java | 1 - .../dao/sql/device/DeviceProfileRepository.java | 1 - .../server/dao/sql/ota/JpaOtaPackageDao.java | 2 +- .../server/dao/sql/ota/JpaOtaPackageInfoDao.java | 4 ++-- .../server/dao/sql/query/AlarmDataAdapter.java | 1 - .../dao/sql/query/AlarmQueryRepository.java | 2 -- .../sql/query/DefaultAlarmQueryRepository.java | 1 - .../server/dao/sql/query/EntityDataAdapter.java | 2 -- .../dao/sql/query/QuerySecurityContext.java | 8 -------- .../AbstractChunkedAggregationTimeseriesDao.java | 7 +++++-- .../sqlts/timescale/TimescaleTimeseriesDao.java | 12 ++++++------ .../dao/tenant/TenantProfileCaffeineCache.java | 2 -- .../dao/tenant/TenantProfileRedisCache.java | 4 ---- .../dao/timeseries/BaseTimeseriesService.java | 3 +-- .../CassandraBaseTimeseriesLatestDao.java | 5 +---- .../dao/timeseries/TimeseriesLatestDao.java | 2 +- .../dao/usagerecord/ApiUsageStateServiceImpl.java | 1 - .../server/dao/user/UserServiceImpl.java | 1 - .../service/BaseDeviceCredentialsCacheTest.java | 1 - 65 files changed, 47 insertions(+), 140 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/session/SessionCaffeineCache.java b/application/src/main/java/org/thingsboard/server/service/session/SessionCaffeineCache.java index f8177c22db..97bfc95f01 100644 --- a/application/src/main/java/org/thingsboard/server/service/session/SessionCaffeineCache.java +++ b/application/src/main/java/org/thingsboard/server/service/session/SessionCaffeineCache.java @@ -19,10 +19,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.DeviceInfo; -import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.dao.asset.AssetCacheKey; import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; import org.thingsboard.server.gen.transport.TransportProtos; diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java index 2909839011..9c7071868b 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java @@ -16,8 +16,6 @@ package org.thingsboard.server.cache; import lombok.Data; -import lombok.Getter; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.CacheManager; diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java index 5dee54a699..664eaa1fb4 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java @@ -20,7 +20,6 @@ import com.github.benmanes.caffeine.cache.RemovalCause; import com.github.benmanes.caffeine.cache.Ticker; import com.github.benmanes.caffeine.cache.Weigher; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; diff --git a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java index 18444bb5f3..4f250c47c8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java @@ -18,7 +18,6 @@ package org.thingsboard.server.dao; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; - import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java index e7533cc736..0432e69bca 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java @@ -22,7 +22,6 @@ import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.TenantEntityDao; diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetRedisCache.java index 996756c7eb..d343ec1d35 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetRedisCache.java @@ -17,17 +17,13 @@ package org.thingsboard.server.dao.asset; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.serializer.RedisSerializer; -import org.springframework.data.redis.serializer.SerializationException; import org.springframework.stereotype.Service; import org.thingsboard.server.cache.CacheSpecsMap; import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; import org.thingsboard.server.dao.cache.TbRedisSerializer; -import org.thingsboard.server.dao.device.DeviceCacheKey; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("AssetCache") diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index edc259fd13..3ce56dfbf3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -25,6 +25,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; import org.thingsboard.server.cache.TbCacheValueWrapper; +import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -34,7 +35,6 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.stats.DefaultCounter; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.cache.CacheExecutorService; -import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.dao.service.Validator; import javax.annotation.PostConstruct; diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineCacheTransactionStorage.java b/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineCacheTransactionStorage.java index af3c40b333..1b1cee537a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineCacheTransactionStorage.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineCacheTransactionStorage.java @@ -16,17 +16,8 @@ package org.thingsboard.server.dao.cache; import lombok.RequiredArgsConstructor; -import org.thingsboard.server.cache.TbCacheTransaction; import java.io.Serializable; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; @RequiredArgsConstructor class CaffeineCacheTransactionStorage { diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/TbRedisSerializer.java b/dao/src/main/java/org/thingsboard/server/dao/cache/TbRedisSerializer.java index cf0fac914a..6ffb50c941 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/TbRedisSerializer.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cache/TbRedisSerializer.java @@ -17,7 +17,6 @@ package org.thingsboard.server.dao.cache; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.SerializationException; -import org.thingsboard.server.common.data.edge.Edge; public class TbRedisSerializer implements RedisSerializer { diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsCaffeineCache.java index c24be4dc04..8744cdf924 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsCaffeineCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsCaffeineCache.java @@ -19,7 +19,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsEvictEvent.java index 4c59dcacbf..ddf1061b04 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsEvictEvent.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsEvictEvent.java @@ -16,8 +16,6 @@ package org.thingsboard.server.dao.device; import lombok.Data; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.TenantId; @Data class DeviceCredentialsEvictEvent { diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsRedisCache.java index 580b130d5e..4e2f4b9af5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsRedisCache.java @@ -21,7 +21,6 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.cache.CacheSpecsMap; import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; import org.thingsboard.server.dao.cache.TbRedisSerializer; diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java index 0c068aad6e..4adb9d3ac2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java @@ -20,7 +20,6 @@ import org.eclipse.leshan.core.SecurityMode; import org.eclipse.leshan.core.util.SecurityUtil; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cache.annotation.CacheEvict; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -47,7 +46,6 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; import org.thingsboard.server.dao.service.DataValidator; -import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CREDENTIALS_CACHE; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateString; diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCaffeineCache.java index 03c5e43f94..b6d2448487 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCaffeineCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCaffeineCache.java @@ -19,7 +19,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileRedisCache.java index 8d0c4eded0..5a046a3ba7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileRedisCache.java @@ -21,7 +21,6 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.cache.CacheSpecsMap; import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; import org.thingsboard.server.dao.cache.TbRedisSerializer; diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceRedisCache.java index 72a7273ed5..e79b6d52e5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceRedisCache.java @@ -17,8 +17,6 @@ package org.thingsboard.server.dao.device; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.serializer.RedisSerializer; -import org.springframework.data.redis.serializer.SerializationException; import org.springframework.stereotype.Service; import org.thingsboard.server.cache.CacheSpecsMap; import org.thingsboard.server.cache.TBRedisCacheConfiguration; diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCaffeineCache.java index c4cff90b7f..dd8677218c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCaffeineCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCaffeineCache.java @@ -19,9 +19,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.dao.asset.AssetCacheKey; import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeRedisCache.java index 9a18f9d855..dc444f693e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeRedisCache.java @@ -17,15 +17,11 @@ package org.thingsboard.server.dao.edge; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.serializer.RedisSerializer; -import org.springframework.data.redis.serializer.SerializationException; import org.springframework.stereotype.Service; import org.thingsboard.server.cache.CacheSpecsMap; import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.dao.asset.AssetCacheKey; import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; import org.thingsboard.server.dao.cache.TbRedisSerializer; diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index 414b33d3c2..71499b1df5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -25,7 +25,6 @@ import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -67,7 +66,6 @@ import java.util.List; import java.util.Optional; import java.util.stream.Collectors; -import static org.thingsboard.server.common.data.CacheConstants.EDGE_CACHE; import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateIds; diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java index 94be99af12..9321fd4e9e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java @@ -18,9 +18,7 @@ package org.thingsboard.server.dao.entity; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Lazy; -import org.springframework.transaction.support.TransactionSynchronizationManager; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; @@ -31,7 +29,6 @@ import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.dao.relation.EntityRelationEvent; import org.thingsboard.server.dao.relation.RelationService; import java.util.List; diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCaffeineCache.java index dd05d7078a..7a8176e0f3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCaffeineCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCaffeineCache.java @@ -19,8 +19,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.dao.asset.AssetCacheKey; import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewRedisCache.java index 9fcc8383f9..77beb90211 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewRedisCache.java @@ -17,14 +17,10 @@ package org.thingsboard.server.dao.entityview; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.serializer.RedisSerializer; -import org.springframework.data.redis.serializer.SerializationException; import org.springframework.stereotype.Service; import org.thingsboard.server.cache.CacheSpecsMap; import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.dao.asset.AssetCacheKey; import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; import org.thingsboard.server.dao.cache.TbRedisSerializer; diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java b/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java index f3574fb419..86eb0d91ec 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java @@ -19,13 +19,11 @@ import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Event; import org.thingsboard.server.common.data.event.EventFilter; import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.Dao; import java.util.List; -import java.util.Optional; import java.util.UUID; /** diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEdgeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEdgeEntity.java index 40ef7d82ab..6709fef21f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEdgeEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEdgeEntity.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.model.sql; -import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEntityViewEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEntityViewEntity.java index 42b3f1d11a..a8a31cd892 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEntityViewEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEntityViewEntity.java @@ -25,7 +25,6 @@ import org.hibernate.annotations.TypeDef; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTenantEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTenantEntity.java index c3a0e8396a..2ddb065ea3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTenantEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTenantEntity.java @@ -21,7 +21,6 @@ import lombok.EqualsAndHashCode; import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.dao.model.BaseSqlEntity; @@ -30,9 +29,7 @@ import org.thingsboard.server.dao.model.SearchTextEntity; import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Column; -import javax.persistence.Entity; import javax.persistence.MappedSuperclass; -import javax.persistence.Table; import java.util.UUID; @Data diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java index 64956bf871..42b2777ca4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java @@ -20,9 +20,9 @@ import lombok.EqualsAndHashCode; import org.hibernate.annotations.TypeDef; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.ApiUsageStateValue; +import org.thingsboard.server.common.data.id.ApiUsageStateId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.ApiUsageStateId; import org.thingsboard.server.dao.model.BaseEntity; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationTemplateEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationTemplateEntity.java index 0a7e1fac33..40344025ba 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationTemplateEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationTemplateEntity.java @@ -21,16 +21,21 @@ import lombok.EqualsAndHashCode; import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.oauth2.*; +import org.thingsboard.server.common.data.oauth2.MapperType; +import org.thingsboard.server.common.data.oauth2.OAuth2BasicMapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; +import org.thingsboard.server.common.data.oauth2.TenantNameStrategyType; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.util.mapping.JsonStringType; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; -import javax.persistence.*; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Table; import java.util.Arrays; -import java.util.UUID; @Data @EqualsAndHashCode(callSuper = true) diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java index 9d61cf52f1..99abac10fd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java @@ -21,11 +21,11 @@ import lombok.EqualsAndHashCode; import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; import org.thingsboard.server.common.data.OtaPackage; -import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; -import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; +import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.SearchTextEntity; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java index d5f34c922e..286c7f6605 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java @@ -22,12 +22,11 @@ import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.OtaPackageInfo; -import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; -import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; +import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.SearchTextEntity; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeStateEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeStateEntity.java index bb62ecdd4e..0ba56a73ca 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeStateEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeStateEntity.java @@ -15,10 +15,8 @@ */ package org.thingsboard.server.dao.model.sql; -import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import lombok.EqualsAndHashCode; -import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.RuleNodeId; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java index 8613c7ced5..6f7b094464 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java @@ -21,13 +21,13 @@ import lombok.Data; import lombok.EqualsAndHashCode; import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.SearchTextEntity; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.dao.util.mapping.JsonBinaryType; import javax.persistence.Column; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeEntity.java index 956b8d7cd8..d03d8d8541 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeEntity.java @@ -28,7 +28,6 @@ import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; -import java.util.UUID; @Data @EqualsAndHashCode(callSuper = true) diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Configuration.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Configuration.java index 605a11d6ce..f557e9c253 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Configuration.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Configuration.java @@ -16,7 +16,6 @@ package org.thingsboard.server.dao.oauth2; import lombok.Data; -import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java index 1f3f24856c..a00f94d7e2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java @@ -18,9 +18,19 @@ package org.thingsboard.server.dao.oauth2; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.id.OAuth2ParamsId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.oauth2.*; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2Domain; +import org.thingsboard.server.common.data.oauth2.OAuth2DomainInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2Info; +import org.thingsboard.server.common.data.oauth2.OAuth2Mobile; +import org.thingsboard.server.common.data.oauth2.OAuth2MobileInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2Params; +import org.thingsboard.server.common.data.oauth2.OAuth2ParamsInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2Registration; +import org.thingsboard.server.common.data.oauth2.OAuth2RegistrationInfo; -import java.util.*; +import java.util.Comparator; +import java.util.List; import java.util.stream.Collectors; public class OAuth2Utils { diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCacheEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCacheEvictEvent.java index cea1c1afcb..c4bbe77a52 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCacheEvictEvent.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCacheEvictEvent.java @@ -18,7 +18,6 @@ package org.thingsboard.server.dao.ota; import lombok.Data; import lombok.RequiredArgsConstructor; import org.thingsboard.server.common.data.id.OtaPackageId; -import org.thingsboard.server.common.data.id.TenantId; @Data @RequiredArgsConstructor diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCaffeineCache.java index 03f2401f42..02eb967091 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCaffeineCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCaffeineCache.java @@ -20,8 +20,6 @@ import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.OtaPackageInfo; -import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.dao.asset.AssetCacheKey; import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java index 445210ca3b..a807f501bc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java @@ -18,7 +18,6 @@ package org.thingsboard.server.dao.ota; import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.Dao; -import org.thingsboard.server.dao.TenantEntityDao; import org.thingsboard.server.dao.TenantEntityWithDataDao; public interface OtaPackageDao extends Dao, TenantEntityWithDataDao { diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageInfoDao.java index 5d2c51ed07..9099b57511 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageInfoDao.java @@ -16,10 +16,10 @@ package org.thingsboard.server.dao.ota; import org.thingsboard.server.common.data.OtaPackageInfo; -import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageRedisCache.java index 7605f24f3c..35ac206c10 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageRedisCache.java @@ -22,8 +22,6 @@ import org.thingsboard.server.cache.CacheSpecsMap; import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.OtaPackageInfo; -import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.dao.asset.AssetCacheKey; import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; import org.thingsboard.server.dao.cache.TbRedisSerializer; diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheValue.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheValue.java index b2cbac681f..b596033b1c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheValue.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCacheValue.java @@ -19,9 +19,7 @@ import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntityRelation; -import org.thingsboard.server.common.data.relation.RelationTypeGroup; import java.io.Serializable; import java.util.List; diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCaffeineCache.java index b6d20331fc..c795d2498a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCaffeineCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCaffeineCache.java @@ -19,8 +19,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.dao.attributes.AttributeCacheKey; import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationRedisCache.java index e433379474..602337ef56 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationRedisCache.java @@ -17,14 +17,10 @@ package org.thingsboard.server.dao.relation; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.serializer.RedisSerializer; -import org.springframework.data.redis.serializer.SerializationException; import org.springframework.stereotype.Service; import org.thingsboard.server.cache.CacheSpecsMap; import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.dao.attributes.AttributeCacheKey; import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; import org.thingsboard.server.dao.cache.TbRedisSerializer; diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java index 542a487949..f53f8a9e9a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java @@ -19,13 +19,11 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleChain; -import org.thingsboard.server.common.data.rule.RuleChainOutputLabelsUsage; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.TenantEntityDao; import java.util.Collection; -import java.util.List; import java.util.UUID; /** diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeStateDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeStateDao.java index f761f9f7f9..9bad52463a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeStateDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeStateDao.java @@ -15,9 +15,6 @@ */ package org.thingsboard.server.dao.rule; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.RuleNodeId; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleNodeState; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java index 378ce33372..be42ee3fd9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java @@ -18,8 +18,6 @@ package org.thingsboard.server.dao.sql; import lombok.Builder; import lombok.Data; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.server.common.stats.MessagesStats; -import org.thingsboard.server.common.stats.StatsFactory; @Slf4j @Data diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java index 33513fa252..472652e357 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java @@ -28,7 +28,6 @@ import org.thingsboard.server.dao.model.sql.AttributeKvEntity; import java.sql.PreparedStatement; import java.sql.SQLException; -import java.sql.SQLType; import java.sql.Types; import java.util.ArrayList; import java.util.List; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java index 3ecc19245d..e61a60b5b6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java @@ -19,7 +19,6 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; import org.thingsboard.server.common.data.DeviceProfileInfo; import org.thingsboard.server.common.data.DeviceTransportType; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java index f6e6dbc5c4..53c1318948 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java @@ -21,8 +21,8 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.ota.OtaPackageDao; import org.thingsboard.server.dao.model.sql.OtaPackageEntity; +import org.thingsboard.server.dao.ota.OtaPackageDao; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; import java.util.UUID; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageInfoDao.java index 8ea805332e..ab8528c3ff 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageInfoDao.java @@ -20,15 +20,15 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.OtaPackageInfo; -import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.ota.OtaPackageInfoDao; import org.thingsboard.server.dao.model.sql.OtaPackageInfoEntity; +import org.thingsboard.server.dao.ota.OtaPackageInfoDao; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; import java.util.Objects; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java index 02b4c17d7e..7d660519eb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java @@ -36,7 +36,6 @@ import org.thingsboard.server.dao.model.ModelConstants; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmQueryRepository.java index a88a2dc0d6..616d9811b6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmQueryRepository.java @@ -15,12 +15,10 @@ */ package org.thingsboard.server.dao.sql.query; -import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; -import org.thingsboard.server.common.data.query.AlarmDataPageLink; import org.thingsboard.server.common.data.query.AlarmDataQuery; import java.util.Collection; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java index 1aaf590afd..a1be8884ff 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java @@ -24,7 +24,6 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; -import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityDataAdapter.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityDataAdapter.java index 2de77a0e73..49640c1640 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityDataAdapter.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityDataAdapter.java @@ -17,7 +17,6 @@ package org.thingsboard.server.dao.sql.query; import org.apache.commons.lang3.math.NumberUtils; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.page.PageData; @@ -27,7 +26,6 @@ import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.TsValue; -import java.nio.ByteBuffer; import java.util.HashMap; import java.util.List; import java.util.Map; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/QuerySecurityContext.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/QuerySecurityContext.java index d4811de363..72da81786a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/QuerySecurityContext.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/QuerySecurityContext.java @@ -17,18 +17,10 @@ package org.thingsboard.server.dao.sql.query; import lombok.AllArgsConstructor; import lombok.Getter; -import org.hibernate.type.PostgresUUIDType; -import org.springframework.jdbc.core.namedparam.SqlParameterSource; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; -import java.sql.Types; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - @AllArgsConstructor public class QuerySecurityContext { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java index 64895a7f5a..f47b658601 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java @@ -21,7 +21,6 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.thingsboard.server.common.data.id.EntityId; @@ -42,7 +41,11 @@ import org.thingsboard.server.dao.timeseries.TimeseriesDao; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; -import java.util.*; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.function.Function; import java.util.stream.Collectors; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java index c938f11a83..9c03a9e3dd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java @@ -21,7 +21,6 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Component; @@ -34,7 +33,6 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity; import org.thingsboard.server.dao.model.sqlts.timescale.ts.TimescaleTsKvEntity; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams; @@ -46,10 +44,12 @@ import org.thingsboard.server.dao.util.TimescaleDBTsDao; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; -import java.sql.CallableStatement; -import java.sql.SQLException; -import java.sql.Types; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.function.Function; diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCaffeineCache.java index b9a9426172..6d6fa531b5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCaffeineCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCaffeineCache.java @@ -20,8 +20,6 @@ import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.TenantProfile; -import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.dao.asset.AssetCacheKey; import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileRedisCache.java index e6cc0a254c..47592f83c5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileRedisCache.java @@ -17,15 +17,11 @@ package org.thingsboard.server.dao.tenant; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.serializer.RedisSerializer; -import org.springframework.data.redis.serializer.SerializationException; import org.springframework.stereotype.Service; import org.thingsboard.server.cache.CacheSpecsMap; import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.TenantProfile; -import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.dao.asset.AssetCacheKey; import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; import org.thingsboard.server.dao.cache.TbRedisSerializer; diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java index 11fab052b5..a74f51ce9e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java @@ -24,7 +24,6 @@ import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; @@ -37,8 +36,8 @@ import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; -import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.Validator; diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java index 79a3136589..124af03a1a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java @@ -20,7 +20,6 @@ import com.datastax.oss.driver.api.core.cql.BoundStatementBuilder; import com.datastax.oss.driver.api.core.cql.PreparedStatement; import com.datastax.oss.driver.api.core.cql.Statement; import com.datastax.oss.driver.api.querybuilder.QueryBuilder; -import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -34,18 +33,16 @@ import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; -import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.nosql.TbResultSet; import org.thingsboard.server.dao.sqlts.AggregationTimeseriesDao; import org.thingsboard.server.dao.util.NoSqlTsLatestDao; -import javax.annotation.Nullable; import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.concurrent.ExecutionException; import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java index c3705fbc3d..d24a229766 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java @@ -20,8 +20,8 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; -import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; import java.util.List; diff --git a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java index a1a3e7ae1f..db52629d8d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.usagerecord; -import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index ba0f09d64e..6ce166be71 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.ListenableFuture; -import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Value; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java index 91becb8c0f..fc1c976664 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java @@ -71,7 +71,6 @@ public abstract class BaseDeviceCredentialsCacheTest extends AbstractServiceTest ReflectionTestUtils.setField(unwrapDeviceCredentialsService(), "deviceCredentialsDao", deviceCredentialsDao); ReflectionTestUtils.setField(unwrapDeviceCredentialsService(), "credentialsValidator", credentialsValidator); - } @After From c96b70fcb4caf70bd571c0d46807eebaae851447 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Wed, 11 May 2022 12:15:12 +0300 Subject: [PATCH 124/276] Improve Default Device Profile creation --- .../server/dao/asset/BaseAssetService.java | 3 ++- .../device/DeviceCredentialsServiceImpl.java | 11 ++++---- .../dao/device/DeviceProfileServiceImpl.java | 27 +++++++++++-------- .../server/dao/device/DeviceServiceImpl.java | 5 ++-- .../server/dao/edge/EdgeServiceImpl.java | 1 + .../server/dao/ota/BaseOtaPackageService.java | 12 ++++++--- .../dao/tenant/TenantProfileServiceImpl.java | 7 +++-- 7 files changed, 41 insertions(+), 25 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java index 25d4029a0c..ac7ab7792f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -129,6 +129,7 @@ public class BaseAssetService extends AbstractCachedEntityService tenantId); + DeviceCredentials oldDeviceCredentials = null; + if (deviceCredentials.getDeviceId() != null) { + oldDeviceCredentials = deviceCredentialsDao.findByDeviceId(tenantId, deviceCredentials.getDeviceId().getId()); + } try { - DeviceCredentials oldDeviceCredentials = null; - if (deviceCredentials.getDeviceId() != null) { - oldDeviceCredentials = deviceCredentialsDao.findByDeviceId(tenantId, deviceCredentials.getDeviceId().getId()); - } var value = deviceCredentialsDao.saveAndFlush(tenantId, deviceCredentials); publishEvictEvent(new DeviceCredentialsEvictEvent(value.getCredentialsId(), oldDeviceCredentials != null ? oldDeviceCredentials.getCredentialsId() : null)); return value; } catch (Exception t) { + handleEvictEvent(new DeviceCredentialsEvictEvent(deviceCredentials.getCredentialsId(), oldDeviceCredentials != null ? oldDeviceCredentials.getCredentialsId() : null)); ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); if (e != null && e.getConstraintName() != null && (e.getConstraintName().equalsIgnoreCase("device_credentials_id_unq_key") || e.getConstraintName().equalsIgnoreCase("device_credentials_device_id_unq_key"))) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index be42372a78..98e5da3081 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -57,6 +57,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService keys = new ArrayList<>(2); - keys.add(DeviceProfileCacheKey.fromId(event.getDeviceProfileId())); keys.add(DeviceProfileCacheKey.fromName(event.getTenantId(), event.getNewName())); + if (event.getDeviceProfileId() != null) { + keys.add(DeviceProfileCacheKey.fromId(event.getDeviceProfileId())); + } if (event.isDefaultProfile()) { keys.add(DeviceProfileCacheKey.defaultProfile(event.getTenantId())); } @@ -117,19 +120,21 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService pageData; @@ -195,14 +200,14 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService keys = new ArrayList<>(2); - keys.add(TenantProfileCacheKey.fromId(event.getTenantProfileId())); + if (event.getTenantProfileId() != null) { + keys.add(TenantProfileCacheKey.fromId(event.getTenantProfileId())); + } if (event.isDefaultProfile()) { keys.add(TenantProfileCacheKey.defaultProfile()); } @@ -89,6 +91,7 @@ public class TenantProfileServiceImpl extends AbstractCachedEntityService Date: Wed, 11 May 2022 12:19:03 +0300 Subject: [PATCH 125/276] replaced mqtt client creation with MqttTestClient in claim tests --- .../transport/TransportSqlTestSuite.java | 18 +++++++++--------- .../mqtt/claim/MqttClaimDeviceTest.java | 12 ++++++------ .../mqtt/claim/MqttClaimProtoDeviceTest.java | 4 +++- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/TransportSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/transport/TransportSqlTestSuite.java index 3f25ded621..5720e19e56 100644 --- a/application/src/test/java/org/thingsboard/server/transport/TransportSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/transport/TransportSqlTestSuite.java @@ -20,15 +20,15 @@ import org.junit.runner.RunWith; @RunWith(ClasspathSuite.class) @ClasspathSuite.ClassnameFilters({ - "org.thingsboard.server.transport.*.rpc.*Test", - "org.thingsboard.server.transport.*.telemetry.timeseries.sql.*Test", - "org.thingsboard.server.transport.*.telemetry.attributes.*Test", - "org.thingsboard.server.transport.*.attributes.updates.*Test", - "org.thingsboard.server.transport.*.attributes.request.*Test", - "org.thingsboard.server.transport.*.claim.*Test", - "org.thingsboard.server.transport.*.provision.*Test", - "org.thingsboard.server.transport.*.credentials.*Test", - "org.thingsboard.server.transport.lwm2m.*.sql.*Test" +// "org.thingsboard.server.transport.*.rpc.*Test", +// "org.thingsboard.server.transport.*.telemetry.timeseries.sql.*Test", +// "org.thingsboard.server.transport.*.telemetry.attributes.*Test", +// "org.thingsboard.server.transport.*.attributes.updates.*Test", +// "org.thingsboard.server.transport.*.attributes.request.*Test", + "org.thingsboard.server.transport.mqtt.claim.*Test", +// "org.thingsboard.server.transport.*.provision.*Test", +// "org.thingsboard.server.transport.*.credentials.*Test", +// "org.thingsboard.server.transport.lwm2m.*.sql.*Test" }) public class TransportSqlTestSuite { diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/MqttClaimDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/MqttClaimDeviceTest.java index d9ef8c3d09..2779fb84f5 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/MqttClaimDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/MqttClaimDeviceTest.java @@ -18,13 +18,11 @@ package org.thingsboard.server.transport.mqtt.claim; import lombok.extern.slf4j.Slf4j; import org.eclipse.paho.client.mqttv3.MqttAsyncClient; import org.eclipse.paho.client.mqttv3.MqttMessage; -import org.junit.After; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.ClaimRequest; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.common.data.security.Authority; @@ -33,6 +31,7 @@ import org.thingsboard.server.dao.device.claim.ClaimResult; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.transport.TransportApiProtos; import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; +import org.thingsboard.server.transport.mqtt.MqttTestClient; import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; import static org.junit.Assert.assertEquals; @@ -99,7 +98,8 @@ public class MqttClaimDeviceTest extends AbstractMqttIntegrationTest { protected void processTestClaimingDevice(boolean emptyPayload) throws Exception { - MqttAsyncClient client = getMqttAsyncClient(accessToken); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(accessToken); byte[] payloadBytes; byte[] failurePayloadBytes; if (emptyPayload) { @@ -112,8 +112,8 @@ public class MqttClaimDeviceTest extends AbstractMqttIntegrationTest { validateClaimResponse(emptyPayload, client, payloadBytes, failurePayloadBytes); } - protected void validateClaimResponse(boolean emptyPayload, MqttAsyncClient client, byte[] payloadBytes, byte[] failurePayloadBytes) throws Exception { - client.publish(MqttTopics.DEVICE_CLAIM_TOPIC, new MqttMessage(failurePayloadBytes)); + protected void validateClaimResponse(boolean emptyPayload, MqttTestClient client, byte[] payloadBytes, byte[] failurePayloadBytes) throws Exception { + client.publishAndWait(MqttTopics.DEVICE_CLAIM_TOPIC, failurePayloadBytes); loginUser(customerAdmin.getName(), CUSTOMER_USER_PASSWORD); ClaimRequest claimRequest; @@ -131,7 +131,7 @@ public class MqttClaimDeviceTest extends AbstractMqttIntegrationTest { assertEquals(claimResponse, ClaimResponse.FAILURE); - client.publish(MqttTopics.DEVICE_CLAIM_TOPIC, new MqttMessage(payloadBytes)); + client.publishAndWait(MqttTopics.DEVICE_CLAIM_TOPIC, payloadBytes); ClaimResult claimResult = doExecuteWithRetriesAndInterval( () -> doPostClaimAsync("/api/customer/device/" + savedDevice.getName() + "/claim", claimRequest, ClaimResult.class, status().isOk()), diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/MqttClaimProtoDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/MqttClaimProtoDeviceTest.java index 3d60e1ede5..440014f8b2 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/MqttClaimProtoDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/MqttClaimProtoDeviceTest.java @@ -22,6 +22,7 @@ import org.junit.Test; import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.transport.TransportApiProtos; +import org.thingsboard.server.transport.mqtt.MqttTestClient; import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; @Slf4j @@ -60,7 +61,8 @@ public class MqttClaimProtoDeviceTest extends MqttClaimDeviceTest { } protected void processTestClaimingDevice(boolean emptyPayload) throws Exception { - MqttAsyncClient client = getMqttAsyncClient(accessToken); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(accessToken); byte[] payloadBytes; if (emptyPayload) { payloadBytes = getClaimDevice(0, emptyPayload).toByteArray(); From 69e31075e45567087363c5f59c94f5c16fbac2b0 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Wed, 11 May 2022 12:28:06 +0300 Subject: [PATCH 126/276] License headers --- .../java/org/thingsboard/server/dao/asset/BaseAssetService.java | 2 +- .../server/dao/device/DeviceCredentialsServiceImpl.java | 2 +- .../thingsboard/server/dao/device/DeviceProfileServiceImpl.java | 2 +- .../org/thingsboard/server/dao/device/DeviceServiceImpl.java | 2 +- .../org/thingsboard/server/dao/ota/BaseOtaPackageService.java | 2 +- .../thingsboard/server/dao/tenant/TenantProfileServiceImpl.java | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java index ac7ab7792f..c769adcd60 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java index 946b5322d3..6a8f972bbb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index 98e5da3081..f4a2a135b2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index df33747ed7..787c3ce0a2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java index 15e6659bf9..a1d074048b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java index 108cdfbc8a..65cbd6f3a1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java @@ -5,7 +5,7 @@ * 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 + * 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, From 7d7d48f42ebd923dadb689f4e2a64fa87acbf358 Mon Sep 17 00:00:00 2001 From: fe-dev Date: Wed, 11 May 2022 14:19:00 +0300 Subject: [PATCH 127/276] UI: delete queue-type-list and add to autocomplete description --- ui-ngx/src/app/core/http/queue.service.ts | 5 - ui-ngx/src/app/modules/common/modules-map.ts | 4 +- .../queue/queue-form.component.html | 4 + .../queue/queue-form.component.scss | 2 + .../components/queue/queue-form.component.ts | 5 +- .../queue/queue-autocomplete.component.html | 1 + .../queue/queue-autocomplete.component.ts | 5 + .../queue/queue-type-list.component.html | 49 ----- .../queue/queue-type-list.component.ts | 199 ------------------ ui-ngx/src/app/shared/models/queue.models.ts | 4 +- ui-ngx/src/app/shared/shared.module.ts | 3 - .../assets/locale/locale.constant-en_US.json | 4 +- 12 files changed, 24 insertions(+), 261 deletions(-) delete mode 100644 ui-ngx/src/app/shared/components/queue/queue-type-list.component.html delete mode 100644 ui-ngx/src/app/shared/components/queue/queue-type-list.component.ts diff --git a/ui-ngx/src/app/core/http/queue.service.ts b/ui-ngx/src/app/core/http/queue.service.ts index a5967eef97..f5dff9b4d5 100644 --- a/ui-ngx/src/app/core/http/queue.service.ts +++ b/ui-ngx/src/app/core/http/queue.service.ts @@ -31,11 +31,6 @@ export class QueueService { private http: HttpClient ) { } - public getTenantQueuesNamesByServiceType(serviceType: ServiceType, config?: RequestConfig): Observable> { - return this.http.get>(`/api/queues?serviceType=${serviceType}`, - defaultHttpOptionsFromConfig(config)); - } - public getQueueById(queueId: string, config?: RequestConfig): Observable { return this.http.get(`/api/queues/${queueId}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index a1fca3f603..8be36ec33d 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -135,7 +135,7 @@ import * as EntitySelectComponent from '@shared/components/entity/entity-select. import * as EntityKeysListComponent from '@shared/components/entity/entity-keys-list.component'; import * as EntityListSelectComponent from '@shared/components/entity/entity-list-select.component'; import * as EntityTypeListComponent from '@shared/components/entity/entity-type-list.component'; -import * as QueueTypeListComponent from '@shared/components/queue/queue-type-list.component'; +import * as QueueAutocompleteComponent from '@shared/components/queue/queue-autocomplete.component'; import * as RelationTypeAutocompleteComponent from '@shared/components/relation/relation-type-autocomplete.component'; import * as SocialSharePanelComponent from '@shared/components/socialshare-panel.component'; import * as JsonObjectEditComponent from '@shared/components/json-object-edit.component'; @@ -419,7 +419,7 @@ class ModulesMap implements IModulesMap { '@shared/components/entity/entity-keys-list.component': EntityKeysListComponent, '@shared/components/entity/entity-list-select.component': EntityListSelectComponent, '@shared/components/entity/entity-type-list.component': EntityTypeListComponent, - '@shared/components/queue/queue-type-list.component': QueueTypeListComponent, + '@shared/components/queue/queue-autocomplete.component': QueueAutocompleteComponent, '@shared/components/relation/relation-type-autocomplete.component': RelationTypeAutocompleteComponent, '@shared/components/socialshare-panel.component': SocialSharePanelComponent, '@shared/components/json-object-edit.component': JsonObjectEditComponent, diff --git a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html index 9ea9720b7b..e13faf0f43 100644 --- a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html +++ b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html @@ -168,4 +168,8 @@ + + queue.description + + diff --git a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.scss b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.scss index d20faee49c..c42c4d714d 100644 --- a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.scss +++ b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.scss @@ -15,6 +15,8 @@ */ :host ::ng-deep { .queue-strategy { + padding-bottom: 16px; + .mat-expansion-panel-body { padding-bottom: 0 !important; } diff --git a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts index c3d4b1273a..38552bff25 100644 --- a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts +++ b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts @@ -109,7 +109,10 @@ export class QueueFormComponent implements ControlValueAccessor, OnInit, Validat pauseBetweenRetries: [3, [Validators.min(1), Validators.required]], maxPauseBetweenRetries: [3, [Validators.min(1), Validators.required]], }), - topic: [''] + topic: [''], + additionalInfo: this.fb.group({ + description: [''] + }) }); this.queueFormGroup.valueChanges.subscribe(() => { this.updateModel(); diff --git a/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.html b/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.html index b991556b78..471d191b61 100644 --- a/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.html @@ -33,6 +33,7 @@ [displayWith]="displayQueueFn"> + {{getDescription(queue)}}
diff --git a/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.ts b/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.ts index 131e99d6e9..0caa487d66 100644 --- a/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.ts @@ -205,6 +205,11 @@ export class QueueAutocompleteComponent implements ControlValueAccessor, OnInit ); } + getDescription(value) { + return value.additionalInfo?.description ? value.additionalInfo.description : + `Submit Strategy: ${value.submitStrategy.type}, Processing Strategy: ${value.processingStrategy.type}`; + } + clear() { this.selectQueueFormGroup.get('queueId').patchValue('', {emitEvent: true}); setTimeout(() => { diff --git a/ui-ngx/src/app/shared/components/queue/queue-type-list.component.html b/ui-ngx/src/app/shared/components/queue/queue-type-list.component.html deleted file mode 100644 index 29de0f2f12..0000000000 --- a/ui-ngx/src/app/shared/components/queue/queue-type-list.component.html +++ /dev/null @@ -1,49 +0,0 @@ - - - {{ 'queue.queue-name' | translate }} - - - - - - - - - {{ translate.get('queue.no-queues-matching', {queue: searchText}) | async }} - - - - - {{ 'queue.name-required' | translate }} - - device-profile.select-queue-hint - diff --git a/ui-ngx/src/app/shared/components/queue/queue-type-list.component.ts b/ui-ngx/src/app/shared/components/queue/queue-type-list.component.ts deleted file mode 100644 index 28a3d94e9e..0000000000 --- a/ui-ngx/src/app/shared/components/queue/queue-type-list.component.ts +++ /dev/null @@ -1,199 +0,0 @@ -/// -/// Copyright © 2016-2022 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 { AfterViewInit, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { Observable, of } from 'rxjs'; -import { - catchError, - debounceTime, - distinctUntilChanged, - map, - publishReplay, - refCount, - switchMap, - tap -} from 'rxjs/operators'; -import { Store } from '@ngrx/store'; -import { AppState } from '@app/core/core.state'; -import { TranslateService } from '@ngx-translate/core'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; -import { QueueService } from '@core/http/queue.service'; -import { ServiceType } from '@shared/models/queue.models'; - -interface Queue { - queueName: string; -} - -@Component({ - selector: 'tb-queue-type-list', - templateUrl: './queue-type-list.component.html', - styleUrls: [], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => QueueTypeListComponent), - multi: true - }] -}) -export class QueueTypeListComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy { - - queueFormGroup: FormGroup; - - modelValue: Queue | null; - - private requiredValue: boolean; - get required(): boolean { - return this.requiredValue; - } - @Input() - set required(value: boolean) { - this.requiredValue = coerceBooleanProperty(value); - } - - @Input() - disabled: boolean; - - @Input() - queueType: ServiceType; - - @ViewChild('queueInput', {static: true}) queueInput: ElementRef; - - filteredQueues: Observable>; - - queues: Observable>; - - searchText = ''; - - private dirty = false; - - private propagateChange = (v: any) => { }; - - constructor(private store: Store, - public translate: TranslateService, - private queueService: QueueService, - private fb: FormBuilder) { - this.queueFormGroup = this.fb.group({ - queue: [null] - }); - } - - registerOnChange(fn: any): void { - this.propagateChange = fn; - } - - registerOnTouched(fn: any): void { - } - - ngOnInit() { - this.filteredQueues = this.queueFormGroup.get('queue').valueChanges - .pipe( - debounceTime(150), - distinctUntilChanged(), - tap(value => { - let modelValue; - if (typeof value === 'string' || !value) { - modelValue = null; - } else { - modelValue = value; - } - this.updateView(modelValue); - }), - map(value => value ? (typeof value === 'string' ? value : value.queueName) : ''), - switchMap(queue => this.fetchQueues(queue) ) - ); - } - - ngAfterViewInit(): void { - } - - ngOnDestroy(): void { - } - - setDisabledState(isDisabled: boolean): void { - this.disabled = isDisabled; - if (this.disabled) { - this.queueFormGroup.disable({emitEvent: false}); - } else { - this.queueFormGroup.enable({emitEvent: false}); - } - } - - writeValue(value: string | null): void { - this.searchText = ''; - this.modelValue = value ? { queueName: value } : null; - this.queueFormGroup.get('queue').patchValue(this.modelValue, {emitEvent: false}); - this.dirty = true; - } - - onFocus() { - if (this.dirty) { - this.queueFormGroup.get('queue').updateValueAndValidity({onlySelf: true, emitEvent: true}); - this.dirty = false; - } - } - - updateView(value: Queue | null) { - if (this.modelValue !== value) { - this.modelValue = value; - this.propagateChange(this.modelValue ? this.modelValue.queueName : null); - } - } - - displayQueueFn(queue?: Queue): string | undefined { - return queue ? queue.queueName : undefined; - } - - fetchQueues(searchText?: string): Observable> { - this.searchText = searchText; - return this.getQueues().pipe( - catchError(() => of([] as Array)), - map(queues => { - const result = queues.filter( queue => { - return searchText ? queue.queueName.toUpperCase().startsWith(searchText.toUpperCase()) : true; - }); - if (result.length) { - result.sort((q1, q2) => q1.queueName.localeCompare(q2.queueName)); - } - return result; - }) - ); - } - - getQueues(): Observable> { - if (!this.queues) { - this.queues = this.queueService. - getTenantQueuesNamesByServiceType(this.queueType, {ignoreLoading: true}).pipe( - map((queues) => { - return queues.map((queueName) => { - return { queueName }; - }); - }), - publishReplay(1), - refCount() - ); - } - return this.queues; - } - - clear() { - this.queueFormGroup.get('queue').patchValue(null, {emitEvent: true}); - setTimeout(() => { - this.queueInput.nativeElement.blur(); - this.queueInput.nativeElement.focus(); - }, 0); - } - -} diff --git a/ui-ngx/src/app/shared/models/queue.models.ts b/ui-ngx/src/app/shared/models/queue.models.ts index 2b4891171b..86bd99a19d 100644 --- a/ui-ngx/src/app/shared/models/queue.models.ts +++ b/ui-ngx/src/app/shared/models/queue.models.ts @@ -61,5 +61,7 @@ export interface QueueInfo extends BaseData { }; tenantId?: TenantId; topic: string; - additionalInfo?: any; + additionalInfo: { + description?: string; + }; } diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 7a2b0fb28b..6c28cf42ab 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -136,7 +136,6 @@ import { JsonObjectEditDialogComponent } from '@shared/components/dialog/json-ob import { HistorySelectorComponent } from '@shared/components/time/history-selector/history-selector.component'; import { EntityGatewaySelectComponent } from '@shared/components/entity/entity-gateway-select.component'; import { DndModule } from 'ngx-drag-drop'; -import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list.component'; import { QueueAutocompleteComponent } from '@shared/components/queue/queue-autocomplete.component'; import { ContactComponent } from '@shared/components/contact.component'; import { TimezoneSelectComponent } from '@shared/components/time/timezone-select.component'; @@ -232,7 +231,6 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) EntityKeysListComponent, EntityListSelectComponent, EntityTypeListComponent, - QueueTypeListComponent, QueueAutocompleteComponent, RelationTypeAutocompleteComponent, SocialSharePanelComponent, @@ -381,7 +379,6 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) EntityKeysListComponent, EntityListSelectComponent, EntityTypeListComponent, - QueueTypeListComponent, QueueAutocompleteComponent, RelationTypeAutocompleteComponent, SocialSharePanelComponent, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 2588a4d410..9fe5d0fbed 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2757,6 +2757,7 @@ "select-name": "Select queue name", "name": "Name", "name-required": "Queue name is required!", + "queue-required": "Queue is required!", "topic-required": "Queue topic is required!", "poll-interval-required": "Poll interval is required!", "poll-interval-min-value": "Poll interval value can't be less then 1", @@ -2801,7 +2802,8 @@ "max-pause-between-retries": "Maximal pause between retries", "delete": "Delete queue", "copyId": "Copy queue Id", - "idCopiedMessage": "Queue Id has been copied to clipboard" + "idCopiedMessage": "Queue Id has been copied to clipboard", + "description": "Description" }, "tenant": { "tenant": "Tenant", From bdf1807223017c1b817ba76585e58264c8c7c79d Mon Sep 17 00:00:00 2001 From: fe-dev Date: Wed, 11 May 2022 14:20:37 +0300 Subject: [PATCH 128/276] UI: Update rulenode core --- .../resources/public/static/rulenode/rulenode-core-config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js index 11b35b2a24..f7ead71ec3 100644 --- a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js +++ b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js @@ -1 +1 @@ -System.register(["@angular/core","@shared/public-api","@ngrx/store","@angular/forms","@angular/material/form-field","@angular/material/checkbox","@angular/flex-layout/flex","@ngx-translate/core","@angular/material/input","@angular/common","@angular/platform-browser","@angular/material/select","@angular/material/core","@angular/material/expansion","@shared/components/button/toggle-password.component","@shared/components/file-input.component","@shared/components/queue/queue-type-list.component","@core/public-api","@shared/components/js-func.component","@angular/material/button","@angular/cdk/keycodes","@angular/material/chips","@angular/material/icon","@angular/flex-layout/extended","@shared/components/entity/entity-type-select.component","@shared/components/entity/entity-select.component","@angular/cdk/coercion","@shared/components/tb-error.component","@angular/material/tooltip","rxjs/operators","@shared/components/tb-checkbox.component","@home/components/sms/sms-provider-configuration.component","@home/components/public-api","@shared/components/relation/relation-type-autocomplete.component","@shared/components/entity/entity-subtype-list.component","@home/components/relation/relation-filters.component","rxjs","@angular/material/autocomplete","@shared/pipe/highlight.pipe","@shared/components/entity/entity-autocomplete.component","@shared/components/entity/entity-type-list.component"],(function(e){"use strict";var t,o,r,a,n,l,i,s,m,u,p,d,f,c,g,x,y,b,h,C,F,L,v,I,N,T,q,k,M,S,A,G,D,V,E,P,R,w,O,H,U,K,B,j,z,_,Q,$,W,Y,J,Z,X,ee,te,oe,re,ae,ne,le,ie,se,me,ue,pe,de,fe,ce,ge,xe,ye,be,he,Ce,Fe,Le,ve,Ie;return{setters:[function(e){t=e,o=e.Component,r=e.Pipe,a=e.ViewChild,n=e.forwardRef,l=e.Input,i=e.NgModule},function(e){s=e.RuleNodeConfigurationComponent,m=e.AttributeScope,u=e.telemetryTypeTranslations,p=e.ServiceType,d=e.AlarmSeverity,f=e.alarmSeverityTranslations,c=e.EntitySearchDirection,g=e.entitySearchDirectionTranslations,x=e.EntityType,y=e.PageComponent,b=e.MessageType,h=e.messageTypeNames,C=e,F=e.SharedModule,L=e.AggregationType,v=e.aggregationTranslations,I=e.alarmStatusTranslations,N=e.AlarmStatus},function(e){T=e},function(e){q=e,k=e.Validators,M=e.NgControl,S=e.NG_VALUE_ACCESSOR,A=e.NG_VALIDATORS,G=e.FormControl},function(e){D=e},function(e){V=e},function(e){E=e},function(e){P=e},function(e){R=e},function(e){w=e,O=e.CommonModule},function(e){H=e},function(e){U=e},function(e){K=e},function(e){B=e},function(e){j=e},function(e){z=e},function(e){_=e},function(e){Q=e,$=e.isDefinedAndNotNull,W=e.isNotEmptyStr},function(e){Y=e},function(e){J=e},function(e){Z=e.ENTER,X=e.COMMA,ee=e.SEMICOLON},function(e){te=e},function(e){oe=e},function(e){re=e},function(e){ae=e},function(e){ne=e},function(e){le=e.coerceBooleanProperty},function(e){ie=e},function(e){se=e},function(e){me=e.distinctUntilChanged,ue=e.startWith,pe=e.map,de=e.mergeMap,fe=e.share},function(e){ce=e},function(e){ge=e},function(e){xe=e.HomeComponentsModule},function(e){ye=e},function(e){be=e},function(e){he=e},function(e){Ce=e.of},function(e){Fe=e},function(e){Le=e},function(e){ve=e},function(e){Ie=e}],execute:function(){class Ne extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.emptyConfigForm}onConfigurationSet(e){this.emptyConfigForm=this.fb.group({})}}e("EmptyConfigComponent",Ne),Ne.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ne,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Ne.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Ne,selector:"tb-node-empty-config",usesInheritance:!0,ngImport:t,template:"
",isInline:!0}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ne,decorators:[{type:o,args:[{selector:"tb-node-empty-config",template:"
",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Te{constructor(e){this.sanitizer=e}transform(e){return this.sanitizer.bypassSecurityTrustHtml(e)}}e("SafeHtmlPipe",Te),Te.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Te,deps:[{token:H.DomSanitizer}],target:t.ɵɵFactoryTarget.Pipe}),Te.ɵpipe=t.ɵɵngDeclarePipe({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Te,name:"safeHtml"}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Te,decorators:[{type:r,args:[{name:"safeHtml"}]}],ctorParameters:function(){return[{type:H.DomSanitizer}]}});class qe extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.assignCustomerConfigForm}onConfigurationSet(e){this.assignCustomerConfigForm=this.fb.group({customerNamePattern:[e?e.customerNamePattern:null,[k.required,k.pattern(/.*\S.*/)]],createCustomerIfNotExists:[!!e&&e.createCustomerIfNotExists,[]],customerCacheExpiration:[e?e.customerCacheExpiration:null,[k.required,k.min(0)]]})}prepareOutputConfig(e){return e.customerNamePattern=e.customerNamePattern.trim(),e}}e("AssignCustomerConfigComponent",qe),qe.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:qe,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),qe.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:qe,selector:"tb-action-node-assign-to-customer-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.customer-name-pattern\n \n \n {{ \'tb.rulenode.customer-name-pattern-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.create-customer-if-not-exists\' | translate }}\n \n \n tb.rulenode.customer-cache-expiration\n \n \n {{ \'tb.rulenode.customer-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.customer-cache-expiration-range\' | translate }}\n \n tb.rulenode.customer-cache-expiration-hint\n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:qe,decorators:[{type:o,args:[{selector:"tb-action-node-assign-to-customer-config",templateUrl:"./assign-customer-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class ke extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.attributeScopes=Object.keys(m),this.telemetryTypeTranslationsMap=u}configForm(){return this.attributesConfigForm}onConfigurationSet(e){this.attributesConfigForm=this.fb.group({scope:[e?e.scope:null,[k.required]],notifyDevice:[!e||e.notifyDevice,[]]})}}var Me;e("AttributesConfigComponent",ke),ke.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ke,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),ke.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:ke,selector:"tb-action-node-attributes-config",usesInheritance:!0,ngImport:t,template:'
\n \n attribute.attributes-scope\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n
\n \n {{ \'tb.rulenode.notify-device\' | translate }}\n \n
tb.rulenode.notify-device-hint
\n
\n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ke,decorators:[{type:o,args:[{selector:"tb-action-node-attributes-config",templateUrl:"./attributes-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}}),function(e){e.CUSTOMER="CUSTOMER",e.TENANT="TENANT",e.RELATED="RELATED",e.ALARM_ORIGINATOR="ALARM_ORIGINATOR"}(Me||(Me={}));const Se=new Map([[Me.CUSTOMER,"tb.rulenode.originator-customer"],[Me.TENANT,"tb.rulenode.originator-tenant"],[Me.RELATED,"tb.rulenode.originator-related"],[Me.ALARM_ORIGINATOR,"tb.rulenode.originator-alarm-originator"]]);var Ae;!function(e){e.CIRCLE="CIRCLE",e.POLYGON="POLYGON"}(Ae||(Ae={}));const Ge=new Map([[Ae.CIRCLE,"tb.rulenode.perimeter-circle"],[Ae.POLYGON,"tb.rulenode.perimeter-polygon"]]);var De;!function(e){e.MILLISECONDS="MILLISECONDS",e.SECONDS="SECONDS",e.MINUTES="MINUTES",e.HOURS="HOURS",e.DAYS="DAYS"}(De||(De={}));const Ve=new Map([[De.MILLISECONDS,"tb.rulenode.time-unit-milliseconds"],[De.SECONDS,"tb.rulenode.time-unit-seconds"],[De.MINUTES,"tb.rulenode.time-unit-minutes"],[De.HOURS,"tb.rulenode.time-unit-hours"],[De.DAYS,"tb.rulenode.time-unit-days"]]);var Ee;!function(e){e.METER="METER",e.KILOMETER="KILOMETER",e.FOOT="FOOT",e.MILE="MILE",e.NAUTICAL_MILE="NAUTICAL_MILE"}(Ee||(Ee={}));const Pe=new Map([[Ee.METER,"tb.rulenode.range-unit-meter"],[Ee.KILOMETER,"tb.rulenode.range-unit-kilometer"],[Ee.FOOT,"tb.rulenode.range-unit-foot"],[Ee.MILE,"tb.rulenode.range-unit-mile"],[Ee.NAUTICAL_MILE,"tb.rulenode.range-unit-nautical-mile"]]);var Re;!function(e){e.TITLE="TITLE",e.COUNTRY="COUNTRY",e.STATE="STATE",e.CITY="CITY",e.ZIP="ZIP",e.ADDRESS="ADDRESS",e.ADDRESS2="ADDRESS2",e.PHONE="PHONE",e.EMAIL="EMAIL",e.ADDITIONAL_INFO="ADDITIONAL_INFO"}(Re||(Re={}));const we=new Map([[Re.TITLE,"tb.rulenode.entity-details-title"],[Re.COUNTRY,"tb.rulenode.entity-details-country"],[Re.STATE,"tb.rulenode.entity-details-state"],[Re.CITY,"tb.rulenode.entity-details-city"],[Re.ZIP,"tb.rulenode.entity-details-zip"],[Re.ADDRESS,"tb.rulenode.entity-details-address"],[Re.ADDRESS2,"tb.rulenode.entity-details-address2"],[Re.PHONE,"tb.rulenode.entity-details-phone"],[Re.EMAIL,"tb.rulenode.entity-details-email"],[Re.ADDITIONAL_INFO,"tb.rulenode.entity-details-additional_info"]]);var Oe,He,Ue;!function(e){e.FIRST="FIRST",e.LAST="LAST",e.ALL="ALL"}(Oe||(Oe={})),function(e){e.ASC="ASC",e.DESC="DESC"}(He||(He={})),function(e){e.STANDARD="STANDARD",e.FIFO="FIFO"}(Ue||(Ue={}));const Ke=new Map([[Ue.STANDARD,"tb.rulenode.sqs-queue-standard"],[Ue.FIFO,"tb.rulenode.sqs-queue-fifo"]]),Be=["anonymous","basic","cert.PEM"],je=new Map([["anonymous","tb.rulenode.credentials-anonymous"],["basic","tb.rulenode.credentials-basic"],["cert.PEM","tb.rulenode.credentials-pem"]]),ze=["sas","cert.PEM"],_e=new Map([["sas","tb.rulenode.credentials-sas"],["cert.PEM","tb.rulenode.credentials-pem"]]);var Qe;!function(e){e.GET="GET",e.POST="POST",e.PUT="PUT",e.DELETE="DELETE"}(Qe||(Qe={}));const $e=["US-ASCII","ISO-8859-1","UTF-8","UTF-16BE","UTF-16LE","UTF-16"],We=new Map([["US-ASCII","tb.rulenode.charset-us-ascii"],["ISO-8859-1","tb.rulenode.charset-iso-8859-1"],["UTF-8","tb.rulenode.charset-utf-8"],["UTF-16BE","tb.rulenode.charset-utf-16be"],["UTF-16LE","tb.rulenode.charset-utf-16le"],["UTF-16","tb.rulenode.charset-utf-16"]]);class Ye extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.allAzureIotHubCredentialsTypes=ze,this.azureIotHubCredentialsTypeTranslationsMap=_e}configForm(){return this.azureIotHubConfigForm}onConfigurationSet(e){this.azureIotHubConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[k.required]],host:[e?e.host:null,[k.required]],port:[e?e.port:null,[k.required,k.min(1),k.max(65535)]],connectTimeoutSec:[e?e.connectTimeoutSec:null,[k.required,k.min(1),k.max(200)]],clientId:[e?e.clientId:null,[k.required]],cleanSession:[!!e&&e.cleanSession,[]],ssl:[!!e&&e.ssl,[]],credentials:this.fb.group({type:[e&&e.credentials?e.credentials.type:null,[k.required]],sasKey:[e&&e.credentials?e.credentials.sasKey:null,[]],caCert:[e&&e.credentials?e.credentials.caCert:null,[]],caCertFileName:[e&&e.credentials?e.credentials.caCertFileName:null,[]],privateKey:[e&&e.credentials?e.credentials.privateKey:null,[]],privateKeyFileName:[e&&e.credentials?e.credentials.privateKeyFileName:null,[]],cert:[e&&e.credentials?e.credentials.cert:null,[]],certFileName:[e&&e.credentials?e.credentials.certFileName:null,[]],password:[e&&e.credentials?e.credentials.password:null,[]]})})}prepareOutputConfig(e){const t=e.credentials.type;return"sas"===t&&(e.credentials={type:t,sasKey:e.credentials.sasKey,caCert:e.credentials.caCert,caCertFileName:e.credentials.caCertFileName}),e}validatorTriggers(){return["credentials.type"]}updateValidators(e){const t=this.azureIotHubConfigForm.get("credentials"),o=t.get("type").value;switch(e&&t.reset({type:o},{emitEvent:!1}),t.get("sasKey").setValidators([]),t.get("privateKey").setValidators([]),t.get("privateKeyFileName").setValidators([]),t.get("cert").setValidators([]),t.get("certFileName").setValidators([]),o){case"sas":t.get("sasKey").setValidators([k.required]);break;case"cert.PEM":t.get("privateKey").setValidators([k.required]),t.get("privateKeyFileName").setValidators([k.required]),t.get("cert").setValidators([k.required]),t.get("certFileName").setValidators([k.required])}t.get("sasKey").updateValueAndValidity({emitEvent:e}),t.get("privateKey").updateValueAndValidity({emitEvent:e}),t.get("privateKeyFileName").updateValueAndValidity({emitEvent:e}),t.get("cert").updateValueAndValidity({emitEvent:e}),t.get("certFileName").updateValueAndValidity({emitEvent:e})}}e("AzureIotHubConfigComponent",Ye),Ye.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ye,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Ye.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Ye,selector:"tb-action-node-azure-iot-hub-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.topic\n \n \n {{ \'tb.rulenode.topic-required\' | translate }}\n \n \n \n \n tb.rulenode.hostname\n \n \n {{ \'tb.rulenode.hostname-required\' | translate }}\n \n \n \n tb.rulenode.device-id\n \n \n {{ \'tb.rulenode.device-id-required\' | translate }}\n \n \n \n \n \n tb.rulenode.credentials\n \n {{ azureIotHubCredentialsTypeTranslationsMap.get(azureIotHubConfigForm.get(\'credentials.type\').value) | translate }}\n \n \n
\n \n tb.rulenode.credentials-type\n \n \n {{ azureIotHubCredentialsTypeTranslationsMap.get(credentialsType) | translate }}\n \n \n \n {{ \'tb.rulenode.credentials-type-required\' | translate }}\n \n \n
\n \n \n \n \n tb.rulenode.sas-key\n \n \n \n {{ \'tb.rulenode.sas-key-required\' | translate }}\n \n \n \n \n \n \n \n \n \n \n \n \n \n tb.rulenode.private-key-password\n \n \n \n \n
\n
\n
\n
\n
\n',styles:[":host .tb-mqtt-credentials-panel-group{margin:0 6px}:host .tb-hint.client-id{margin-top:-1.25em;max-width:-moz-fit-content;max-width:fit-content}:host mat-checkbox{padding-bottom:16px}\n"],components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:B.MatExpansionPanel,selector:"mat-expansion-panel",inputs:["disabled","expanded","hideToggle","togglePosition"],outputs:["opened","closed","expandedChange","afterExpand","afterCollapse"],exportAs:["matExpansionPanel"]},{type:B.MatExpansionPanelHeader,selector:"mat-expansion-panel-header",inputs:["tabIndex","expandedHeight","collapsedHeight"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:j.TogglePasswordComponent,selector:"tb-toggle-password"},{type:z.FileInputComponent,selector:"tb-file-input",inputs:["label","accept","noFileText","inputId","allowedExtensions","dropLabel","contentConvertFunction","required","requiredAsError","disabled","existingFileName","readAsBinary","workFromFileObj","multipleFile"],outputs:["fileNameChanged"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]},{type:B.MatAccordion,selector:"mat-accordion",inputs:["multi","displayMode","togglePosition","hideToggle"],exportAs:["matAccordion"]},{type:B.MatExpansionPanelTitle,selector:"mat-panel-title"},{type:B.MatExpansionPanelDescription,selector:"mat-panel-description"},{type:q.FormGroupName,selector:"[formGroupName]",inputs:["formGroupName"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:w.NgSwitch,selector:"[ngSwitch]",inputs:["ngSwitch"]},{type:w.NgSwitchCase,selector:"[ngSwitchCase]",inputs:["ngSwitchCase"]},{type:D.MatSuffix,selector:"[matSuffix]"}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ye,decorators:[{type:o,args:[{selector:"tb-action-node-azure-iot-hub-config",templateUrl:"./azure-iot-hub-config.component.html",styleUrls:["./mqtt-config.component.scss"]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Je extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.serviceType=p.TB_RULE_ENGINE}configForm(){return this.checkPointConfigForm}onConfigurationSet(e){this.checkPointConfigForm=this.fb.group({queueName:[e?e.queueName:null,[k.required]]})}}e("CheckPointConfigComponent",Je),Je.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Je,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Je.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Je,selector:"tb-action-node-check-point-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n
\n',components:[{type:_.QueueTypeListComponent,selector:"tb-queue-type-list",inputs:["required","disabled","queueType"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Je,decorators:[{type:o,args:[{selector:"tb-action-node-check-point-config",templateUrl:"./check-point-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Ze extends s{constructor(e,t,o,r){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=o,this.translate=r}configForm(){return this.clearAlarmConfigForm}onConfigurationSet(e){this.clearAlarmConfigForm=this.fb.group({alarmDetailsBuildJs:[e?e.alarmDetailsBuildJs:null,[k.required]],alarmType:[e?e.alarmType:null,[k.required]]})}testScript(){const e=this.clearAlarmConfigForm.get("alarmDetailsBuildJs").value;this.nodeScriptTestService.testNodeScript(e,"json",this.translate.instant("tb.rulenode.details"),"Details",["msg","metadata","msgType"],this.ruleNodeId,"rulenode/clear_alarm_node_script_fn").subscribe((e=>{e&&this.clearAlarmConfigForm.get("alarmDetailsBuildJs").setValue(e)}))}onValidate(){this.jsFuncComponent.validateOnSubmit()}}e("ClearAlarmConfigComponent",Ze),Ze.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ze,deps:[{token:T.Store},{token:q.FormBuilder},{token:Q.NodeScriptTestService},{token:P.TranslateService}],target:t.ɵɵFactoryTarget.Component}),Ze.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Ze,selector:"tb-action-node-clear-alarm-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0,static:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n \n \n
\n \n
\n \n tb.rulenode.alarm-type\n \n \n {{ \'tb.rulenode.alarm-type-required\' | translate }}\n \n \n \n
\n',components:[{type:Y.JsFuncComponent,selector:"tb-js-func",inputs:["functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","noValidate","required"]},{type:J.MatButton,selector:"button[mat-button], button[mat-raised-button], button[mat-icon-button], button[mat-fab], button[mat-mini-fab], button[mat-stroked-button], button[mat-flat-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:D.MatLabel,selector:"mat-label"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ze,decorators:[{type:o,args:[{selector:"tb-action-node-clear-alarm-config",templateUrl:"./clear-alarm-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder},{type:Q.NodeScriptTestService},{type:P.TranslateService}]},propDecorators:{jsFuncComponent:[{type:a,args:["jsFuncComponent",{static:!0}]}]}});class Xe extends s{constructor(e,t,o,r){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=o,this.translate=r,this.alarmSeverities=Object.keys(d),this.alarmSeverityTranslationMap=f,this.separatorKeysCodes=[Z,X,ee]}configForm(){return this.createAlarmConfigForm}onConfigurationSet(e){this.createAlarmConfigForm=this.fb.group({alarmDetailsBuildJs:[e?e.alarmDetailsBuildJs:null,[k.required]],useMessageAlarmData:[!!e&&e.useMessageAlarmData,[]],overwriteAlarmDetails:[!!e&&e.overwriteAlarmDetails,[]],alarmType:[e?e.alarmType:null,[]],severity:[e?e.severity:null,[]],propagate:[!!e&&e.propagate,[]],relationTypes:[e?e.relationTypes:null,[]],propagateToOwner:[!!e&&e.propagateToOwner,[]],propagateToTenant:[!!e&&e.propagateToTenant,[]],dynamicSeverity:!1}),this.createAlarmConfigForm.get("dynamicSeverity").valueChanges.subscribe((e=>{e?this.createAlarmConfigForm.get("severity").patchValue("",{emitEvent:!1}):this.createAlarmConfigForm.get("severity").patchValue(this.alarmSeverities[0],{emitEvent:!1})}))}validatorTriggers(){return["useMessageAlarmData"]}updateValidators(e){this.createAlarmConfigForm.get("useMessageAlarmData").value?(this.createAlarmConfigForm.get("alarmType").setValidators([]),this.createAlarmConfigForm.get("severity").setValidators([])):(this.createAlarmConfigForm.get("alarmType").setValidators([k.required]),this.createAlarmConfigForm.get("severity").setValidators([k.required])),this.createAlarmConfigForm.get("alarmType").updateValueAndValidity({emitEvent:e}),this.createAlarmConfigForm.get("severity").updateValueAndValidity({emitEvent:e})}testScript(){const e=this.createAlarmConfigForm.get("alarmDetailsBuildJs").value;this.nodeScriptTestService.testNodeScript(e,"json",this.translate.instant("tb.rulenode.details"),"Details",["msg","metadata","msgType"],this.ruleNodeId,"rulenode/create_alarm_node_script_fn").subscribe((e=>{e&&this.createAlarmConfigForm.get("alarmDetailsBuildJs").setValue(e)}))}removeKey(e,t){const o=this.createAlarmConfigForm.get(t).value,r=o.indexOf(e);r>=0&&(o.splice(r,1),this.createAlarmConfigForm.get(t).setValue(o,{emitEvent:!0}))}addKey(e,t){const o=e.input;let r=e.value;if((r||"").trim()){r=r.trim();let e=this.createAlarmConfigForm.get(t).value;e&&-1!==e.indexOf(r)||(e||(e=[]),e.push(r),this.createAlarmConfigForm.get(t).setValue(e,{emitEvent:!0}))}o&&(o.value="")}onValidate(){const e=this.createAlarmConfigForm.get("useMessageAlarmData").value,t=this.createAlarmConfigForm.get("overwriteAlarmDetails").value;e&&!t||this.jsFuncComponent.validateOnSubmit()}}e("CreateAlarmConfigComponent",Xe),Xe.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Xe,deps:[{token:T.Store},{token:q.FormBuilder},{token:Q.NodeScriptTestService},{token:P.TranslateService}],target:t.ɵɵFactoryTarget.Component}),Xe.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Xe,selector:"tb-action-node-create-alarm-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0,static:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'tb.rulenode.use-message-alarm-data\' | translate }}\n \n \n {{ \'tb.rulenode.overwrite-alarm-details\' | translate }}\n \n
\n \n \n \n
\n \n
\n
\n
\n \n tb.rulenode.alarm-type\n \n \n {{ \'tb.rulenode.alarm-type-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.use-alarm-severity-pattern\' | translate }}\n \n \n tb.rulenode.alarm-severity\n \n \n {{ alarmSeverityTranslationMap.get(severity) | translate }}\n \n \n \n {{ \'tb.rulenode.alarm-severity-required\' | translate }}\n \n \n \n tb.rulenode.alarm-severity-pattern\n \n \n {{ \'tb.rulenode.alarm-severity-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.propagate\' | translate }}\n \n
\n \n tb.rulenode.relation-types-list\n \n \n {{key}}\n close\n \n \n \n tb.rulenode.relation-types-list-hint\n \n
\n \n {{ \'tb.rulenode.propagate-to-owner\' | translate }}\n \n \n {{ \'tb.rulenode.propagate-to-tenant\' | translate }}\n \n
\n
\n',components:[{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:Y.JsFuncComponent,selector:"tb-js-func",inputs:["functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","noValidate","required"]},{type:J.MatButton,selector:"button[mat-button], button[mat-raised-button], button[mat-icon-button], button[mat-fab], button[mat-mini-fab], button[mat-stroked-button], button[mat-flat-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:te.MatChipList,selector:"mat-chip-list",inputs:["aria-orientation","multiple","compareWith","value","required","placeholder","disabled","selectable","tabIndex","errorStateMatcher"],outputs:["change","valueChange"],exportAs:["matChipList"]},{type:oe.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:re.DefaultShowHideDirective,selector:" [fxShow], [fxShow.print], [fxShow.xs], [fxShow.sm], [fxShow.md], [fxShow.lg], [fxShow.xl], [fxShow.lt-sm], [fxShow.lt-md], [fxShow.lt-lg], [fxShow.lt-xl], [fxShow.gt-xs], [fxShow.gt-sm], [fxShow.gt-md], [fxShow.gt-lg], [fxHide], [fxHide.print], [fxHide.xs], [fxHide.sm], [fxHide.md], [fxHide.lg], [fxHide.xl], [fxHide.lt-sm], [fxHide.lt-md], [fxHide.lt-lg], [fxHide.lt-xl], [fxHide.gt-xs], [fxHide.gt-sm], [fxHide.gt-md], [fxHide.gt-lg]",inputs:["fxShow","fxShow.print","fxShow.xs","fxShow.sm","fxShow.md","fxShow.lg","fxShow.xl","fxShow.lt-sm","fxShow.lt-md","fxShow.lt-lg","fxShow.lt-xl","fxShow.gt-xs","fxShow.gt-sm","fxShow.gt-md","fxShow.gt-lg","fxHide","fxHide.print","fxHide.xs","fxHide.sm","fxHide.md","fxHide.lg","fxHide.xl","fxHide.lt-sm","fxHide.lt-md","fxHide.lt-lg","fxHide.lt-xl","fxHide.gt-xs","fxHide.gt-sm","fxHide.gt-md","fxHide.gt-lg"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:D.MatLabel,selector:"mat-label"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:te.MatChip,selector:"mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]",inputs:["color","disableRipple","tabIndex","selected","value","selectable","disabled","removable"],outputs:["selectionChange","destroyed","removed"],exportAs:["matChip"]},{type:te.MatChipRemove,selector:"[matChipRemove]"},{type:te.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputSeparatorKeyCodes","placeholder","id","matChipInputFor","matChipInputAddOnBlur","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Xe,decorators:[{type:o,args:[{selector:"tb-action-node-create-alarm-config",templateUrl:"./create-alarm-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder},{type:Q.NodeScriptTestService},{type:P.TranslateService}]},propDecorators:{jsFuncComponent:[{type:a,args:["jsFuncComponent",{static:!0}]}]}});class et extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.directionTypes=Object.keys(c),this.directionTypeTranslations=g,this.entityType=x}configForm(){return this.createRelationConfigForm}onConfigurationSet(e){this.createRelationConfigForm=this.fb.group({direction:[e?e.direction:null,[k.required]],entityType:[e?e.entityType:null,[k.required]],entityNamePattern:[e?e.entityNamePattern:null,[]],entityTypePattern:[e?e.entityTypePattern:null,[]],relationType:[e?e.relationType:null,[k.required]],createEntityIfNotExists:[!!e&&e.createEntityIfNotExists,[]],removeCurrentRelations:[!!e&&e.removeCurrentRelations,[]],changeOriginatorToRelatedEntity:[!!e&&e.changeOriginatorToRelatedEntity,[]],entityCacheExpiration:[e?e.entityCacheExpiration:null,[k.required,k.min(0)]]})}validatorTriggers(){return["entityType"]}updateValidators(e){const t=this.createRelationConfigForm.get("entityType").value;t?this.createRelationConfigForm.get("entityNamePattern").setValidators([k.required,k.pattern(/.*\S.*/)]):this.createRelationConfigForm.get("entityNamePattern").setValidators([]),!t||t!==x.DEVICE&&t!==x.ASSET?this.createRelationConfigForm.get("entityTypePattern").setValidators([]):this.createRelationConfigForm.get("entityTypePattern").setValidators([k.required,k.pattern(/.*\S.*/)]),this.createRelationConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e}),this.createRelationConfigForm.get("entityTypePattern").updateValueAndValidity({emitEvent:e})}prepareOutputConfig(e){return e.entityNamePattern=e.entityNamePattern?e.entityNamePattern.trim():null,e.entityTypePattern=e.entityTypePattern?e.entityTypePattern.trim():null,e}}e("CreateRelationConfigComponent",et),et.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:et,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),et.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:et,selector:"tb-action-node-create-relation-config",usesInheritance:!0,ngImport:t,template:'
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.entity-type-pattern\n \n \n {{ \'tb.rulenode.entity-type-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.relation-type-pattern\n \n \n {{ \'tb.rulenode.relation-type-pattern-required\' | translate }}\n \n \n \n
\n \n {{ \'tb.rulenode.create-entity-if-not-exists\' | translate }}\n \n
tb.rulenode.create-entity-if-not-exists-hint
\n
\n \n {{ \'tb.rulenode.remove-current-relations\' | translate }}\n \n
tb.rulenode.remove-current-relations-hint
\n \n {{ \'tb.rulenode.change-originator-to-related-entity\' | translate }}\n \n
tb.rulenode.change-originator-to-related-entity-hint
\n \n tb.rulenode.entity-cache-expiration\n \n \n {{ \'tb.rulenode.entity-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.entity-cache-expiration-range\' | translate }}\n \n tb.rulenode.entity-cache-expiration-hint\n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:ae.EntityTypeSelectComponent,selector:"tb-entity-type-select",inputs:["allowedEntityTypes","useAliasEntityTypes","showLabel","required","disabled"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:E.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:et,decorators:[{type:o,args:[{selector:"tb-action-node-create-relation-config",templateUrl:"./create-relation-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class tt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.directionTypes=Object.keys(c),this.directionTypeTranslations=g,this.entityType=x}configForm(){return this.deleteRelationConfigForm}onConfigurationSet(e){this.deleteRelationConfigForm=this.fb.group({deleteForSingleEntity:[!!e&&e.deleteForSingleEntity,[]],direction:[e?e.direction:null,[k.required]],entityType:[e?e.entityType:null,[]],entityNamePattern:[e?e.entityNamePattern:null,[]],relationType:[e?e.relationType:null,[k.required]],entityCacheExpiration:[e?e.entityCacheExpiration:null,[k.required,k.min(0)]]})}validatorTriggers(){return["deleteForSingleEntity","entityType"]}updateValidators(e){const t=this.deleteRelationConfigForm.get("deleteForSingleEntity").value,o=this.deleteRelationConfigForm.get("entityType").value;t?this.deleteRelationConfigForm.get("entityType").setValidators([k.required]):this.deleteRelationConfigForm.get("entityType").setValidators([]),t&&o?this.deleteRelationConfigForm.get("entityNamePattern").setValidators([k.required,k.pattern(/.*\S.*/)]):this.deleteRelationConfigForm.get("entityNamePattern").setValidators([]),this.deleteRelationConfigForm.get("entityType").updateValueAndValidity({emitEvent:!1}),this.deleteRelationConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e})}prepareOutputConfig(e){return e.entityNamePattern=e.entityNamePattern?e.entityNamePattern.trim():null,e}}e("DeleteRelationConfigComponent",tt),tt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:tt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),tt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:tt,selector:"tb-action-node-delete-relation-config",usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'tb.rulenode.delete-relation-to-specific-entity\' | translate }}\n \n
tb.rulenode.delete-relation-hint
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.relation-type-pattern\n \n \n {{ \'tb.rulenode.relation-type-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.entity-cache-expiration\n \n \n {{ \'tb.rulenode.entity-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.entity-cache-expiration-range\' | translate }}\n \n tb.rulenode.entity-cache-expiration-hint\n \n
\n',components:[{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:ae.EntityTypeSelectComponent,selector:"tb-entity-type-select",inputs:["allowedEntityTypes","useAliasEntityTypes","showLabel","required","disabled"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:D.MatLabel,selector:"mat-label"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:E.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:tt,decorators:[{type:o,args:[{selector:"tb-action-node-delete-relation-config",templateUrl:"./delete-relation-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class ot extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.deviceProfile}onConfigurationSet(e){this.deviceProfile=this.fb.group({persistAlarmRulesState:[!!e&&e.persistAlarmRulesState,k.required],fetchAlarmRulesStateOnStart:[!!e&&e.fetchAlarmRulesStateOnStart,k.required]})}}e("DeviceProfileConfigComponent",ot),ot.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ot,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),ot.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:ot,selector:"tb-device-profile-config",usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'tb.rulenode.persist-alarm-rules\' | translate }}\n \n \n {{ \'tb.rulenode.fetch-alarm-rules\' | translate }}\n \n
\n',components:[{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ot,decorators:[{type:o,args:[{selector:"tb-device-profile-config",templateUrl:"./device-profile-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class rt extends s{constructor(e,t,o,r){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=o,this.translate=r}configForm(){return this.generatorConfigForm}onConfigurationSet(e){this.generatorConfigForm=this.fb.group({msgCount:[e?e.msgCount:null,[k.required,k.min(0)]],periodInSeconds:[e?e.periodInSeconds:null,[k.required,k.min(1)]],originator:[e?e.originator:null,[]],jsScript:[e?e.jsScript:null,[k.required]]})}prepareInputConfig(e){return e&&(e.originatorId&&e.originatorType?e.originator={id:e.originatorId,entityType:e.originatorType}:e.originator=null,delete e.originatorId,delete e.originatorType),e}prepareOutputConfig(e){return e.originator?(e.originatorId=e.originator.id,e.originatorType=e.originator.entityType):(e.originatorId=null,e.originatorType=null),delete e.originator,e}testScript(){const e=this.generatorConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(e,"generate",this.translate.instant("tb.rulenode.generator"),"Generate",["prevMsg","prevMetadata","prevMsgType"],this.ruleNodeId,"rulenode/generator_node_script_fn").subscribe((e=>{e&&this.generatorConfigForm.get("jsScript").setValue(e)}))}onValidate(){this.jsFuncComponent.validateOnSubmit()}}e("GeneratorConfigComponent",rt),rt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:rt,deps:[{token:T.Store},{token:q.FormBuilder},{token:Q.NodeScriptTestService},{token:P.TranslateService}],target:t.ɵɵFactoryTarget.Component}),rt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:rt,selector:"tb-action-node-generator-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0,static:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.message-count\n \n \n {{ \'tb.rulenode.message-count-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-message-count-message\' | translate }}\n \n \n \n tb.rulenode.period-seconds\n \n \n {{ \'tb.rulenode.period-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-period-seconds-message\' | translate }}\n \n \n
\n \n \n \n
\n \n \n \n
\n \n
\n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:ne.EntitySelectComponent,selector:"tb-entity-select",inputs:["allowedEntityTypes","useAliasEntityTypes","required","disabled"]},{type:Y.JsFuncComponent,selector:"tb-js-func",inputs:["functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","noValidate","required"]},{type:J.MatButton,selector:"button[mat-button], button[mat-raised-button], button[mat-icon-button], button[mat-fab], button[mat-mini-fab], button[mat-stroked-button], button[mat-flat-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:rt,decorators:[{type:o,args:[{selector:"tb-action-node-generator-config",templateUrl:"./generator-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder},{type:Q.NodeScriptTestService},{type:P.TranslateService}]},propDecorators:{jsFuncComponent:[{type:a,args:["jsFuncComponent",{static:!0}]}]}});class at extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.perimeterType=Ae,this.perimeterTypes=Object.keys(Ae),this.perimeterTypeTranslationMap=Ge,this.rangeUnits=Object.keys(Ee),this.rangeUnitTranslationMap=Pe,this.timeUnits=Object.keys(De),this.timeUnitsTranslationMap=Ve}configForm(){return this.geoActionConfigForm}onConfigurationSet(e){this.geoActionConfigForm=this.fb.group({latitudeKeyName:[e?e.latitudeKeyName:null,[k.required]],longitudeKeyName:[e?e.longitudeKeyName:null,[k.required]],perimeterType:[e?e.perimeterType:null,[k.required]],fetchPerimeterInfoFromMessageMetadata:[!!e&&e.fetchPerimeterInfoFromMessageMetadata,[]],perimeterKeyName:[e?e.perimeterKeyName:null,[]],centerLatitude:[e?e.centerLatitude:null,[]],centerLongitude:[e?e.centerLatitude:null,[]],range:[e?e.range:null,[]],rangeUnit:[e?e.rangeUnit:null,[]],polygonsDefinition:[e?e.polygonsDefinition:null,[]],minInsideDuration:[e?e.minInsideDuration:null,[k.required,k.min(1),k.max(2147483647)]],minInsideDurationTimeUnit:[e?e.minInsideDurationTimeUnit:null,[k.required]],minOutsideDuration:[e?e.minOutsideDuration:null,[k.required,k.min(1),k.max(2147483647)]],minOutsideDurationTimeUnit:[e?e.minOutsideDurationTimeUnit:null,[k.required]]})}validatorTriggers(){return["fetchPerimeterInfoFromMessageMetadata","perimeterType"]}updateValidators(e){const t=this.geoActionConfigForm.get("fetchPerimeterInfoFromMessageMetadata").value,o=this.geoActionConfigForm.get("perimeterType").value;t?this.geoActionConfigForm.get("perimeterKeyName").setValidators([k.required]):this.geoActionConfigForm.get("perimeterKeyName").setValidators([]),t||o!==Ae.CIRCLE?(this.geoActionConfigForm.get("centerLatitude").setValidators([]),this.geoActionConfigForm.get("centerLongitude").setValidators([]),this.geoActionConfigForm.get("range").setValidators([]),this.geoActionConfigForm.get("rangeUnit").setValidators([])):(this.geoActionConfigForm.get("centerLatitude").setValidators([k.required,k.min(-90),k.max(90)]),this.geoActionConfigForm.get("centerLongitude").setValidators([k.required,k.min(-180),k.max(180)]),this.geoActionConfigForm.get("range").setValidators([k.required,k.min(0)]),this.geoActionConfigForm.get("rangeUnit").setValidators([k.required])),t||o!==Ae.POLYGON?this.geoActionConfigForm.get("polygonsDefinition").setValidators([]):this.geoActionConfigForm.get("polygonsDefinition").setValidators([k.required]),this.geoActionConfigForm.get("perimeterKeyName").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("centerLatitude").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("centerLongitude").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("range").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("rangeUnit").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("polygonsDefinition").updateValueAndValidity({emitEvent:e})}}e("GpsGeoActionConfigComponent",at),at.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:at,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),at.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:at,selector:"tb-action-node-gps-geofencing-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.latitude-key-name\n \n \n {{ \'tb.rulenode.latitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.longitude-key-name\n \n \n {{ \'tb.rulenode.longitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \n \n \n {{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}\n \n \n tb.rulenode.perimeter-key-name\n \n \n {{ \'tb.rulenode.perimeter-key-name-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.circle-center-latitude\n \n \n {{ \'tb.rulenode.circle-center-latitude-required\' | translate }}\n \n \n \n tb.rulenode.circle-center-longitude\n \n \n {{ \'tb.rulenode.circle-center-longitude-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.range\n \n \n {{ \'tb.rulenode.range-required\' | translate }}\n \n \n \n tb.rulenode.range-units\n \n \n {{ rangeUnitTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n \n tb.rulenode.polygon-definition\n \n \n {{ \'tb.rulenode.polygon-definition-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.min-inside-duration\n \n \n {{ \'tb.rulenode.min-inside-duration-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.min-inside-duration-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n tb.rulenode.min-outside-duration\n \n \n {{ \'tb.rulenode.min-outside-duration-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.min-outside-duration-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:E.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:at,decorators:[{type:o,args:[{selector:"tb-action-node-gps-geofencing-config",templateUrl:"./gps-geo-action-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class nt extends y{constructor(e,t,o,r){super(e),this.store=e,this.translate=t,this.injector=o,this.fb=r,this.propagateChange=null,this.valueChangeSubscription=null}get required(){return this.requiredValue}set required(e){this.requiredValue=le(e)}ngOnInit(){this.ngControl=this.injector.get(M),null!=this.ngControl&&(this.ngControl.valueAccessor=this),this.kvListFormGroup=this.fb.group({}),this.kvListFormGroup.addControl("keyVals",this.fb.array([]))}keyValsFormArray(){return this.kvListFormGroup.get("keyVals")}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}setDisabledState(e){this.disabled=e,this.disabled?this.kvListFormGroup.disable({emitEvent:!1}):this.kvListFormGroup.enable({emitEvent:!1})}writeValue(e){this.valueChangeSubscription&&this.valueChangeSubscription.unsubscribe();const t=[];if(e)for(const o of Object.keys(e))Object.prototype.hasOwnProperty.call(e,o)&&t.push(this.fb.group({key:[o,[k.required]],value:[e[o],[k.required]]}));this.kvListFormGroup.setControl("keyVals",this.fb.array(t)),this.valueChangeSubscription=this.kvListFormGroup.valueChanges.subscribe((()=>{this.updateModel()}))}removeKeyVal(e){this.kvListFormGroup.get("keyVals").removeAt(e)}addKeyVal(){this.kvListFormGroup.get("keyVals").push(this.fb.group({key:["",[k.required]],value:["",[k.required]]}))}validate(e){return!this.kvListFormGroup.get("keyVals").value.length&&this.required?{kvMapRequired:!0}:this.kvListFormGroup.valid?null:{kvFieldsRequired:!0}}updateModel(){const e=this.kvListFormGroup.get("keyVals").value;if(this.required&&!e.length||!this.kvListFormGroup.valid)this.propagateChange(null);else{const t={};e.forEach((e=>{t[e.key]=e.value})),this.propagateChange(t)}}}e("KvMapConfigComponent",nt),nt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:nt,deps:[{token:T.Store},{token:P.TranslateService},{token:t.Injector},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),nt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:nt,selector:"tb-kv-map-config",inputs:{disabled:"disabled",requiredText:"requiredText",keyText:"keyText",keyRequiredText:"keyRequiredText",valText:"valText",valRequiredText:"valRequiredText",hintText:"hintText",required:"required"},providers:[{provide:S,useExisting:n((()=>nt)),multi:!0},{provide:A,useExisting:n((()=>nt)),multi:!0}],usesInheritance:!0,ngImport:t,template:'
\n
\n {{ keyText | translate }}\n {{ valText | translate }}\n \n
\n
\n
\n \n \n \n \n {{ keyRequiredText | translate }}\n \n \n \n \n \n \n {{ valRequiredText | translate }}\n \n \n \n
\n
\n
\n \n
\n \n
\n
\n',styles:[":host .tb-kv-map-config{margin-bottom:16px}:host .tb-kv-map-config .header{padding-left:5px;padding-right:5px;padding-bottom:5px}:host .tb-kv-map-config .header .cell{padding-left:5px;padding-right:5px;color:#0000008a;font-size:12px;font-weight:700;white-space:nowrap}:host .tb-kv-map-config .body{padding-left:5px;padding-right:5px;padding-bottom:20px;max-height:300px;overflow:auto}:host .tb-kv-map-config .body .cell{padding-left:5px;padding-right:5px}:host ::ng-deep .tb-kv-map-config .body mat-form-field.cell{margin:0}:host ::ng-deep .tb-kv-map-config .body mat-form-field.cell .mat-form-field-infix{border-top:0}:host ::ng-deep .tb-kv-map-config .body button.mat-button{margin:0;align-self:baseline}\n"],components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:J.MatButton,selector:"button[mat-button], button[mat-raised-button], button[mat-icon-button], button[mat-fab], button[mat-mini-fab], button[mat-stroked-button], button[mat-flat-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{type:oe.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{type:ie.TbErrorComponent,selector:"tb-error",inputs:["error"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:E.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{type:re.DefaultShowHideDirective,selector:" [fxShow], [fxShow.print], [fxShow.xs], [fxShow.sm], [fxShow.md], [fxShow.lg], [fxShow.xl], [fxShow.lt-sm], [fxShow.lt-md], [fxShow.lt-lg], [fxShow.lt-xl], [fxShow.gt-xs], [fxShow.gt-sm], [fxShow.gt-md], [fxShow.gt-lg], [fxHide], [fxHide.print], [fxHide.xs], [fxHide.sm], [fxHide.md], [fxHide.lg], [fxHide.xl], [fxHide.lt-sm], [fxHide.lt-md], [fxHide.lt-lg], [fxHide.lt-xl], [fxHide.gt-xs], [fxHide.gt-sm], [fxHide.gt-md], [fxHide.gt-lg]",inputs:["fxShow","fxShow.print","fxShow.xs","fxShow.sm","fxShow.md","fxShow.lg","fxShow.xl","fxShow.lt-sm","fxShow.lt-md","fxShow.lt-lg","fxShow.lt-xl","fxShow.gt-xs","fxShow.gt-sm","fxShow.gt-md","fxShow.gt-lg","fxHide","fxHide.print","fxHide.xs","fxHide.sm","fxHide.md","fxHide.lg","fxHide.xl","fxHide.lt-sm","fxHide.lt-md","fxHide.lt-lg","fxHide.lt-xl","fxHide.gt-xs","fxHide.gt-sm","fxHide.gt-md","fxHide.gt-lg"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:E.DefaultLayoutAlignDirective,selector:" [fxLayoutAlign], [fxLayoutAlign.xs], [fxLayoutAlign.sm], [fxLayoutAlign.md], [fxLayoutAlign.lg], [fxLayoutAlign.xl], [fxLayoutAlign.lt-sm], [fxLayoutAlign.lt-md], [fxLayoutAlign.lt-lg], [fxLayoutAlign.lt-xl], [fxLayoutAlign.gt-xs], [fxLayoutAlign.gt-sm], [fxLayoutAlign.gt-md], [fxLayoutAlign.gt-lg]",inputs:["fxLayoutAlign","fxLayoutAlign.xs","fxLayoutAlign.sm","fxLayoutAlign.md","fxLayoutAlign.lg","fxLayoutAlign.xl","fxLayoutAlign.lt-sm","fxLayoutAlign.lt-md","fxLayoutAlign.lt-lg","fxLayoutAlign.lt-xl","fxLayoutAlign.gt-xs","fxLayoutAlign.gt-sm","fxLayoutAlign.gt-md","fxLayoutAlign.gt-lg"]},{type:q.FormArrayName,selector:"[formArrayName]",inputs:["formArrayName"]},{type:D.MatLabel,selector:"mat-label"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlDirective,selector:"[formControl]",inputs:["disabled","formControl","ngModel"],outputs:["ngModelChange"],exportAs:["ngForm"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:se.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]}],pipes:{translate:P.TranslatePipe,async:w.AsyncPipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:nt,decorators:[{type:o,args:[{selector:"tb-kv-map-config",templateUrl:"./kv-map-config.component.html",styleUrls:["./kv-map-config.component.scss"],providers:[{provide:S,useExisting:n((()=>nt)),multi:!0},{provide:A,useExisting:n((()=>nt)),multi:!0}]}]}],ctorParameters:function(){return[{type:T.Store},{type:P.TranslateService},{type:t.Injector},{type:q.FormBuilder}]},propDecorators:{disabled:[{type:l}],requiredText:[{type:l}],keyText:[{type:l}],keyRequiredText:[{type:l}],valText:[{type:l}],valRequiredText:[{type:l}],hintText:[{type:l}],required:[{type:l}]}});class lt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.ackValues=["all","-1","0","1"],this.ToByteStandartCharsetTypesValues=$e,this.ToByteStandartCharsetTypeTranslationMap=We}configForm(){return this.kafkaConfigForm}onConfigurationSet(e){this.kafkaConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[k.required]],bootstrapServers:[e?e.bootstrapServers:null,[k.required]],retries:[e?e.retries:null,[k.min(0)]],batchSize:[e?e.batchSize:null,[k.min(0)]],linger:[e?e.linger:null,[k.min(0)]],bufferMemory:[e?e.bufferMemory:null,[k.min(0)]],acks:[e?e.acks:null,[k.required]],keySerializer:[e?e.keySerializer:null,[k.required]],valueSerializer:[e?e.valueSerializer:null,[k.required]],otherProperties:[e?e.otherProperties:null,[]],addMetadataKeyValuesAsKafkaHeaders:[!!e&&e.addMetadataKeyValuesAsKafkaHeaders,[]],kafkaHeadersCharset:[e?e.kafkaHeadersCharset:null,[]]})}validatorTriggers(){return["addMetadataKeyValuesAsKafkaHeaders"]}updateValidators(e){this.kafkaConfigForm.get("addMetadataKeyValuesAsKafkaHeaders").value?this.kafkaConfigForm.get("kafkaHeadersCharset").setValidators([k.required]):this.kafkaConfigForm.get("kafkaHeadersCharset").setValidators([]),this.kafkaConfigForm.get("kafkaHeadersCharset").updateValueAndValidity({emitEvent:e})}}e("KafkaConfigComponent",lt),lt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:lt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),lt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:lt,selector:"tb-action-node-kafka-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.topic-pattern\n \n \n {{ \'tb.rulenode.topic-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.bootstrap-servers\n \n \n {{ \'tb.rulenode.bootstrap-servers-required\' | translate }}\n \n \n \n tb.rulenode.retries\n \n \n {{ \'tb.rulenode.min-retries-message\' | translate }}\n \n \n \n tb.rulenode.batch-size-bytes\n \n \n {{ \'tb.rulenode.min-batch-size-bytes-message\' | translate }}\n \n \n \n tb.rulenode.linger-ms\n \n \n {{ \'tb.rulenode.min-linger-ms-message\' | translate }}\n \n \n \n tb.rulenode.buffer-memory-bytes\n \n \n {{ \'tb.rulenode.min-buffer-memory-bytes-message\' | translate }}\n \n \n \n tb.rulenode.acks\n \n \n {{ ackValue }}\n \n \n \n \n tb.rulenode.key-serializer\n \n \n {{ \'tb.rulenode.key-serializer-required\' | translate }}\n \n \n \n tb.rulenode.value-serializer\n \n \n {{ \'tb.rulenode.value-serializer-required\' | translate }}\n \n \n \n \n \n \n {{ \'tb.rulenode.add-metadata-key-values-as-kafka-headers\' | translate }}\n \n
tb.rulenode.add-metadata-key-values-as-kafka-headers-hint
\n \n tb.rulenode.charset-encoding\n \n \n {{ ToByteStandartCharsetTypeTranslationMap.get(charset) | translate }}\n \n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:nt,selector:"tb-kv-map-config",inputs:["disabled","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:lt,decorators:[{type:o,args:[{selector:"tb-action-node-kafka-config",templateUrl:"./kafka-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class it extends s{constructor(e,t,o,r){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=o,this.translate=r}configForm(){return this.logConfigForm}onConfigurationSet(e){this.logConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[k.required]]})}testScript(){const e=this.logConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(e,"string",this.translate.instant("tb.rulenode.to-string"),"ToString",["msg","metadata","msgType"],this.ruleNodeId,"rulenode/log_node_script_fn").subscribe((e=>{e&&this.logConfigForm.get("jsScript").setValue(e)}))}onValidate(){this.jsFuncComponent.validateOnSubmit()}}e("LogConfigComponent",it),it.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:it,deps:[{token:T.Store},{token:q.FormBuilder},{token:Q.NodeScriptTestService},{token:P.TranslateService}],target:t.ɵɵFactoryTarget.Component}),it.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:it,selector:"tb-action-node-log-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0,static:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n \n \n
\n \n
\n
\n',components:[{type:Y.JsFuncComponent,selector:"tb-js-func",inputs:["functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","noValidate","required"]},{type:J.MatButton,selector:"button[mat-button], button[mat-raised-button], button[mat-icon-button], button[mat-fab], button[mat-mini-fab], button[mat-stroked-button], button[mat-flat-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:it,decorators:[{type:o,args:[{selector:"tb-action-node-log-config",templateUrl:"./log-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder},{type:Q.NodeScriptTestService},{type:P.TranslateService}]},propDecorators:{jsFuncComponent:[{type:a,args:["jsFuncComponent",{static:!0}]}]}});class st extends y{constructor(e,t){super(e),this.store=e,this.fb=t,this.subscriptions=[],this.disableCertPemCredentials=!1,this.passwordFieldRquired=!0,this.allCredentialsTypes=Be,this.credentialsTypeTranslationsMap=je,this.propagateChange=null}get required(){return this.requiredValue}set required(e){this.requiredValue=le(e)}ngOnInit(){this.credentialsConfigFormGroup=this.fb.group({type:[null,[k.required]],username:[null,[]],password:[null,[]],caCert:[null,[]],caCertFileName:[null,[]],privateKey:[null,[]],privateKeyFileName:[null,[]],cert:[null,[]],certFileName:[null,[]]}),this.subscriptions.push(this.credentialsConfigFormGroup.valueChanges.pipe(me()).subscribe((()=>{this.updateView()}))),this.subscriptions.push(this.credentialsConfigFormGroup.get("type").valueChanges.subscribe((()=>{this.credentialsTypeChanged()})))}ngOnChanges(e){for(const t of Object.keys(e)){const o=e[t];if(!o.firstChange&&o.currentValue!==o.previousValue&&o.currentValue&&"disableCertPemCredentials"===t){"cert.PEM"===this.credentialsConfigFormGroup.get("type").value&&setTimeout((()=>{this.credentialsConfigFormGroup.get("type").patchValue("anonymous",{emitEvent:!0})}))}}}ngOnDestroy(){this.subscriptions.forEach((e=>e.unsubscribe()))}writeValue(e){$(e)&&(this.credentialsConfigFormGroup.reset(e,{emitEvent:!1}),this.updateValidators(!1))}setDisabledState(e){e?this.credentialsConfigFormGroup.disable():(this.credentialsConfigFormGroup.enable(),this.updateValidators())}updateView(){let e=this.credentialsConfigFormGroup.value;const t=e.type;switch(t){case"anonymous":e={type:t};break;case"basic":e={type:t,username:e.username,password:e.password};break;case"cert.PEM":delete e.username}this.propagateChange(e)}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}validate(e){return this.credentialsConfigFormGroup.valid?null:{credentialsConfig:{valid:!1}}}credentialsTypeChanged(){this.credentialsConfigFormGroup.patchValue({username:null,password:null,caCert:null,caCertFileName:null,privateKey:null,privateKeyFileName:null,cert:null,certFileName:null}),this.updateValidators()}updateValidators(e=!1){const t=this.credentialsConfigFormGroup.get("type").value;switch(e&&this.credentialsConfigFormGroup.reset({type:t},{emitEvent:!1}),this.credentialsConfigFormGroup.setValidators([]),this.credentialsConfigFormGroup.get("username").setValidators([]),this.credentialsConfigFormGroup.get("password").setValidators([]),t){case"anonymous":break;case"basic":this.credentialsConfigFormGroup.get("username").setValidators([k.required]),this.credentialsConfigFormGroup.get("password").setValidators(this.passwordFieldRquired?[k.required]:[]);break;case"cert.PEM":this.credentialsConfigFormGroup.setValidators([this.requiredFilesSelected(k.required,[["caCert","caCertFileName"],["privateKey","privateKeyFileName","cert","certFileName"]])])}this.credentialsConfigFormGroup.get("username").updateValueAndValidity({emitEvent:e}),this.credentialsConfigFormGroup.get("password").updateValueAndValidity({emitEvent:e}),this.credentialsConfigFormGroup.updateValueAndValidity({emitEvent:e})}requiredFilesSelected(e,t=null){return o=>{t||(t=[Object.keys(o.controls)]);return(null==o?void 0:o.controls)&&t.some((t=>t.every((t=>!e(o.controls[t])))))?null:{notAllRequiredFilesSelected:!0}}}}e("CredentialsConfigComponent",st),st.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:st,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),st.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:st,selector:"tb-credentials-config",inputs:{required:"required",disableCertPemCredentials:"disableCertPemCredentials",passwordFieldRquired:"passwordFieldRquired"},providers:[{provide:S,useExisting:n((()=>st)),multi:!0},{provide:A,useExisting:n((()=>st)),multi:!0}],usesInheritance:!0,usesOnChanges:!0,ngImport:t,template:'
\n \n \n tb.rulenode.credentials\n \n {{ credentialsTypeTranslationsMap.get(credentialsConfigFormGroup.get(\'type\').value) | translate }}\n \n \n \n \n tb.rulenode.credentials-type\n \n \n {{ credentialsTypeTranslationsMap.get(credentialsType) | translate }}\n \n \n \n {{ \'tb.rulenode.credentials-type-required\' | translate }}\n \n \n
\n \n \n \n \n tb.rulenode.username\n \n \n {{ \'tb.rulenode.username-required\' | translate }}\n \n \n \n tb.rulenode.password\n \n \n \n {{ \'tb.rulenode.password-required\' | translate }}\n \n \n \n \n
{{ \'tb.rulenode.credentials-pem-hint\' | translate }}
\n \n \n \n \n \n \n \n tb.rulenode.private-key-password\n \n \n \n
\n
\n
\n
\n
\n',components:[{type:B.MatExpansionPanel,selector:"mat-expansion-panel",inputs:["disabled","expanded","hideToggle","togglePosition"],outputs:["opened","closed","expandedChange","afterExpand","afterCollapse"],exportAs:["matExpansionPanel"]},{type:B.MatExpansionPanelHeader,selector:"mat-expansion-panel-header",inputs:["tabIndex","expandedHeight","collapsedHeight"]},{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:j.TogglePasswordComponent,selector:"tb-toggle-password"},{type:z.FileInputComponent,selector:"tb-file-input",inputs:["label","accept","noFileText","inputId","allowedExtensions","dropLabel","contentConvertFunction","required","requiredAsError","disabled","existingFileName","readAsBinary","workFromFileObj","multipleFile"],outputs:["fileNameChanged"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:B.MatExpansionPanelTitle,selector:"mat-panel-title"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:B.MatExpansionPanelDescription,selector:"mat-panel-description"},{type:B.MatExpansionPanelContent,selector:"ng-template[matExpansionPanelContent]"},{type:D.MatLabel,selector:"mat-label"},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:w.NgSwitch,selector:"[ngSwitch]",inputs:["ngSwitch"]},{type:w.NgSwitchCase,selector:"[ngSwitchCase]",inputs:["ngSwitchCase"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:D.MatSuffix,selector:"[matSuffix]"}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:st,decorators:[{type:o,args:[{selector:"tb-credentials-config",templateUrl:"./credentials-config.component.html",styleUrls:[],providers:[{provide:S,useExisting:n((()=>st)),multi:!0},{provide:A,useExisting:n((()=>st)),multi:!0}]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]},propDecorators:{required:[{type:l}],disableCertPemCredentials:[{type:l}],passwordFieldRquired:[{type:l}]}});class mt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.subscriptions=[]}configForm(){return this.mqttConfigForm}onConfigurationSet(e){this.mqttConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[k.required]],host:[e?e.host:null,[k.required]],port:[e?e.port:null,[k.required,k.min(1),k.max(65535)]],connectTimeoutSec:[e?e.connectTimeoutSec:null,[k.required,k.min(1),k.max(200)]],clientId:[e?e.clientId:null,[]],appendClientIdSuffix:[{value:!!e&&e.appendClientIdSuffix,disabled:!(e&&W(e.clientId))},[]],cleanSession:[!!e&&e.cleanSession,[]],ssl:[!!e&&e.ssl,[]],credentials:[e?e.credentials:null,[]]}),this.subscriptions.push(this.mqttConfigForm.get("clientId").valueChanges.subscribe((e=>{W(e)?this.mqttConfigForm.get("appendClientIdSuffix").enable({emitEvent:!1}):this.mqttConfigForm.get("appendClientIdSuffix").disable({emitEvent:!1})})))}ngOnDestroy(){this.subscriptions.forEach((e=>e.unsubscribe()))}}e("MqttConfigComponent",mt),mt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:mt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),mt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:mt,selector:"tb-action-node-mqtt-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.topic-pattern\n \n \n {{ \'tb.rulenode.topic-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.host\n \n \n {{ \'tb.rulenode.host-required\' | translate }}\n \n \n \n tb.rulenode.port\n \n \n {{ \'tb.rulenode.port-required\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n \n tb.rulenode.connect-timeout\n \n \n {{ \'tb.rulenode.connect-timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.connect-timeout-range\' | translate }}\n \n \n {{ \'tb.rulenode.connect-timeout-range\' | translate }}\n \n \n
\n \n tb.rulenode.client-id\n \n \n
tb.rulenode.client-id-hint
\n \n {{ \'tb.rulenode.append-client-id-suffix\' | translate }}\n \n
{{ "tb.rulenode.client-id-suffix-hint" | translate }}
\n \n {{ \'tb.rulenode.clean-session\' | translate }}\n \n \n {{ \'tb.rulenode.enable-ssl\' | translate }}\n \n \n
\n',styles:[":host .tb-mqtt-credentials-panel-group{margin:0 6px}:host .tb-hint.client-id{margin-top:-1.25em;max-width:-moz-fit-content;max-width:fit-content}:host mat-checkbox{padding-bottom:16px}\n"],components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:st,selector:"tb-credentials-config",inputs:["required","disableCertPemCredentials","passwordFieldRquired"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:E.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:mt,decorators:[{type:o,args:[{selector:"tb-action-node-mqtt-config",templateUrl:"./mqtt-config.component.html",styleUrls:["./mqtt-config.component.scss"]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class ut extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.msgCountConfigForm}onConfigurationSet(e){this.msgCountConfigForm=this.fb.group({interval:[e?e.interval:null,[k.required,k.min(1)]],telemetryPrefix:[e?e.telemetryPrefix:null,[k.required]]})}}e("MsgCountConfigComponent",ut),ut.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ut,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),ut.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:ut,selector:"tb-action-node-msg-count-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.interval-seconds\n \n \n {{ \'tb.rulenode.interval-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-interval-seconds-message\' | translate }}\n \n \n \n tb.rulenode.output-timeseries-key-prefix\n \n \n {{ \'tb.rulenode.output-timeseries-key-prefix-required\' | translate }}\n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ut,decorators:[{type:o,args:[{selector:"tb-action-node-msg-count-config",templateUrl:"./msg-count-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class pt extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.msgDelayConfigForm}onConfigurationSet(e){this.msgDelayConfigForm=this.fb.group({useMetadataPeriodInSecondsPatterns:[!!e&&e.useMetadataPeriodInSecondsPatterns,[]],periodInSeconds:[e?e.periodInSeconds:null,[]],periodInSecondsPattern:[e?e.periodInSecondsPattern:null,[]],maxPendingMsgs:[e?e.maxPendingMsgs:null,[k.required,k.min(1),k.max(1e5)]]})}validatorTriggers(){return["useMetadataPeriodInSecondsPatterns"]}updateValidators(e){this.msgDelayConfigForm.get("useMetadataPeriodInSecondsPatterns").value?(this.msgDelayConfigForm.get("periodInSecondsPattern").setValidators([k.required]),this.msgDelayConfigForm.get("periodInSeconds").setValidators([])):(this.msgDelayConfigForm.get("periodInSecondsPattern").setValidators([]),this.msgDelayConfigForm.get("periodInSeconds").setValidators([k.required,k.min(0)])),this.msgDelayConfigForm.get("periodInSecondsPattern").updateValueAndValidity({emitEvent:e}),this.msgDelayConfigForm.get("periodInSeconds").updateValueAndValidity({emitEvent:e})}}e("MsgDelayConfigComponent",pt),pt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:pt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),pt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:pt,selector:"tb-action-node-msg-delay-config",usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'tb.rulenode.use-metadata-period-in-seconds-patterns\' | translate }}\n \n
tb.rulenode.use-metadata-period-in-seconds-patterns-hint
\n \n tb.rulenode.period-seconds\n \n \n {{ \'tb.rulenode.period-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-period-0-seconds-message\' | translate }}\n \n \n \n \n tb.rulenode.period-in-seconds-pattern\n \n \n {{ \'tb.rulenode.period-in-seconds-pattern-required\' | translate }}\n \n \n \n \n \n tb.rulenode.max-pending-messages\n \n \n {{ \'tb.rulenode.max-pending-messages-required\' | translate }}\n \n \n {{ \'tb.rulenode.max-pending-messages-range\' | translate }}\n \n \n {{ \'tb.rulenode.max-pending-messages-range\' | translate }}\n \n \n
\n',components:[{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatLabel,selector:"mat-label"},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]},{type:q.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:pt,decorators:[{type:o,args:[{selector:"tb-action-node-msg-delay-config",templateUrl:"./msg-delay-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class dt extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.pubSubConfigForm}onConfigurationSet(e){this.pubSubConfigForm=this.fb.group({projectId:[e?e.projectId:null,[k.required]],topicName:[e?e.topicName:null,[k.required]],serviceAccountKey:[e?e.serviceAccountKey:null,[k.required]],serviceAccountKeyFileName:[e?e.serviceAccountKeyFileName:null,[k.required]],messageAttributes:[e?e.messageAttributes:null,[]]})}}e("PubSubConfigComponent",dt),dt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:dt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),dt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:dt,selector:"tb-action-node-pub-sub-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.gcp-project-id\n \n \n {{ \'tb.rulenode.gcp-project-id-required\' | translate }}\n \n \n \n tb.rulenode.pubsub-topic-name\n \n \n {{ \'tb.rulenode.pubsub-topic-name-required\' | translate }}\n \n \n \n \n \n
\n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:z.FileInputComponent,selector:"tb-file-input",inputs:["label","accept","noFileText","inputId","allowedExtensions","dropLabel","contentConvertFunction","required","requiredAsError","disabled","existingFileName","readAsBinary","workFromFileObj","multipleFile"],outputs:["fileNameChanged"]},{type:nt,selector:"tb-kv-map-config",inputs:["disabled","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:dt,decorators:[{type:o,args:[{selector:"tb-action-node-pub-sub-config",templateUrl:"./pubsub-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class ft extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.attributeScopes=Object.keys(m),this.telemetryTypeTranslationsMap=u}configForm(){return this.pushToCloudConfigForm}onConfigurationSet(e){this.pushToCloudConfigForm=this.fb.group({scope:[e?e.scope:null,[k.required]]})}}e("PushToCloudConfigComponent",ft),ft.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ft,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),ft.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:ft,selector:"tb-action-node-push-to-cloud-config",usesInheritance:!0,ngImport:t,template:'
\n \n attribute.attributes-scope\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ft,decorators:[{type:o,args:[{selector:"tb-action-node-push-to-cloud-config",templateUrl:"./push-to-cloud-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class ct extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.attributeScopes=Object.keys(m),this.telemetryTypeTranslationsMap=u}configForm(){return this.pushToEdgeConfigForm}onConfigurationSet(e){this.pushToEdgeConfigForm=this.fb.group({scope:[e?e.scope:null,[k.required]]})}}e("PushToEdgeConfigComponent",ct),ct.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ct,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),ct.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:ct,selector:"tb-action-node-push-to-edge-config",usesInheritance:!0,ngImport:t,template:'
\n \n attribute.attributes-scope\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ct,decorators:[{type:o,args:[{selector:"tb-action-node-push-to-edge-config",templateUrl:"./push-to-edge-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class gt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.messageProperties=[null,"BASIC","TEXT_PLAIN","MINIMAL_BASIC","MINIMAL_PERSISTENT_BASIC","PERSISTENT_BASIC","PERSISTENT_TEXT_PLAIN"]}configForm(){return this.rabbitMqConfigForm}onConfigurationSet(e){this.rabbitMqConfigForm=this.fb.group({exchangeNamePattern:[e?e.exchangeNamePattern:null,[]],routingKeyPattern:[e?e.routingKeyPattern:null,[]],messageProperties:[e?e.messageProperties:null,[]],host:[e?e.host:null,[k.required]],port:[e?e.port:null,[k.required,k.min(1),k.max(65535)]],virtualHost:[e?e.virtualHost:null,[]],username:[e?e.username:null,[]],password:[e?e.password:null,[]],automaticRecoveryEnabled:[!!e&&e.automaticRecoveryEnabled,[]],connectionTimeout:[e?e.connectionTimeout:null,[k.min(0)]],handshakeTimeout:[e?e.handshakeTimeout:null,[k.min(0)]],clientProperties:[e?e.clientProperties:null,[]]})}}e("RabbitMqConfigComponent",gt),gt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:gt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),gt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:gt,selector:"tb-action-node-rabbit-mq-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.exchange-name-pattern\n \n \n \n tb.rulenode.routing-key-pattern\n \n \n \n tb.rulenode.message-properties\n \n \n {{ property }}\n \n \n \n
\n \n tb.rulenode.host\n \n \n {{ \'tb.rulenode.host-required\' | translate }}\n \n \n \n tb.rulenode.port\n \n \n {{ \'tb.rulenode.port-required\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n
\n \n tb.rulenode.virtual-host\n \n \n \n tb.rulenode.username\n \n \n \n tb.rulenode.password\n \n \n \n \n {{ \'tb.rulenode.automatic-recovery\' | translate }}\n \n \n tb.rulenode.connection-timeout-ms\n \n \n {{ \'tb.rulenode.min-connection-timeout-ms-message\' | translate }}\n \n \n \n tb.rulenode.handshake-timeout-ms\n \n \n {{ \'tb.rulenode.min-handshake-timeout-ms-message\' | translate }}\n \n \n \n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:j.TogglePasswordComponent,selector:"tb-toggle-password"},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:nt,selector:"tb-kv-map-config",inputs:["disabled","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:E.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{type:D.MatSuffix,selector:"[matSuffix]"}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:gt,decorators:[{type:o,args:[{selector:"tb-action-node-rabbit-mq-config",templateUrl:"./rabbit-mq-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class xt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.proxySchemes=["http","https"],this.httpRequestTypes=Object.keys(Qe)}configForm(){return this.restApiCallConfigForm}onConfigurationSet(e){this.restApiCallConfigForm=this.fb.group({restEndpointUrlPattern:[e?e.restEndpointUrlPattern:null,[k.required]],requestMethod:[e?e.requestMethod:null,[k.required]],useSimpleClientHttpFactory:[!!e&&e.useSimpleClientHttpFactory,[]],ignoreRequestBody:[!!e&&e.ignoreRequestBody,[]],enableProxy:[!!e&&e.enableProxy,[]],useSystemProxyProperties:[!!e&&e.enableProxy,[]],proxyScheme:[e?e.proxyHost:null,[]],proxyHost:[e?e.proxyHost:null,[]],proxyPort:[e?e.proxyPort:null,[]],proxyUser:[e?e.proxyUser:null,[]],proxyPassword:[e?e.proxyPassword:null,[]],readTimeoutMs:[e?e.readTimeoutMs:null,[]],maxParallelRequestsCount:[e?e.maxParallelRequestsCount:null,[k.min(0)]],headers:[e?e.headers:null,[]],useRedisQueueForMsgPersistence:[!!e&&e.useRedisQueueForMsgPersistence,[]],trimQueue:[!!e&&e.trimQueue,[]],maxQueueSize:[e?e.maxQueueSize:null,[]],credentials:[e?e.credentials:null,[]]})}validatorTriggers(){return["useSimpleClientHttpFactory","useRedisQueueForMsgPersistence","enableProxy","useSystemProxyProperties"]}updateValidators(e){const t=this.restApiCallConfigForm.get("useSimpleClientHttpFactory").value,o=this.restApiCallConfigForm.get("useRedisQueueForMsgPersistence").value,r=this.restApiCallConfigForm.get("enableProxy").value,a=this.restApiCallConfigForm.get("useSystemProxyProperties").value;r&&!a?(this.restApiCallConfigForm.get("proxyHost").setValidators(r?[k.required]:[]),this.restApiCallConfigForm.get("proxyPort").setValidators(r?[k.required,k.min(1),k.max(65535)]:[])):(this.restApiCallConfigForm.get("proxyHost").setValidators([]),this.restApiCallConfigForm.get("proxyPort").setValidators([]),t?this.restApiCallConfigForm.get("readTimeoutMs").setValidators([]):this.restApiCallConfigForm.get("readTimeoutMs").setValidators([k.min(0)])),o?this.restApiCallConfigForm.get("maxQueueSize").setValidators([k.min(0)]):this.restApiCallConfigForm.get("maxQueueSize").setValidators([]),this.restApiCallConfigForm.get("readTimeoutMs").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("maxQueueSize").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("proxyHost").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("proxyPort").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("credentials").updateValueAndValidity({emitEvent:e})}}e("RestApiCallConfigComponent",xt),xt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:xt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),xt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:xt,selector:"tb-action-node-rest-api-call-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.endpoint-url-pattern\n \n \n {{ \'tb.rulenode.endpoint-url-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.request-method\n \n \n {{ requestType }}\n \n \n \n \n {{ \'tb.rulenode.enable-proxy\' | translate }}\n \n \n {{ \'tb.rulenode.use-simple-client-http-factory\' | translate }}\n \n \n {{ \'tb.rulenode.ignore-request-body\' | translate }}\n \n
\n \n {{ \'tb.rulenode.use-system-proxy-properties\' | translate }}\n \n
\n
\n \n tb.rulenode.proxy-scheme\n \n \n {{ proxyScheme }}\n \n \n \n \n tb.rulenode.proxy-host\n \n \n {{ \'tb.rulenode.proxy-host-required\' | translate }}\n \n \n \n tb.rulenode.proxy-port\n \n \n {{ \'tb.rulenode.proxy-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.proxy-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.proxy-user\n \n \n \n tb.rulenode.proxy-password\n \n \n
\n
\n \n tb.rulenode.read-timeout\n \n tb.rulenode.read-timeout-hint\n \n \n tb.rulenode.max-parallel-requests-count\n \n tb.rulenode.max-parallel-requests-count-hint\n \n \n
\n \n \n \n {{ \'tb.rulenode.use-redis-queue\' | translate }}\n \n
\n \n {{ \'tb.rulenode.trim-redis-queue\' | translate }}\n \n \n tb.rulenode.redis-queue-max-size\n \n \n
\n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:nt,selector:"tb-kv-map-config",inputs:["disabled","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]},{type:st,selector:"tb-credentials-config",inputs:["required","disableCertPemCredentials","passwordFieldRquired"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:E.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:xt,decorators:[{type:o,args:[{selector:"tb-action-node-rest-api-call-config",templateUrl:"./rest-api-call-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class yt extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.rpcReplyConfigForm}onConfigurationSet(e){this.rpcReplyConfigForm=this.fb.group({requestIdMetaDataAttribute:[e?e.requestIdMetaDataAttribute:null,[]]})}}e("RpcReplyConfigComponent",yt),yt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:yt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),yt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:yt,selector:"tb-action-node-rpc-reply-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.request-id-metadata-attribute\n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:yt,decorators:[{type:o,args:[{selector:"tb-action-node-rpc-reply-config",templateUrl:"./rpc-reply-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class bt extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.rpcRequestConfigForm}onConfigurationSet(e){this.rpcRequestConfigForm=this.fb.group({timeoutInSeconds:[e?e.timeoutInSeconds:null,[k.required,k.min(0)]]})}}e("RpcRequestConfigComponent",bt),bt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:bt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),bt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:bt,selector:"tb-action-node-rpc-request-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.timeout-sec\n \n \n {{ \'tb.rulenode.timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-timeout-message\' | translate }}\n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:bt,decorators:[{type:o,args:[{selector:"tb-action-node-rpc-request-config",templateUrl:"./rpc-request-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class ht extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.saveToCustomTableConfigForm}onConfigurationSet(e){this.saveToCustomTableConfigForm=this.fb.group({tableName:[e?e.tableName:null,[k.required,k.pattern(/.*\S.*/)]],fieldsMapping:[e?e.fieldsMapping:null,[k.required]]})}prepareOutputConfig(e){return e.tableName=e.tableName.trim(),e}}e("SaveToCustomTableConfigComponent",ht),ht.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ht,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),ht.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:ht,selector:"tb-action-node-custom-table-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.custom-table-name\n \n \n {{ \'tb.rulenode.custom-table-name-required\' | translate }}\n \n tb.rulenode.custom-table-hint\n \n \n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:nt,selector:"tb-kv-map-config",inputs:["disabled","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ht,decorators:[{type:o,args:[{selector:"tb-action-node-custom-table-config",templateUrl:"./save-to-custom-table-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Ct extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.smtpProtocols=["smtp","smtps"],this.tlsVersions=["TLSv1","TLSv1.1","TLSv1.2","TLSv1.3"]}configForm(){return this.sendEmailConfigForm}onConfigurationSet(e){this.sendEmailConfigForm=this.fb.group({useSystemSmtpSettings:[!!e&&e.useSystemSmtpSettings,[]],smtpProtocol:[e?e.smtpProtocol:null,[]],smtpHost:[e?e.smtpHost:null,[]],smtpPort:[e?e.smtpPort:null,[]],timeout:[e?e.timeout:null,[]],enableTls:[!!e&&e.enableTls,[]],tlsVersion:[e?e.tlsVersion:null,[]],enableProxy:[!!e&&e.enableProxy,[]],proxyHost:[e?e.proxyHost:null,[]],proxyPort:[e?e.proxyPort:null,[]],proxyUser:[e?e.proxyUser:null,[]],proxyPassword:[e?e.proxyPassword:null,[]],username:[e?e.username:null,[]],password:[e?e.password:null,[]]})}validatorTriggers(){return["useSystemSmtpSettings","enableProxy"]}updateValidators(e){const t=this.sendEmailConfigForm.get("useSystemSmtpSettings").value,o=this.sendEmailConfigForm.get("enableProxy").value;t?(this.sendEmailConfigForm.get("smtpProtocol").setValidators([]),this.sendEmailConfigForm.get("smtpHost").setValidators([]),this.sendEmailConfigForm.get("smtpPort").setValidators([]),this.sendEmailConfigForm.get("timeout").setValidators([]),this.sendEmailConfigForm.get("proxyHost").setValidators([]),this.sendEmailConfigForm.get("proxyPort").setValidators([])):(this.sendEmailConfigForm.get("smtpProtocol").setValidators([k.required]),this.sendEmailConfigForm.get("smtpHost").setValidators([k.required]),this.sendEmailConfigForm.get("smtpPort").setValidators([k.required,k.min(1),k.max(65535)]),this.sendEmailConfigForm.get("timeout").setValidators([k.required,k.min(0)]),this.sendEmailConfigForm.get("proxyHost").setValidators(o?[k.required]:[]),this.sendEmailConfigForm.get("proxyPort").setValidators(o?[k.required,k.min(1),k.max(65535)]:[])),this.sendEmailConfigForm.get("smtpProtocol").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("smtpHost").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("smtpPort").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("timeout").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("proxyHost").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("proxyPort").updateValueAndValidity({emitEvent:e})}}e("SendEmailConfigComponent",Ct),Ct.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ct,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Ct.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Ct,selector:"tb-action-node-send-email-config",usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'tb.rulenode.use-system-smtp-settings\' | translate }}\n \n
\n \n tb.rulenode.smtp-protocol\n \n \n {{ smtpProtocol.toUpperCase() }}\n \n \n \n
\n \n tb.rulenode.smtp-host\n \n \n {{ \'tb.rulenode.smtp-host-required\' | translate }}\n \n \n \n tb.rulenode.smtp-port\n \n \n {{ \'tb.rulenode.smtp-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.smtp-port-range\' | translate }}\n \n \n {{ \'tb.rulenode.smtp-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.timeout-msec\n \n \n {{ \'tb.rulenode.timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-timeout-msec-message\' | translate }}\n \n \n \n {{ \'tb.rulenode.enable-tls\' | translate }}\n \n \n tb.rulenode.tls-version\n \n \n {{ tlsVersion }}\n \n \n \n \n {{ \'tb.rulenode.enable-proxy\' | translate }}\n \n
\n
\n \n tb.rulenode.proxy-host\n \n \n {{ \'tb.rulenode.proxy-host-required\' | translate }}\n \n \n \n tb.rulenode.proxy-port\n \n \n {{ \'tb.rulenode.proxy-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.proxy-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.proxy-user\n \n \n \n tb.rulenode.proxy-password\n \n \n
\n \n tb.rulenode.username\n \n \n \n tb.rulenode.password\n \n \n \n
\n
\n',components:[{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:ce.TbCheckboxComponent,selector:"tb-checkbox",inputs:["disabled","trueValue","falseValue"],outputs:["valueChange"]},{type:j.TogglePasswordComponent,selector:"tb-toggle-password"}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:E.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{type:D.MatSuffix,selector:"[matSuffix]"}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ct,decorators:[{type:o,args:[{selector:"tb-action-node-send-email-config",templateUrl:"./send-email-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Ft extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.sendSmsConfigForm}onConfigurationSet(e){this.sendSmsConfigForm=this.fb.group({numbersToTemplate:[e?e.numbersToTemplate:null,[k.required]],smsMessageTemplate:[e?e.smsMessageTemplate:null,[k.required]],useSystemSmsSettings:[!!e&&e.useSystemSmsSettings,[]],smsProviderConfiguration:[e?e.smsProviderConfiguration:null,[]]})}validatorTriggers(){return["useSystemSmsSettings"]}updateValidators(e){this.sendSmsConfigForm.get("useSystemSmsSettings").value?this.sendSmsConfigForm.get("smsProviderConfiguration").setValidators([]):this.sendSmsConfigForm.get("smsProviderConfiguration").setValidators([k.required]),this.sendSmsConfigForm.get("smsProviderConfiguration").updateValueAndValidity({emitEvent:e})}}e("SendSmsConfigComponent",Ft),Ft.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ft,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Ft.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Ft,selector:"tb-action-node-send-sms-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.numbers-to-template\n \n \n {{ \'tb.rulenode.numbers-to-template-required\' | translate }}\n \n \n \n \n tb.rulenode.sms-message-template\n \n \n {{ \'tb.rulenode.sms-message-template-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.use-system-sms-settings\' | translate }}\n \n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:ge.SmsProviderConfigurationComponent,selector:"tb-sms-provider-configuration",inputs:["required","disabled"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ft,decorators:[{type:o,args:[{selector:"tb-action-node-send-sms-config",templateUrl:"./send-sms-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Lt extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.snsConfigForm}onConfigurationSet(e){this.snsConfigForm=this.fb.group({topicArnPattern:[e?e.topicArnPattern:null,[k.required]],accessKeyId:[e?e.accessKeyId:null,[k.required]],secretAccessKey:[e?e.secretAccessKey:null,[k.required]],region:[e?e.region:null,[k.required]]})}}e("SnsConfigComponent",Lt),Lt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Lt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Lt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Lt,selector:"tb-action-node-sns-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.topic-arn-pattern\n \n \n {{ \'tb.rulenode.topic-arn-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.aws-access-key-id\n \n \n {{ \'tb.rulenode.aws-access-key-id-required\' | translate }}\n \n \n \n tb.rulenode.aws-secret-access-key\n \n \n {{ \'tb.rulenode.aws-secret-access-key-required\' | translate }}\n \n \n \n tb.rulenode.aws-region\n \n \n {{ \'tb.rulenode.aws-region-required\' | translate }}\n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Lt,decorators:[{type:o,args:[{selector:"tb-action-node-sns-config",templateUrl:"./sns-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class vt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.sqsQueueType=Ue,this.sqsQueueTypes=Object.keys(Ue),this.sqsQueueTypeTranslationsMap=Ke}configForm(){return this.sqsConfigForm}onConfigurationSet(e){this.sqsConfigForm=this.fb.group({queueType:[e?e.queueType:null,[k.required]],queueUrlPattern:[e?e.queueUrlPattern:null,[k.required]],delaySeconds:[e?e.delaySeconds:null,[k.min(0),k.max(900)]],messageAttributes:[e?e.messageAttributes:null,[]],accessKeyId:[e?e.accessKeyId:null,[k.required]],secretAccessKey:[e?e.secretAccessKey:null,[k.required]],region:[e?e.region:null,[k.required]]})}}e("SqsConfigComponent",vt),vt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:vt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),vt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:vt,selector:"tb-action-node-sqs-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.queue-type\n \n \n {{ sqsQueueTypeTranslationsMap.get(type) | translate }}\n \n \n \n \n tb.rulenode.queue-url-pattern\n \n \n {{ \'tb.rulenode.queue-url-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.delay-seconds\n \n \n {{ \'tb.rulenode.min-delay-seconds-message\' | translate }}\n \n \n {{ \'tb.rulenode.max-delay-seconds-message\' | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.aws-access-key-id\n \n \n {{ \'tb.rulenode.aws-access-key-id-required\' | translate }}\n \n \n \n tb.rulenode.aws-secret-access-key\n \n \n {{ \'tb.rulenode.aws-secret-access-key-required\' | translate }}\n \n \n \n tb.rulenode.aws-region\n \n \n {{ \'tb.rulenode.aws-region-required\' | translate }}\n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:nt,selector:"tb-kv-map-config",inputs:["disabled","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:vt,decorators:[{type:o,args:[{selector:"tb-action-node-sqs-config",templateUrl:"./sqs-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class It extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.timeseriesConfigForm}onConfigurationSet(e){this.timeseriesConfigForm=this.fb.group({defaultTTL:[e?e.defaultTTL:null,[k.required,k.min(0)]],skipLatestPersistence:[!!e&&e.skipLatestPersistence,[]],useServerTs:[!!e&&e.useServerTs,[]]})}}e("TimeseriesConfigComponent",It),It.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:It,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),It.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:It,selector:"tb-action-node-timeseries-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.default-ttl\n \n \n {{ \'tb.rulenode.default-ttl-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-default-ttl-message\' | translate }}\n \n \n \n {{ \'tb.rulenode.skip-latest-persistence\' | translate }}\n \n \n {{ \'tb.rulenode.use-server-ts\' | translate }}\n \n
tb.rulenode.use-server-ts-hint
\n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:It,decorators:[{type:o,args:[{selector:"tb-action-node-timeseries-config",templateUrl:"./timeseries-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Nt extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.unassignCustomerConfigForm}onConfigurationSet(e){this.unassignCustomerConfigForm=this.fb.group({customerNamePattern:[e?e.customerNamePattern:null,[k.required,k.pattern(/.*\S.*/)]],customerCacheExpiration:[e?e.customerCacheExpiration:null,[k.required,k.min(0)]]})}prepareOutputConfig(e){return e.customerNamePattern=e.customerNamePattern.trim(),e}}e("UnassignCustomerConfigComponent",Nt),Nt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Nt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Nt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Nt,selector:"tb-action-node-un-assign-to-customer-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.customer-name-pattern\n \n \n {{ \'tb.rulenode.customer-name-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.customer-cache-expiration\n \n \n {{ \'tb.rulenode.customer-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.customer-cache-expiration-range\' | translate }}\n \n tb.rulenode.customer-cache-expiration-hint\n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Nt,decorators:[{type:o,args:[{selector:"tb-action-node-un-assign-to-customer-config",templateUrl:"./unassign-customer-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Tt extends y{constructor(e,t){super(e),this.store=e,this.fb=t,this.directionTypes=Object.keys(c),this.directionTypeTranslations=g,this.entityType=x,this.propagateChange=null}get required(){return this.requiredValue}set required(e){this.requiredValue=le(e)}ngOnInit(){this.deviceRelationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[k.required]],maxLevel:[null,[]],relationType:[null],deviceTypes:[null,[k.required]]}),this.deviceRelationsQueryFormGroup.valueChanges.subscribe((e=>{this.deviceRelationsQueryFormGroup.valid?this.propagateChange(e):this.propagateChange(null)}))}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}setDisabledState(e){this.disabled=e,this.disabled?this.deviceRelationsQueryFormGroup.disable({emitEvent:!1}):this.deviceRelationsQueryFormGroup.enable({emitEvent:!1})}writeValue(e){this.deviceRelationsQueryFormGroup.reset(e,{emitEvent:!1})}}e("DeviceRelationsQueryConfigComponent",Tt),Tt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Tt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Tt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Tt,selector:"tb-device-relations-query-config",inputs:{disabled:"disabled",required:"required"},providers:[{provide:S,useExisting:n((()=>Tt)),multi:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'alias.last-level-relation\' | translate }}\n \n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n
\n
relation.relation-type
\n \n \n
device.device-types
\n \n \n
\n',components:[{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:ye.RelationTypeAutocompleteComponent,selector:"tb-relation-type-autocomplete",inputs:["required","disabled"]},{type:be.EntitySubTypeListComponent,selector:"tb-entity-subtype-list",inputs:["required","disabled","entityType"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:E.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Tt,decorators:[{type:o,args:[{selector:"tb-device-relations-query-config",templateUrl:"./device-relations-query-config.component.html",styleUrls:[],providers:[{provide:S,useExisting:n((()=>Tt)),multi:!0}]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]},propDecorators:{disabled:[{type:l}],required:[{type:l}]}});class qt extends y{constructor(e,t){super(e),this.store=e,this.fb=t,this.directionTypes=Object.keys(c),this.directionTypeTranslations=g,this.propagateChange=null}get required(){return this.requiredValue}set required(e){this.requiredValue=le(e)}ngOnInit(){this.relationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[k.required]],maxLevel:[null,[]],filters:[null]}),this.relationsQueryFormGroup.valueChanges.subscribe((e=>{this.relationsQueryFormGroup.valid?this.propagateChange(e):this.propagateChange(null)}))}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}setDisabledState(e){this.disabled=e,this.disabled?this.relationsQueryFormGroup.disable({emitEvent:!1}):this.relationsQueryFormGroup.enable({emitEvent:!1})}writeValue(e){this.relationsQueryFormGroup.reset(e||{},{emitEvent:!1})}}e("RelationsQueryConfigComponent",qt),qt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:qt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),qt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:qt,selector:"tb-relations-query-config",inputs:{disabled:"disabled",required:"required"},providers:[{provide:S,useExisting:n((()=>qt)),multi:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'alias.last-level-relation\' | translate }}\n \n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n
\n
relation.relation-filters
\n \n
\n',components:[{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:he.RelationFiltersComponent,selector:"tb-relation-filters",inputs:["disabled","allowedEntityTypes"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:E.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:qt,decorators:[{type:o,args:[{selector:"tb-relations-query-config",templateUrl:"./relations-query-config.component.html",styleUrls:[],providers:[{provide:S,useExisting:n((()=>qt)),multi:!0}]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]},propDecorators:{disabled:[{type:l}],required:[{type:l}]}});class kt extends y{constructor(e,t,o,r){super(e),this.store=e,this.translate=t,this.truncate=o,this.fb=r,this.placeholder="tb.rulenode.message-type",this.separatorKeysCodes=[Z,X,ee],this.messageTypes=[],this.messageTypesList=[],this.searchText="",this.propagateChange=e=>{},this.messageTypeConfigForm=this.fb.group({messageType:[null]});for(const e of Object.keys(b))this.messageTypesList.push({name:h.get(b[e]),value:e})}get required(){return this.requiredValue}set required(e){this.requiredValue=le(e)}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}ngOnInit(){this.filteredMessageTypes=this.messageTypeConfigForm.get("messageType").valueChanges.pipe(ue(""),pe((e=>e||"")),de((e=>this.fetchMessageTypes(e))),fe())}ngAfterViewInit(){}setDisabledState(e){this.disabled=e,this.disabled?this.messageTypeConfigForm.disable({emitEvent:!1}):this.messageTypeConfigForm.enable({emitEvent:!1})}writeValue(e){this.searchText="",this.messageTypes.length=0,e&&e.forEach((e=>{const t=this.messageTypesList.find((t=>t.value===e));t?this.messageTypes.push({name:t.name,value:t.value}):this.messageTypes.push({name:e,value:e})}))}displayMessageTypeFn(e){return e?e.name:void 0}textIsNotEmpty(e){return!!(e&&null!=e&&e.length>0)}createMessageType(e,t){e.preventDefault(),this.transformMessageType(t)}add(e){this.transformMessageType(e.value)}fetchMessageTypes(e){if(this.searchText=e,this.searchText&&this.searchText.length){const e=this.searchText.toUpperCase();return Ce(this.messageTypesList.filter((t=>t.name.toUpperCase().includes(e))))}return Ce(this.messageTypesList)}transformMessageType(e){if((e||"").trim()){let t=null;const o=e.trim(),r=this.messageTypesList.find((e=>e.name===o));t=r?{name:r.name,value:r.value}:{name:o,value:o},t&&this.addMessageType(t)}this.clear("")}remove(e){const t=this.messageTypes.indexOf(e);t>=0&&(this.messageTypes.splice(t,1),this.updateModel())}selected(e){this.addMessageType(e.option.value),this.clear("")}addMessageType(e){-1===this.messageTypes.findIndex((t=>t.value===e.value))&&(this.messageTypes.push(e),this.updateModel())}onFocus(){this.messageTypeConfigForm.get("messageType").updateValueAndValidity({onlySelf:!0,emitEvent:!0})}clear(e=""){this.messageTypeInput.nativeElement.value=e,this.messageTypeConfigForm.get("messageType").patchValue(null,{emitEvent:!0}),setTimeout((()=>{this.messageTypeInput.nativeElement.blur(),this.messageTypeInput.nativeElement.focus()}),0)}updateModel(){const e=this.messageTypes.map((e=>e.value));this.required?(this.chipList.errorState=!e.length,this.propagateChange(e.length>0?e:null)):(this.chipList.errorState=!1,this.propagateChange(e))}}e("MessageTypesConfigComponent",kt),kt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:kt,deps:[{token:T.Store},{token:P.TranslateService},{token:C.TruncatePipe},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),kt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:kt,selector:"tb-message-types-config",inputs:{required:"required",label:"label",placeholder:"placeholder",disabled:"disabled"},providers:[{provide:S,useExisting:n((()=>kt)),multi:!0}],viewQueries:[{propertyName:"chipList",first:!0,predicate:["chipList"],descendants:!0},{propertyName:"matAutocomplete",first:!0,predicate:["messageTypeAutocomplete"],descendants:!0},{propertyName:"messageTypeInput",first:!0,predicate:["messageTypeInput"],descendants:!0}],usesInheritance:!0,ngImport:t,template:'\n {{ label }}\n \n \n {{messageType.name}}\n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-message-types-found\n
\n \n \n {{ translate.get(\'tb.rulenode.no-message-type-matching\',\n {messageType: truncate.transform(searchText, true, 6, '...')}) | async }}\n \n \n \n tb.rulenode.create-new-message-type\n \n
\n
\n
\n \n {{ \'tb.rulenode.message-types-required\' | translate }}\n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:te.MatChipList,selector:"mat-chip-list",inputs:["aria-orientation","multiple","compareWith","value","required","placeholder","disabled","selectable","tabIndex","errorStateMatcher"],outputs:["change","valueChange"],exportAs:["matChipList"]},{type:oe.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{type:Fe.MatAutocomplete,selector:"mat-autocomplete",inputs:["disableRipple"],exportAs:["matAutocomplete"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]}],directives:[{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:te.MatChip,selector:"mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]",inputs:["color","disableRipple","tabIndex","selected","value","selectable","disabled","removable"],outputs:["selectionChange","destroyed","removed"],exportAs:["matChip"]},{type:te.MatChipRemove,selector:"[matChipRemove]"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:Fe.MatAutocompleteTrigger,selector:"input[matAutocomplete], textarea[matAutocomplete]",exportAs:["matAutocompleteTrigger"]},{type:te.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputSeparatorKeyCodes","placeholder","id","matChipInputFor","matChipInputAddOnBlur","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:Fe.MatAutocompleteOrigin,selector:"[matAutocompleteOrigin]",exportAs:["matAutocompleteOrigin"]},{type:D.MatError,selector:"mat-error",inputs:["id"]}],pipes:{translate:P.TranslatePipe,async:w.AsyncPipe,highlight:Le.HighlightPipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:kt,decorators:[{type:o,args:[{selector:"tb-message-types-config",templateUrl:"./message-types-config.component.html",styleUrls:[],providers:[{provide:S,useExisting:n((()=>kt)),multi:!0}]}]}],ctorParameters:function(){return[{type:T.Store},{type:P.TranslateService},{type:C.TruncatePipe},{type:q.FormBuilder}]},propDecorators:{required:[{type:l}],label:[{type:l}],placeholder:[{type:l}],disabled:[{type:l}],chipList:[{type:a,args:["chipList",{static:!1}]}],matAutocomplete:[{type:a,args:["messageTypeAutocomplete",{static:!1}]}],messageTypeInput:[{type:a,args:["messageTypeInput",{static:!1}]}]}});class Mt{}e("RulenodeCoreConfigCommonModule",Mt),Mt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Mt,deps:[],target:t.ɵɵFactoryTarget.NgModule}),Mt.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Mt,declarations:[nt,Tt,qt,kt,st,Te],imports:[O,F,xe],exports:[nt,Tt,qt,kt,st,Te]}),Mt.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Mt,imports:[[O,F,xe]]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Mt,decorators:[{type:i,args:[{declarations:[nt,Tt,qt,kt,st,Te],imports:[O,F,xe],exports:[nt,Tt,qt,kt,st,Te]}]}]});class St{}e("RuleNodeCoreConfigActionModule",St),St.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:St,deps:[],target:t.ɵɵFactoryTarget.NgModule}),St.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:St,declarations:[ke,It,bt,it,qe,Ze,Xe,et,pt,tt,rt,at,ut,yt,ht,Nt,Lt,vt,dt,lt,mt,gt,xt,Ct,Je,Ye,ot,Ft,ct,ft],imports:[O,F,xe,Mt],exports:[ke,It,bt,it,qe,Ze,Xe,et,pt,tt,rt,at,ut,yt,ht,Nt,Lt,vt,dt,lt,mt,gt,xt,Ct,Je,Ye,ot,Ft,ct,ft]}),St.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:St,imports:[[O,F,xe,Mt]]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:St,decorators:[{type:i,args:[{declarations:[ke,It,bt,it,qe,Ze,Xe,et,pt,tt,rt,at,ut,yt,ht,Nt,Lt,vt,dt,lt,mt,gt,xt,Ct,Je,Ye,ot,Ft,ct,ft],imports:[O,F,xe,Mt],exports:[ke,It,bt,it,qe,Ze,Xe,et,pt,tt,rt,at,ut,yt,ht,Nt,Lt,vt,dt,lt,mt,gt,xt,Ct,Je,Ye,ot,Ft,ct,ft]}]}]});class At extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.separatorKeysCodes=[Z,X,ee]}configForm(){return this.calculateDeltaConfigForm}onConfigurationSet(e){this.calculateDeltaConfigForm=this.fb.group({inputValueKey:[e?e.inputValueKey:null,[k.required]],outputValueKey:[e?e.outputValueKey:null,[k.required]],useCache:[e?e.useCache:null,[]],addPeriodBetweenMsgs:[!!e&&e.addPeriodBetweenMsgs,[]],periodValueKey:[e?e.periodValueKey:null,[]],round:[e?e.round:null,[k.min(0),k.max(15)]],tellFailureIfDeltaIsNegative:[e?e.tellFailureIfDeltaIsNegative:null,[]]})}updateValidators(e){this.calculateDeltaConfigForm.get("addPeriodBetweenMsgs").value?this.calculateDeltaConfigForm.get("periodValueKey").setValidators([k.required]):this.calculateDeltaConfigForm.get("periodValueKey").setValidators([]),this.calculateDeltaConfigForm.get("periodValueKey").updateValueAndValidity({emitEvent:e})}validatorTriggers(){return["addPeriodBetweenMsgs"]}}e("CalculateDeltaConfigComponent",At),At.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:At,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),At.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:At,selector:"tb-enrichment-node-calculate-delta-config",usesInheritance:!0,ngImport:t,template:'
\n
\n \n tb.rulenode.input-value-key\n \n \n {{ \'tb.rulenode.input-value-key-required\' | translate }}\n \n \n \n tb.rulenode.output-value-key\n \n \n {{ \'tb.rulenode.output-value-key-required\' | translate }}\n \n \n \n tb.rulenode.round\n \n \n {{ \'tb.rulenode.round-range\' | translate }}\n \n \n {{ \'tb.rulenode.round-range\' | translate }}\n \n \n
\n \n {{ \'tb.rulenode.use-cache\' | translate }}\n \n \n {{ \'tb.rulenode.tell-failure-if-delta-is-negative\' | translate }}\n \n \n {{ \'tb.rulenode.add-period-between-msgs\' | translate }}\n \n \n tb.rulenode.period-value-key\n \n \n {{ \'tb.rulenode.period-value-key-required\' | translate }}\n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:E.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:At,decorators:[{type:o,args:[{selector:"tb-enrichment-node-calculate-delta-config",templateUrl:"./calculate-delta-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Gt extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.customerAttributesConfigForm}onConfigurationSet(e){this.customerAttributesConfigForm=this.fb.group({telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[k.required]]})}}e("CustomerAttributesConfigComponent",Gt),Gt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Gt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Gt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Gt,selector:"tb-enrichment-node-customer-attributes-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n',components:[{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:nt,selector:"tb-kv-map-config",inputs:["disabled","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Gt,decorators:[{type:o,args:[{selector:"tb-enrichment-node-customer-attributes-config",templateUrl:"./customer-attributes-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Dt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.separatorKeysCodes=[Z,X,ee]}configForm(){return this.deviceAttributesConfigForm}onConfigurationSet(e){this.deviceAttributesConfigForm=this.fb.group({deviceRelationsQuery:[e?e.deviceRelationsQuery:null,[k.required]],tellFailureIfAbsent:[!!e&&e.tellFailureIfAbsent,[]],clientAttributeNames:[e?e.clientAttributeNames:null,[]],sharedAttributeNames:[e?e.sharedAttributeNames:null,[]],serverAttributeNames:[e?e.serverAttributeNames:null,[]],latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],getLatestValueWithTs:[!!e&&e.getLatestValueWithTs,[]]})}removeKey(e,t){const o=this.deviceAttributesConfigForm.get(t).value,r=o.indexOf(e);r>=0&&(o.splice(r,1),this.deviceAttributesConfigForm.get(t).setValue(o,{emitEvent:!0}))}addKey(e,t){const o=e.input;let r=e.value;if((r||"").trim()){r=r.trim();let e=this.deviceAttributesConfigForm.get(t).value;e&&-1!==e.indexOf(r)||(e||(e=[]),e.push(r),this.deviceAttributesConfigForm.get(t).setValue(e,{emitEvent:!0}))}o&&(o.value="")}}e("DeviceAttributesConfigComponent",Dt),Dt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Dt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Dt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Dt,selector:"tb-enrichment-node-device-attributes-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n \n \n {{ \'tb.rulenode.tell-failure-if-absent\' | translate }}\n \n
tb.rulenode.tell-failure-if-absent-hint
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n {{ \'tb.rulenode.get-latest-value-with-ts\' | translate }}\n \n
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}\n"],components:[{type:Tt,selector:"tb-device-relations-query-config",inputs:["disabled","required"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:te.MatChipList,selector:"mat-chip-list",inputs:["aria-orientation","multiple","compareWith","value","required","placeholder","disabled","selectable","tabIndex","errorStateMatcher"],outputs:["change","valueChange"],exportAs:["matChipList"]},{type:oe.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:D.MatLabel,selector:"mat-label"},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:te.MatChip,selector:"mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]",inputs:["color","disableRipple","tabIndex","selected","value","selectable","disabled","removable"],outputs:["selectionChange","destroyed","removed"],exportAs:["matChip"]},{type:te.MatChipRemove,selector:"[matChipRemove]"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:te.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputSeparatorKeyCodes","placeholder","id","matChipInputFor","matChipInputAddOnBlur","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Dt,decorators:[{type:o,args:[{selector:"tb-enrichment-node-device-attributes-config",templateUrl:"./device-attributes-config.component.html",styleUrls:["./device-attributes-config.component.scss"]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Vt extends s{constructor(e,t,o){super(e),this.store=e,this.translate=t,this.fb=o,this.entityDetailsTranslationsMap=we,this.entityDetailsList=[],this.searchText="",this.displayDetailsFn=this.displayDetails.bind(this);for(const e of Object.keys(Re))this.entityDetailsList.push(Re[e]);this.detailsFormControl=new G(""),this.filteredEntityDetails=this.detailsFormControl.valueChanges.pipe(ue(""),pe((e=>e||"")),de((e=>this.fetchEntityDetails(e))),fe())}ngOnInit(){super.ngOnInit()}configForm(){return this.entityDetailsConfigForm}prepareInputConfig(e){return this.searchText="",this.detailsFormControl.patchValue("",{emitEvent:!0}),e}onConfigurationSet(e){this.entityDetailsConfigForm=this.fb.group({detailsList:[e?e.detailsList:null,[k.required]],addToMetadata:[!!e&&e.addToMetadata,[]]})}displayDetails(e){return e?this.translate.instant(we.get(e)):void 0}fetchEntityDetails(e){if(this.searchText=e,this.searchText&&this.searchText.length){const e=this.searchText.toUpperCase();return Ce(this.entityDetailsList.filter((t=>this.translate.instant(we.get(Re[t])).toUpperCase().includes(e))))}return Ce(this.entityDetailsList)}detailsFieldSelected(e){this.addDetailsField(e.option.value),this.clear("")}removeDetailsField(e){const t=this.entityDetailsConfigForm.get("detailsList").value;if(t){const o=t.indexOf(e);o>=0&&(t.splice(o,1),this.entityDetailsConfigForm.get("detailsList").setValue(t))}}addDetailsField(e){let t=this.entityDetailsConfigForm.get("detailsList").value;t||(t=[]);-1===t.indexOf(e)&&(t.push(e),this.entityDetailsConfigForm.get("detailsList").setValue(t))}onEntityDetailsInputFocus(){this.detailsFormControl.updateValueAndValidity({onlySelf:!0,emitEvent:!0})}clear(e=""){this.detailsInput.nativeElement.value=e,this.detailsFormControl.patchValue(null,{emitEvent:!0}),setTimeout((()=>{this.detailsInput.nativeElement.blur(),this.detailsInput.nativeElement.focus()}),0)}}e("EntityDetailsConfigComponent",Vt),Vt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Vt,deps:[{token:T.Store},{token:P.TranslateService},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Vt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Vt,selector:"tb-enrichment-node-entity-details-config",viewQueries:[{propertyName:"detailsInput",first:!0,predicate:["detailsInput"],descendants:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.entity-details\n \n \n \n {{entityDetailsTranslationsMap.get(details) | translate}}\n \n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-entity-details-matching\n
\n
\n
\n
\n
\n \n \n {{ \'tb.rulenode.add-to-metadata\' | translate }}\n \n
tb.rulenode.add-to-metadata-hint
\n
\n',styles:[":host ::ng-deep mat-form-field.entity-fields-list .mat-form-field-wrapper{margin-bottom:-1.25em}\n"],components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:te.MatChipList,selector:"mat-chip-list",inputs:["aria-orientation","multiple","compareWith","value","required","placeholder","disabled","selectable","tabIndex","errorStateMatcher"],outputs:["change","valueChange"],exportAs:["matChipList"]},{type:oe.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{type:Fe.MatAutocomplete,selector:"mat-autocomplete",inputs:["disableRipple"],exportAs:["matAutocomplete"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:ie.TbErrorComponent,selector:"tb-error",inputs:["error"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:te.MatChip,selector:"mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]",inputs:["color","disableRipple","tabIndex","selected","value","selectable","disabled","removable"],outputs:["selectionChange","destroyed","removed"],exportAs:["matChip"]},{type:te.MatChipRemove,selector:"[matChipRemove]"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:Fe.MatAutocompleteTrigger,selector:"input[matAutocomplete], textarea[matAutocomplete]",exportAs:["matAutocompleteTrigger"]},{type:te.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputSeparatorKeyCodes","placeholder","id","matChipInputFor","matChipInputAddOnBlur","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]},{type:Fe.MatAutocompleteOrigin,selector:"[matAutocompleteOrigin]",exportAs:["matAutocompleteOrigin"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlDirective,selector:"[formControl]",inputs:["disabled","formControl","ngModel"],outputs:["ngModelChange"],exportAs:["ngForm"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]}],pipes:{translate:P.TranslatePipe,async:w.AsyncPipe,highlight:Le.HighlightPipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Vt,decorators:[{type:o,args:[{selector:"tb-enrichment-node-entity-details-config",templateUrl:"./entity-details-config.component.html",styleUrls:["./entity-details-config.component.scss"]}]}],ctorParameters:function(){return[{type:T.Store},{type:P.TranslateService},{type:q.FormBuilder}]},propDecorators:{detailsInput:[{type:a,args:["detailsInput",{static:!1}]}]}});class Et extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.separatorKeysCodes=[Z,X,ee],this.aggregationTypes=L,this.aggregations=Object.keys(L),this.aggregationTypesTranslations=v,this.fetchMode=Oe,this.fetchModes=Object.keys(Oe),this.samplingOrders=Object.keys(He),this.timeUnits=Object.values(De),this.timeUnitsTranslationMap=Ve}configForm(){return this.getTelemetryFromDatabaseConfigForm}onConfigurationSet(e){this.getTelemetryFromDatabaseConfigForm=this.fb.group({latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],aggregation:[e?e.aggregation:null,[k.required]],fetchMode:[e?e.fetchMode:null,[k.required]],orderBy:[e?e.orderBy:null,[]],limit:[e?e.limit:null,[]],useMetadataIntervalPatterns:[!!e&&e.useMetadataIntervalPatterns,[]],startInterval:[e?e.startInterval:null,[]],startIntervalTimeUnit:[e?e.startIntervalTimeUnit:null,[]],endInterval:[e?e.endInterval:null,[]],endIntervalTimeUnit:[e?e.endIntervalTimeUnit:null,[]],startIntervalPattern:[e?e.startIntervalPattern:null,[]],endIntervalPattern:[e?e.endIntervalPattern:null,[]]})}validatorTriggers(){return["fetchMode","useMetadataIntervalPatterns"]}updateValidators(e){const t=this.getTelemetryFromDatabaseConfigForm.get("fetchMode").value,o=this.getTelemetryFromDatabaseConfigForm.get("useMetadataIntervalPatterns").value;t&&t===Oe.ALL?(this.getTelemetryFromDatabaseConfigForm.get("aggregation").setValidators([k.required]),this.getTelemetryFromDatabaseConfigForm.get("orderBy").setValidators([k.required]),this.getTelemetryFromDatabaseConfigForm.get("limit").setValidators([k.required,k.min(2),k.max(1e3)])):(this.getTelemetryFromDatabaseConfigForm.get("aggregation").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("orderBy").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("limit").setValidators([])),o?(this.getTelemetryFromDatabaseConfigForm.get("startInterval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endInterval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").setValidators([k.required]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").setValidators([k.required])):(this.getTelemetryFromDatabaseConfigForm.get("startInterval").setValidators([k.required,k.min(1),k.max(2147483647)]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").setValidators([k.required]),this.getTelemetryFromDatabaseConfigForm.get("endInterval").setValidators([k.required,k.min(1),k.max(2147483647)]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").setValidators([k.required]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").setValidators([])),this.getTelemetryFromDatabaseConfigForm.get("aggregation").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("orderBy").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("limit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startInterval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endInterval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").updateValueAndValidity({emitEvent:e})}removeKey(e,t){const o=this.getTelemetryFromDatabaseConfigForm.get(t).value,r=o.indexOf(e);r>=0&&(o.splice(r,1),this.getTelemetryFromDatabaseConfigForm.get(t).setValue(o,{emitEvent:!0}))}addKey(e,t){const o=e.input;let r=e.value;if((r||"").trim()){r=r.trim();let e=this.getTelemetryFromDatabaseConfigForm.get(t).value;e&&-1!==e.indexOf(r)||(e||(e=[]),e.push(r),this.getTelemetryFromDatabaseConfigForm.get(t).setValue(e,{emitEvent:!0}))}o&&(o.value="")}}e("GetTelemetryFromDatabaseConfigComponent",Et),Et.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Et,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Et.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Et,selector:"tb-enrichment-node-get-telemetry-from-database",usesInheritance:!0,ngImport:t,template:'
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n tb.rulenode.fetch-mode\n \n \n {{ mode }}\n \n \n tb.rulenode.fetch-mode-hint\n \n
\n \n aggregation.function\n \n \n {{ aggregationTypesTranslations.get(aggregationTypes[aggregation]) | translate }}\n \n \n \n \n tb.rulenode.order-by\n \n \n {{ order }}\n \n \n tb.rulenode.order-by-hint\n \n \n tb.rulenode.limit\n \n tb.rulenode.limit-hint\n \n
\n \n {{ \'tb.rulenode.use-metadata-interval-patterns\' | translate }}\n \n
tb.rulenode.use-metadata-interval-patterns-hint
\n
\n
\n \n tb.rulenode.start-interval\n \n \n {{ \'tb.rulenode.start-interval-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.start-interval-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n tb.rulenode.end-interval\n \n \n {{ \'tb.rulenode.end-interval-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.end-interval-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n \n tb.rulenode.start-interval-pattern\n \n \n {{ \'tb.rulenode.start-interval-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.end-interval-pattern\n \n \n {{ \'tb.rulenode.end-interval-pattern-required\' | translate }}\n \n \n \n \n
\n',styles:[":host label.tb-title{margin-bottom:-10px}\n"],components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:te.MatChipList,selector:"mat-chip-list",inputs:["aria-orientation","multiple","compareWith","value","required","placeholder","disabled","selectable","tabIndex","errorStateMatcher"],outputs:["change","valueChange"],exportAs:["matChipList"]},{type:oe.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:D.MatLabel,selector:"mat-label"},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:te.MatChip,selector:"mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]",inputs:["color","disableRipple","tabIndex","selected","value","selectable","disabled","removable"],outputs:["selectionChange","destroyed","removed"],exportAs:["matChip"]},{type:te.MatChipRemove,selector:"[matChipRemove]"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:te.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputSeparatorKeyCodes","placeholder","id","matChipInputFor","matChipInputAddOnBlur","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:E.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:D.MatError,selector:"mat-error",inputs:["id"]}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Et,decorators:[{type:o,args:[{selector:"tb-enrichment-node-get-telemetry-from-database",templateUrl:"./get-telemetry-from-database-config.component.html",styleUrls:["./get-telemetry-from-database-config.component.scss"]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Pt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.separatorKeysCodes=[Z,X,ee]}configForm(){return this.originatorAttributesConfigForm}onConfigurationSet(e){this.originatorAttributesConfigForm=this.fb.group({tellFailureIfAbsent:[!!e&&e.tellFailureIfAbsent,[]],clientAttributeNames:[e?e.clientAttributeNames:null,[]],sharedAttributeNames:[e?e.sharedAttributeNames:null,[]],serverAttributeNames:[e?e.serverAttributeNames:null,[]],latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],getLatestValueWithTs:[!!e&&e.getLatestValueWithTs,[]]})}removeKey(e,t){const o=this.originatorAttributesConfigForm.get(t).value,r=o.indexOf(e);r>=0&&(o.splice(r,1),this.originatorAttributesConfigForm.get(t).setValue(o,{emitEvent:!0}))}addKey(e,t){const o=e.input;let r=e.value;if((r||"").trim()){r=r.trim();let e=this.originatorAttributesConfigForm.get(t).value;e&&-1!==e.indexOf(r)||(e||(e=[]),e.push(r),this.originatorAttributesConfigForm.get(t).setValue(e,{emitEvent:!0}))}o&&(o.value="")}}e("OriginatorAttributesConfigComponent",Pt),Pt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Pt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Pt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Pt,selector:"tb-enrichment-node-originator-attributes-config",usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'tb.rulenode.tell-failure-if-absent\' | translate }}\n \n
tb.rulenode.tell-failure-if-absent-hint
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n {{ \'tb.rulenode.get-latest-value-with-ts\' | translate }}\n \n
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}\n"],components:[{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:te.MatChipList,selector:"mat-chip-list",inputs:["aria-orientation","multiple","compareWith","value","required","placeholder","disabled","selectable","tabIndex","errorStateMatcher"],outputs:["change","valueChange"],exportAs:["matChipList"]},{type:oe.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:D.MatLabel,selector:"mat-label"},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:te.MatChip,selector:"mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]",inputs:["color","disableRipple","tabIndex","selected","value","selectable","disabled","removable"],outputs:["selectionChange","destroyed","removed"],exportAs:["matChip"]},{type:te.MatChipRemove,selector:"[matChipRemove]"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:te.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputSeparatorKeyCodes","placeholder","id","matChipInputFor","matChipInputAddOnBlur","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Pt,decorators:[{type:o,args:[{selector:"tb-enrichment-node-originator-attributes-config",templateUrl:"./originator-attributes-config.component.html",styleUrls:["./originator-attributes-config.component.scss"]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Rt extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.originatorFieldsConfigForm}onConfigurationSet(e){this.originatorFieldsConfigForm=this.fb.group({fieldsMapping:[e?e.fieldsMapping:null,[k.required]]})}}e("OriginatorFieldsConfigComponent",Rt),Rt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Rt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Rt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Rt,selector:"tb-enrichment-node-originator-fields-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n \n
\n',components:[{type:nt,selector:"tb-kv-map-config",inputs:["disabled","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Rt,decorators:[{type:o,args:[{selector:"tb-enrichment-node-originator-fields-config",templateUrl:"./originator-fields-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class wt extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.relatedAttributesConfigForm}onConfigurationSet(e){this.relatedAttributesConfigForm=this.fb.group({relationsQuery:[e?e.relationsQuery:null,[k.required]],telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[k.required]]})}}e("RelatedAttributesConfigComponent",wt),wt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:wt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),wt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:wt,selector:"tb-enrichment-node-related-attributes-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n \n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n',components:[{type:qt,selector:"tb-relations-query-config",inputs:["disabled","required"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:nt,selector:"tb-kv-map-config",inputs:["disabled","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:wt,decorators:[{type:o,args:[{selector:"tb-enrichment-node-related-attributes-config",templateUrl:"./related-attributes-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Ot extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.tenantAttributesConfigForm}onConfigurationSet(e){this.tenantAttributesConfigForm=this.fb.group({telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[k.required]]})}}e("TenantAttributesConfigComponent",Ot),Ot.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ot,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Ot.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Ot,selector:"tb-enrichment-node-tenant-attributes-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n',components:[{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:nt,selector:"tb-kv-map-config",inputs:["disabled","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ot,decorators:[{type:o,args:[{selector:"tb-enrichment-node-tenant-attributes-config",templateUrl:"./tenant-attributes-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Ht{}e("RulenodeCoreConfigEnrichmentModule",Ht),Ht.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ht,deps:[],target:t.ɵɵFactoryTarget.NgModule}),Ht.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ht,declarations:[Gt,Vt,Dt,Pt,Rt,Et,wt,Ot,At],imports:[O,F,Mt],exports:[Gt,Vt,Dt,Pt,Rt,Et,wt,Ot,At]}),Ht.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ht,imports:[[O,F,Mt]]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ht,decorators:[{type:i,args:[{declarations:[Gt,Vt,Dt,Pt,Rt,Et,wt,Ot,At],imports:[O,F,Mt],exports:[Gt,Vt,Dt,Pt,Rt,Et,wt,Ot,At]}]}]});class Ut extends s{constructor(e,t,o){super(e),this.store=e,this.translate=t,this.fb=o,this.alarmStatusTranslationsMap=I,this.alarmStatusList=[],this.searchText="",this.displayStatusFn=this.displayStatus.bind(this);for(const e of Object.keys(N))this.alarmStatusList.push(N[e]);this.statusFormControl=new G(""),this.filteredAlarmStatus=this.statusFormControl.valueChanges.pipe(ue(""),pe((e=>e||"")),de((e=>this.fetchAlarmStatus(e))),fe())}ngOnInit(){super.ngOnInit()}configForm(){return this.alarmStatusConfigForm}prepareInputConfig(e){return this.searchText="",this.statusFormControl.patchValue("",{emitEvent:!0}),e}onConfigurationSet(e){this.alarmStatusConfigForm=this.fb.group({alarmStatusList:[e?e.alarmStatusList:null,[k.required]]})}displayStatus(e){return e?this.translate.instant(I.get(e)):void 0}fetchAlarmStatus(e){const t=this.getAlarmStatusList();if(this.searchText=e,this.searchText&&this.searchText.length){const e=this.searchText.toUpperCase();return Ce(t.filter((t=>this.translate.instant(I.get(N[t])).toUpperCase().includes(e))))}return Ce(t)}alarmStatusSelected(e){this.addAlarmStatus(e.option.value),this.clear("")}removeAlarmStatus(e){const t=this.alarmStatusConfigForm.get("alarmStatusList").value;if(t){const o=t.indexOf(e);o>=0&&(t.splice(o,1),this.alarmStatusConfigForm.get("alarmStatusList").setValue(t))}}addAlarmStatus(e){let t=this.alarmStatusConfigForm.get("alarmStatusList").value;t||(t=[]);-1===t.indexOf(e)&&(t.push(e),this.alarmStatusConfigForm.get("alarmStatusList").setValue(t))}getAlarmStatusList(){return this.alarmStatusList.filter((e=>-1===this.alarmStatusConfigForm.get("alarmStatusList").value.indexOf(e)))}onAlarmStatusInputFocus(){this.statusFormControl.updateValueAndValidity({onlySelf:!0,emitEvent:!0})}clear(e=""){this.alarmStatusInput.nativeElement.value=e,this.statusFormControl.patchValue(null,{emitEvent:!0}),setTimeout((()=>{this.alarmStatusInput.nativeElement.blur(),this.alarmStatusInput.nativeElement.focus()}),0)}}e("CheckAlarmStatusComponent",Ut),Ut.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ut,deps:[{token:T.Store},{token:P.TranslateService},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Ut.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Ut,selector:"tb-filter-node-check-alarm-status-config",viewQueries:[{propertyName:"alarmStatusInput",first:!0,predicate:["alarmStatusInput"],descendants:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.alarm-status-filter\n \n \n \n {{alarmStatusTranslationsMap.get(alarmStatus) | translate}}\n \n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-alarm-status-matching\n
\n
\n
\n
\n
\n \n
\n\n\n\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:te.MatChipList,selector:"mat-chip-list",inputs:["aria-orientation","multiple","compareWith","value","required","placeholder","disabled","selectable","tabIndex","errorStateMatcher"],outputs:["change","valueChange"],exportAs:["matChipList"]},{type:oe.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{type:Fe.MatAutocomplete,selector:"mat-autocomplete",inputs:["disableRipple"],exportAs:["matAutocomplete"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:ie.TbErrorComponent,selector:"tb-error",inputs:["error"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:te.MatChip,selector:"mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]",inputs:["color","disableRipple","tabIndex","selected","value","selectable","disabled","removable"],outputs:["selectionChange","destroyed","removed"],exportAs:["matChip"]},{type:te.MatChipRemove,selector:"[matChipRemove]"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:Fe.MatAutocompleteTrigger,selector:"input[matAutocomplete], textarea[matAutocomplete]",exportAs:["matAutocompleteTrigger"]},{type:te.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputSeparatorKeyCodes","placeholder","id","matChipInputFor","matChipInputAddOnBlur","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]},{type:Fe.MatAutocompleteOrigin,selector:"[matAutocompleteOrigin]",exportAs:["matAutocompleteOrigin"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlDirective,selector:"[formControl]",inputs:["disabled","formControl","ngModel"],outputs:["ngModelChange"],exportAs:["ngForm"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]}],pipes:{translate:P.TranslatePipe,async:w.AsyncPipe,highlight:Le.HighlightPipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ut,decorators:[{type:o,args:[{selector:"tb-filter-node-check-alarm-status-config",templateUrl:"./check-alarm-status.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:P.TranslateService},{type:q.FormBuilder}]},propDecorators:{alarmStatusInput:[{type:a,args:["alarmStatusInput",{static:!1}]}]}});class Kt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.separatorKeysCodes=[Z,X,ee]}configForm(){return this.checkMessageConfigForm}onConfigurationSet(e){this.checkMessageConfigForm=this.fb.group({messageNames:[e?e.messageNames:null,[]],metadataNames:[e?e.metadataNames:null,[]],checkAllKeys:[!!e&&e.checkAllKeys,[]]})}validateConfig(){const e=this.checkMessageConfigForm.get("messageNames").value,t=this.checkMessageConfigForm.get("metadataNames").value;return e.length>0||t.length>0}removeMessageName(e){const t=this.checkMessageConfigForm.get("messageNames").value,o=t.indexOf(e);o>=0&&(t.splice(o,1),this.checkMessageConfigForm.get("messageNames").setValue(t,{emitEvent:!0}))}removeMetadataName(e){const t=this.checkMessageConfigForm.get("metadataNames").value,o=t.indexOf(e);o>=0&&(t.splice(o,1),this.checkMessageConfigForm.get("metadataNames").setValue(t,{emitEvent:!0}))}addMessageName(e){const t=e.input;let o=e.value;if((o||"").trim()){o=o.trim();let e=this.checkMessageConfigForm.get("messageNames").value;e&&-1!==e.indexOf(o)||(e||(e=[]),e.push(o),this.checkMessageConfigForm.get("messageNames").setValue(e,{emitEvent:!0}))}t&&(t.value="")}addMetadataName(e){const t=e.input;let o=e.value;if((o||"").trim()){o=o.trim();let e=this.checkMessageConfigForm.get("metadataNames").value;e&&-1!==e.indexOf(o)||(e||(e=[]),e.push(o),this.checkMessageConfigForm.get("metadataNames").setValue(e,{emitEvent:!0}))}t&&(t.value="")}}e("CheckMessageConfigComponent",Kt),Kt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Kt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Kt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Kt,selector:"tb-filter-node-check-message-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n \n \n \n {{messageName}}\n close\n \n \n \n \n
tb.rulenode.separator-hint
\n \n \n \n \n \n {{metadataName}}\n close\n \n \n \n \n
tb.rulenode.separator-hint
\n \n {{ \'tb.rulenode.check-all-keys\' | translate }}\n \n
tb.rulenode.check-all-keys-hint
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}\n"],components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:te.MatChipList,selector:"mat-chip-list",inputs:["aria-orientation","multiple","compareWith","value","required","placeholder","disabled","selectable","tabIndex","errorStateMatcher"],outputs:["change","valueChange"],exportAs:["matChipList"]},{type:oe.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:D.MatLabel,selector:"mat-label"},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:te.MatChip,selector:"mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]",inputs:["color","disableRipple","tabIndex","selected","value","selectable","disabled","removable"],outputs:["selectionChange","destroyed","removed"],exportAs:["matChip"]},{type:te.MatChipRemove,selector:"[matChipRemove]"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:te.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputSeparatorKeyCodes","placeholder","id","matChipInputFor","matChipInputAddOnBlur","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Kt,decorators:[{type:o,args:[{selector:"tb-filter-node-check-message-config",templateUrl:"./check-message-config.component.html",styleUrls:["./check-message-config.component.scss"]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Bt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.entitySearchDirection=Object.keys(c),this.entitySearchDirectionTranslationsMap=g}configForm(){return this.checkRelationConfigForm}onConfigurationSet(e){this.checkRelationConfigForm=this.fb.group({checkForSingleEntity:[!!e&&e.checkForSingleEntity,[]],direction:[e?e.direction:null,[]],entityType:[e?e.entityType:null,e&&e.checkForSingleEntity?[k.required]:[]],entityId:[e?e.entityId:null,e&&e.checkForSingleEntity?[k.required]:[]],relationType:[e?e.relationType:null,[k.required]]})}validatorTriggers(){return["checkForSingleEntity"]}updateValidators(e){const t=this.checkRelationConfigForm.get("checkForSingleEntity").value;this.checkRelationConfigForm.get("entityType").setValidators(t?[k.required]:[]),this.checkRelationConfigForm.get("entityType").updateValueAndValidity({emitEvent:e}),this.checkRelationConfigForm.get("entityId").setValidators(t?[k.required]:[]),this.checkRelationConfigForm.get("entityId").updateValueAndValidity({emitEvent:e})}}e("CheckRelationConfigComponent",Bt),Bt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Bt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Bt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Bt,selector:"tb-filter-node-check-relation-config",usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'tb.rulenode.check-relation-to-specific-entity\' | translate }}\n \n
tb.rulenode.check-relation-hint
\n \n relation.direction\n \n \n {{ entitySearchDirectionTranslationsMap.get(direction) | translate }}\n \n \n \n
\n \n \n \n \n
\n \n \n
\n',components:[{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:ae.EntityTypeSelectComponent,selector:"tb-entity-type-select",inputs:["allowedEntityTypes","useAliasEntityTypes","showLabel","required","disabled"]},{type:ve.EntityAutocompleteComponent,selector:"tb-entity-autocomplete",inputs:["entityType","entitySubtype","excludeEntityIds","labelText","requiredText","required","disabled"],outputs:["entityChanged"]},{type:ye.RelationTypeAutocompleteComponent,selector:"tb-relation-type-autocomplete",inputs:["required","disabled"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:D.MatLabel,selector:"mat-label"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Bt,decorators:[{type:o,args:[{selector:"tb-filter-node-check-relation-config",templateUrl:"./check-relation-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class jt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.perimeterType=Ae,this.perimeterTypes=Object.keys(Ae),this.perimeterTypeTranslationMap=Ge,this.rangeUnits=Object.keys(Ee),this.rangeUnitTranslationMap=Pe}configForm(){return this.geoFilterConfigForm}onConfigurationSet(e){this.geoFilterConfigForm=this.fb.group({latitudeKeyName:[e?e.latitudeKeyName:null,[k.required]],longitudeKeyName:[e?e.longitudeKeyName:null,[k.required]],perimeterType:[e?e.perimeterType:null,[k.required]],fetchPerimeterInfoFromMessageMetadata:[!!e&&e.fetchPerimeterInfoFromMessageMetadata,[]],perimeterKeyName:[e?e.perimeterKeyName:null,[]],centerLatitude:[e?e.centerLatitude:null,[]],centerLongitude:[e?e.centerLatitude:null,[]],range:[e?e.range:null,[]],rangeUnit:[e?e.rangeUnit:null,[]],polygonsDefinition:[e?e.polygonsDefinition:null,[]]})}validatorTriggers(){return["fetchPerimeterInfoFromMessageMetadata","perimeterType"]}updateValidators(e){const t=this.geoFilterConfigForm.get("fetchPerimeterInfoFromMessageMetadata").value,o=this.geoFilterConfigForm.get("perimeterType").value;t?this.geoFilterConfigForm.get("perimeterKeyName").setValidators([k.required]):this.geoFilterConfigForm.get("perimeterKeyName").setValidators([]),t||o!==Ae.CIRCLE?(this.geoFilterConfigForm.get("centerLatitude").setValidators([]),this.geoFilterConfigForm.get("centerLongitude").setValidators([]),this.geoFilterConfigForm.get("range").setValidators([]),this.geoFilterConfigForm.get("rangeUnit").setValidators([])):(this.geoFilterConfigForm.get("centerLatitude").setValidators([k.required,k.min(-90),k.max(90)]),this.geoFilterConfigForm.get("centerLongitude").setValidators([k.required,k.min(-180),k.max(180)]),this.geoFilterConfigForm.get("range").setValidators([k.required,k.min(0)]),this.geoFilterConfigForm.get("rangeUnit").setValidators([k.required])),t||o!==Ae.POLYGON?this.geoFilterConfigForm.get("polygonsDefinition").setValidators([]):this.geoFilterConfigForm.get("polygonsDefinition").setValidators([k.required]),this.geoFilterConfigForm.get("perimeterKeyName").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("centerLatitude").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("centerLongitude").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("range").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("rangeUnit").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("polygonsDefinition").updateValueAndValidity({emitEvent:e})}}e("GpsGeoFilterConfigComponent",jt),jt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:jt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),jt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:jt,selector:"tb-filter-node-gps-geofencing-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.latitude-key-name\n \n \n {{ \'tb.rulenode.latitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.longitude-key-name\n \n \n {{ \'tb.rulenode.longitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \n \n \n {{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}\n \n \n tb.rulenode.perimeter-key-name\n \n \n {{ \'tb.rulenode.perimeter-key-name-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.circle-center-latitude\n \n \n {{ \'tb.rulenode.circle-center-latitude-required\' | translate }}\n \n \n \n tb.rulenode.circle-center-longitude\n \n \n {{ \'tb.rulenode.circle-center-longitude-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.range\n \n \n {{ \'tb.rulenode.range-required\' | translate }}\n \n \n \n tb.rulenode.range-units\n \n \n {{ rangeUnitTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n
\n \n tb.rulenode.polygon-definition\n \n \n {{ \'tb.rulenode.polygon-definition-required\' | translate }}\n \n \n
\n
\n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:E.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:jt,decorators:[{type:o,args:[{selector:"tb-filter-node-gps-geofencing-config",templateUrl:"./gps-geo-filter-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class zt extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.messageTypeConfigForm}onConfigurationSet(e){this.messageTypeConfigForm=this.fb.group({messageTypes:[e?e.messageTypes:null,[k.required]]})}}e("MessageTypeConfigComponent",zt),zt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:zt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),zt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:zt,selector:"tb-filter-node-message-type-config",usesInheritance:!0,ngImport:t,template:'
\n \n
\n',components:[{type:kt,selector:"tb-message-types-config",inputs:["required","label","placeholder","disabled"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:zt,decorators:[{type:o,args:[{selector:"tb-filter-node-message-type-config",templateUrl:"./message-type-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class _t extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.allowedEntityTypes=[x.DEVICE,x.ASSET,x.ENTITY_VIEW,x.TENANT,x.CUSTOMER,x.USER,x.DASHBOARD,x.RULE_CHAIN,x.RULE_NODE]}configForm(){return this.originatorTypeConfigForm}onConfigurationSet(e){this.originatorTypeConfigForm=this.fb.group({originatorTypes:[e?e.originatorTypes:null,[k.required]]})}}e("OriginatorTypeConfigComponent",_t),_t.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:_t,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),_t.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:_t,selector:"tb-filter-node-originator-type-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n \n
\n',styles:[":host ::ng-deep tb-entity-type-list .mat-form-field-flex{padding-top:0}:host ::ng-deep tb-entity-type-list .mat-form-field-infix{border-top:0}\n"],components:[{type:Ie.EntityTypeListComponent,selector:"tb-entity-type-list",inputs:["required","disabled","allowedEntityTypes","ignoreAuthorityFilter"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:_t,decorators:[{type:o,args:[{selector:"tb-filter-node-originator-type-config",templateUrl:"./originator-type-config.component.html",styleUrls:["./originator-type-config.component.scss"]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Qt extends s{constructor(e,t,o,r){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=o,this.translate=r}configForm(){return this.scriptConfigForm}onConfigurationSet(e){this.scriptConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[k.required]]})}testScript(){const e=this.scriptConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(e,"filter",this.translate.instant("tb.rulenode.filter"),"Filter",["msg","metadata","msgType"],this.ruleNodeId,"rulenode/filter_node_script_fn").subscribe((e=>{e&&this.scriptConfigForm.get("jsScript").setValue(e)}))}onValidate(){this.jsFuncComponent.validateOnSubmit()}}e("ScriptConfigComponent",Qt),Qt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Qt,deps:[{token:T.Store},{token:q.FormBuilder},{token:Q.NodeScriptTestService},{token:P.TranslateService}],target:t.ɵɵFactoryTarget.Component}),Qt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Qt,selector:"tb-filter-node-script-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0,static:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n \n \n
\n \n
\n
\n',components:[{type:Y.JsFuncComponent,selector:"tb-js-func",inputs:["functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","noValidate","required"]},{type:J.MatButton,selector:"button[mat-button], button[mat-raised-button], button[mat-icon-button], button[mat-fab], button[mat-mini-fab], button[mat-stroked-button], button[mat-flat-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Qt,decorators:[{type:o,args:[{selector:"tb-filter-node-script-config",templateUrl:"./script-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder},{type:Q.NodeScriptTestService},{type:P.TranslateService}]},propDecorators:{jsFuncComponent:[{type:a,args:["jsFuncComponent",{static:!0}]}]}});class $t extends s{constructor(e,t,o,r){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=o,this.translate=r}configForm(){return this.switchConfigForm}onConfigurationSet(e){this.switchConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[k.required]]})}testScript(){const e=this.switchConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(e,"switch",this.translate.instant("tb.rulenode.switch"),"Switch",["msg","metadata","msgType"],this.ruleNodeId,"rulenode/switch_node_script_fn").subscribe((e=>{e&&this.switchConfigForm.get("jsScript").setValue(e)}))}onValidate(){this.jsFuncComponent.validateOnSubmit()}}e("SwitchConfigComponent",$t),$t.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:$t,deps:[{token:T.Store},{token:q.FormBuilder},{token:Q.NodeScriptTestService},{token:P.TranslateService}],target:t.ɵɵFactoryTarget.Component}),$t.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:$t,selector:"tb-filter-node-switch-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0,static:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n \n \n
\n \n
\n
\n',components:[{type:Y.JsFuncComponent,selector:"tb-js-func",inputs:["functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","noValidate","required"]},{type:J.MatButton,selector:"button[mat-button], button[mat-raised-button], button[mat-icon-button], button[mat-fab], button[mat-mini-fab], button[mat-stroked-button], button[mat-flat-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:$t,decorators:[{type:o,args:[{selector:"tb-filter-node-switch-config",templateUrl:"./switch-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder},{type:Q.NodeScriptTestService},{type:P.TranslateService}]},propDecorators:{jsFuncComponent:[{type:a,args:["jsFuncComponent",{static:!0}]}]}});class Wt{}e("RuleNodeCoreConfigFilterModule",Wt),Wt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Wt,deps:[],target:t.ɵɵFactoryTarget.NgModule}),Wt.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Wt,declarations:[Kt,Bt,jt,zt,_t,Qt,$t,Ut],imports:[O,F,Mt],exports:[Kt,Bt,jt,zt,_t,Qt,$t,Ut]}),Wt.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Wt,imports:[[O,F,Mt]]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Wt,decorators:[{type:i,args:[{declarations:[Kt,Bt,jt,zt,_t,Qt,$t,Ut],imports:[O,F,Mt],exports:[Kt,Bt,jt,zt,_t,Qt,$t,Ut]}]}]});class Yt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.originatorSource=Me,this.originatorSources=Object.keys(Me),this.originatorSourceTranslationMap=Se}configForm(){return this.changeOriginatorConfigForm}onConfigurationSet(e){this.changeOriginatorConfigForm=this.fb.group({originatorSource:[e?e.originatorSource:null,[k.required]],relationsQuery:[e?e.relationsQuery:null,[]]})}validatorTriggers(){return["originatorSource"]}updateValidators(e){const t=this.changeOriginatorConfigForm.get("originatorSource").value;t&&t===Me.RELATED?this.changeOriginatorConfigForm.get("relationsQuery").setValidators([k.required]):this.changeOriginatorConfigForm.get("relationsQuery").setValidators([]),this.changeOriginatorConfigForm.get("relationsQuery").updateValueAndValidity({emitEvent:e})}}e("ChangeOriginatorConfigComponent",Yt),Yt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Yt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Yt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Yt,selector:"tb-transformation-node-change-originator-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.originator-source\n \n \n {{ originatorSourceTranslationMap.get(source) | translate }}\n \n \n \n
\n \n \n \n
\n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:qt,selector:"tb-relations-query-config",inputs:["disabled","required"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Yt,decorators:[{type:o,args:[{selector:"tb-transformation-node-change-originator-config",templateUrl:"./change-originator-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Jt extends s{constructor(e,t,o,r){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=o,this.translate=r}configForm(){return this.scriptConfigForm}onConfigurationSet(e){this.scriptConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[k.required]]})}testScript(){const e=this.scriptConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(e,"update",this.translate.instant("tb.rulenode.transformer"),"Transform",["msg","metadata","msgType"],this.ruleNodeId,"rulenode/transformation_node_script_fn").subscribe((e=>{e&&this.scriptConfigForm.get("jsScript").setValue(e)}))}onValidate(){this.jsFuncComponent.validateOnSubmit()}}e("TransformScriptConfigComponent",Jt),Jt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Jt,deps:[{token:T.Store},{token:q.FormBuilder},{token:Q.NodeScriptTestService},{token:P.TranslateService}],target:t.ɵɵFactoryTarget.Component}),Jt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Jt,selector:"tb-transformation-node-script-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0,static:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n \n \n
\n \n
\n
\n',components:[{type:Y.JsFuncComponent,selector:"tb-js-func",inputs:["functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","noValidate","required"]},{type:J.MatButton,selector:"button[mat-button], button[mat-raised-button], button[mat-icon-button], button[mat-fab], button[mat-mini-fab], button[mat-stroked-button], button[mat-flat-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Jt,decorators:[{type:o,args:[{selector:"tb-transformation-node-script-config",templateUrl:"./script-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder},{type:Q.NodeScriptTestService},{type:P.TranslateService}]},propDecorators:{jsFuncComponent:[{type:a,args:["jsFuncComponent",{static:!0}]}]}});class Zt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.mailBodyTypes=[{name:"tb.mail-body-type.plain-text",value:"false"},{name:"tb.mail-body-type.html",value:"true"},{name:"tb.mail-body-type.dynamic",value:"dynamic"}]}configForm(){return this.toEmailConfigForm}onConfigurationSet(e){this.toEmailConfigForm=this.fb.group({fromTemplate:[e?e.fromTemplate:null,[k.required]],toTemplate:[e?e.toTemplate:null,[k.required]],ccTemplate:[e?e.ccTemplate:null,[]],bccTemplate:[e?e.bccTemplate:null,[]],subjectTemplate:[e?e.subjectTemplate:null,[k.required]],mailBodyType:[e?e.mailBodyType:null],isHtmlTemplate:[e?e.isHtmlTemplate:null],bodyTemplate:[e?e.bodyTemplate:null,[k.required]]}),this.toEmailConfigForm.get("mailBodyType").valueChanges.pipe(ue([null==e?void 0:e.subjectTemplate])).subscribe((e=>{"dynamic"===e?(this.toEmailConfigForm.get("isHtmlTemplate").patchValue("",{emitEvent:!1}),this.toEmailConfigForm.get("isHtmlTemplate").setValidators(k.required)):this.toEmailConfigForm.get("isHtmlTemplate").clearValidators(),this.toEmailConfigForm.get("isHtmlTemplate").updateValueAndValidity()}))}}e("ToEmailConfigComponent",Zt),Zt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Zt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Zt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Zt,selector:"tb-transformation-node-to-email-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.from-template\n \n \n {{ \'tb.rulenode.from-template-required\' | translate }}\n \n \n \n \n tb.rulenode.to-template\n \n \n {{ \'tb.rulenode.to-template-required\' | translate }}\n \n \n \n \n tb.rulenode.cc-template\n \n \n \n \n tb.rulenode.bcc-template\n \n \n \n \n tb.rulenode.subject-template\n \n \n {{ \'tb.rulenode.subject-template-required\' | translate }}\n \n \n \n \n tb.rulenode.mail-body-type\n \n \n {{ type.name | translate }}\n \n \n \n \n tb.rulenode.dynamic-mail-body-type\n \n \n \n \n tb.rulenode.body-template\n \n \n {{ \'tb.rulenode.body-template-required\' | translate }}\n \n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Zt,decorators:[{type:o,args:[{selector:"tb-transformation-node-to-email-config",templateUrl:"./to-email-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Xt{}e("RulenodeCoreConfigTransformModule",Xt),Xt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Xt,deps:[],target:t.ɵɵFactoryTarget.NgModule}),Xt.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Xt,declarations:[Yt,Jt,Zt],imports:[O,F,Mt],exports:[Yt,Jt,Zt]}),Xt.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Xt,imports:[[O,F,Mt]]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Xt,decorators:[{type:i,args:[{declarations:[Yt,Jt,Zt],imports:[O,F,Mt],exports:[Yt,Jt,Zt]}]}]});class eo extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.entityType=x}configForm(){return this.ruleChainInputConfigForm}onConfigurationSet(e){this.ruleChainInputConfigForm=this.fb.group({ruleChainId:[e?e.ruleChainId:null,[k.required]]})}}e("RuleChainInputComponent",eo),eo.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:eo,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),eo.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:eo,selector:"tb-flow-node-rule-chain-input-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n
\n',components:[{type:ve.EntityAutocompleteComponent,selector:"tb-entity-autocomplete",inputs:["entityType","entitySubtype","excludeEntityIds","labelText","requiredText","required","disabled"],outputs:["entityChanged"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:eo,decorators:[{type:o,args:[{selector:"tb-flow-node-rule-chain-input-config",templateUrl:"./rule-chain-input.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class to extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.ruleChainOutputConfigForm}onConfigurationSet(e){this.ruleChainOutputConfigForm=this.fb.group({})}}e("RuleChainOutputComponent",to),to.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:to,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),to.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:to,selector:"tb-flow-node-rule-chain-output-config",usesInheritance:!0,ngImport:t,template:'
\n
\n
\n',directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:to,decorators:[{type:o,args:[{selector:"tb-flow-node-rule-chain-output-config",templateUrl:"./rule-chain-output.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class oo{}e("RuleNodeCoreConfigFlowModule",oo),oo.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:oo,deps:[],target:t.ɵɵFactoryTarget.NgModule}),oo.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:oo,declarations:[eo,to],imports:[O,F,Mt],exports:[eo,to]}),oo.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:oo,imports:[[O,F,Mt]]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:oo,decorators:[{type:i,args:[{declarations:[eo,to],imports:[O,F,Mt],exports:[eo,to]}]}]});class ro{constructor(e){!function(e){e.setTranslation("en_US",{tb:{rulenode:{"create-entity-if-not-exists":"Create new entity if not exists","create-entity-if-not-exists-hint":"Create a new entity set above if it does not exist.","entity-name-pattern":"Name pattern","entity-name-pattern-required":"Name pattern is required","entity-type-pattern":"Type pattern","entity-type-pattern-required":"Type pattern is required","entity-cache-expiration":"Entities cache expiration time (sec)","entity-cache-expiration-hint":"Specifies maximum time interval allowed to store found entity records. 0 value means that records will never expire.","entity-cache-expiration-required":"Entities cache expiration time is required.","entity-cache-expiration-range":"Entities cache expiration time should be greater than or equal to 0.","customer-name-pattern":"Customer name pattern","customer-name-pattern-required":"Customer name pattern is required","create-customer-if-not-exists":"Create new customer if not exists","customer-cache-expiration":"Customers cache expiration time (sec)","customer-cache-expiration-hint":"Specifies maximum time interval allowed to store found customer records. 0 value means that records will never expire.","customer-cache-expiration-required":"Customers cache expiration time is required.","customer-cache-expiration-range":"Customers cache expiration time should be greater than or equal to 0.","start-interval":"Start Interval","end-interval":"End Interval","start-interval-time-unit":"Start Interval Time Unit","end-interval-time-unit":"End Interval Time Unit","fetch-mode":"Fetch mode","fetch-mode-hint":"If selected fetch mode 'ALL' you able to choose telemetry sampling order.","order-by":"Order by","order-by-hint":"Select to choose telemetry sampling order.",limit:"Limit","limit-hint":"Min limit value is 2, max - 1000. In case you want to fetch a single entry, select fetch mode 'FIRST' or 'LAST'.","time-unit-milliseconds":"Milliseconds","time-unit-seconds":"Seconds","time-unit-minutes":"Minutes","time-unit-hours":"Hours","time-unit-days":"Days","time-value-range":"Time value should be in a range from 1 to 2147483647.","start-interval-value-required":"Start interval value is required.","end-interval-value-required":"End interval value is required.",filter:"Filter",switch:"Switch","message-type":"Message type","message-type-required":"Message type is required.","message-types-filter":"Message types filter","no-message-types-found":"No message types found","no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one!","message-types-required":"Message types are required.","client-attributes":"Client attributes","shared-attributes":"Shared attributes","server-attributes":"Server attributes","notify-device":"Notify Device","notify-device-hint":"If the message arrives from the device, we will push it back to the device by default.","latest-timeseries":"Latest timeseries","timeseries-key":"Timeseries key","data-keys":"Message data","metadata-keys":"Message metadata","relations-query":"Relations query","device-relations-query":"Device relations query","max-relation-level":"Max relation level","relation-type-pattern":"Relation type pattern","relation-type-pattern-required":"Relation type pattern is required","relation-types-list":"Relation types to propagate","relation-types-list-hint":"If Propagate relation types are not selected, alarms will be propagated without filtering by relation type.","unlimited-level":"Unlimited level","latest-telemetry":"Latest telemetry","attr-mapping":"Attributes mapping","source-attribute":"Source attribute","source-attribute-required":"Source attribute is required.","source-telemetry":"Source telemetry","source-telemetry-required":"Source telemetry is required.","target-attribute":"Target attribute","target-attribute-required":"Target attribute is required.","attr-mapping-required":"At least one attribute mapping should be specified.","fields-mapping":"Fields mapping","fields-mapping-required":"At least one field mapping should be specified.","source-field":"Source field","source-field-required":"Source field is required.","originator-source":"Originator source","originator-customer":"Customer","originator-tenant":"Tenant","originator-related":"Related","originator-alarm-originator":"Alarm Originator","clone-message":"Clone message",transform:"Transform","default-ttl":"Default TTL in seconds","default-ttl-required":"Default TTL is required.","min-default-ttl-message":"Only 0 minimum TTL is allowed.","message-count":"Message count (0 - unlimited)","message-count-required":"Message count is required.","min-message-count-message":"Only 0 minimum message count is allowed.","period-seconds":"Period in seconds","period-seconds-required":"Period is required.","use-metadata-period-in-seconds-patterns":"Use period in seconds pattern","use-metadata-period-in-seconds-patterns-hint":"If selected, rule node use period in seconds interval pattern from message metadata or data assuming that intervals are in the seconds.","period-in-seconds-pattern":"Period in seconds pattern","period-in-seconds-pattern-required":"Period in seconds pattern is required","min-period-seconds-message":"Only 1 second minimum period is allowed.",originator:"Originator","message-body":"Message body","message-metadata":"Message metadata",generate:"Generate","test-generator-function":"Test generator function",generator:"Generator","test-filter-function":"Test filter function","test-switch-function":"Test switch function","test-transformer-function":"Test transformer function",transformer:"Transformer","alarm-create-condition":"Alarm create condition","test-condition-function":"Test condition function","alarm-clear-condition":"Alarm clear condition","alarm-details-builder":"Alarm details builder","test-details-function":"Test details function","alarm-type":"Alarm type","alarm-type-required":"Alarm type is required.","alarm-severity":"Alarm severity","alarm-severity-required":"Alarm severity is required","alarm-severity-pattern":"Alarm severity pattern","alarm-status-filter":"Alarm status filter","alarm-status-list-empty":"Alarm status list is empty","no-alarm-status-matching":"No alarm status matching were found.",propagate:"Propagate alarm to related entities","propagate-to-owner":"Propagate alarm to entity owner (Customer or Tenant)","propagate-to-tenant":"Propagate alarm to Tenant",condition:"Condition",details:"Details","to-string":"To string","test-to-string-function":"Test to string function","from-template":"From Template","from-template-required":"From Template is required","to-template":"To Template","to-template-required":"To Template is required","mail-address-list-template-hint":'Comma separated address list, use ${metadataKey} for value from metadata, $[messageKey] for value from message body',"cc-template":"Cc Template","bcc-template":"Bcc Template","subject-template":"Subject Template","subject-template-required":"Subject Template is required","body-template":"Body Template","body-template-required":"Body Template is required","dynamic-mail-body-type":"Dynamic mail body type","mail-body-type":"Mail body type","request-id-metadata-attribute":"Request Id Metadata attribute name","timeout-sec":"Timeout in seconds","timeout-required":"Timeout is required","min-timeout-message":"Only 0 minimum timeout value is allowed.","endpoint-url-pattern":"Endpoint URL pattern","endpoint-url-pattern-required":"Endpoint URL pattern is required","request-method":"Request method","use-simple-client-http-factory":"Use simple client HTTP factory","ignore-request-body":"Without request body","read-timeout":"Read timeout in millis","read-timeout-hint":"The value of 0 means an infinite timeout","max-parallel-requests-count":"Max number of parallel requests","max-parallel-requests-count-hint":"The value of 0 specifies no limit in parallel processing",headers:"Headers","headers-hint":'Use ${metadataKey} for value from metadata, $[messageKey] for value from message body in header/value fields',header:"Header","header-required":"Header is required",value:"Value","value-required":"Value is required","topic-pattern":"Topic pattern","topic-pattern-required":"Topic pattern is required",topic:"Topic","topic-required":"Topic is required","bootstrap-servers":"Bootstrap servers","bootstrap-servers-required":"Bootstrap servers value is required","other-properties":"Other properties",key:"Key","key-required":"Key is required",retries:"Automatically retry times if fails","min-retries-message":"Only 0 minimum retries is allowed.","batch-size-bytes":"Produces batch size in bytes","min-batch-size-bytes-message":"Only 0 minimum batch size is allowed.","linger-ms":"Time to buffer locally (ms)","min-linger-ms-message":"Only 0 ms minimum value is allowed.","buffer-memory-bytes":"Client buffer max size in bytes","min-buffer-memory-message":"Only 0 minimum buffer size is allowed.",acks:"Number of acknowledgments","key-serializer":"Key serializer","key-serializer-required":"Key serializer is required","value-serializer":"Value serializer","value-serializer-required":"Value serializer is required","topic-arn-pattern":"Topic ARN pattern","topic-arn-pattern-required":"Topic ARN pattern is required","aws-access-key-id":"AWS Access Key ID","aws-access-key-id-required":"AWS Access Key ID is required","aws-secret-access-key":"AWS Secret Access Key","aws-secret-access-key-required":"AWS Secret Access Key is required","aws-region":"AWS Region","aws-region-required":"AWS Region is required","exchange-name-pattern":"Exchange name pattern","routing-key-pattern":"Routing key pattern","message-properties":"Message properties",host:"Host","host-required":"Host is required",port:"Port","port-required":"Port is required","port-range":"Port should be in a range from 1 to 65535.","virtual-host":"Virtual host",username:"Username",password:"Password","automatic-recovery":"Automatic recovery","connection-timeout-ms":"Connection timeout (ms)","min-connection-timeout-ms-message":"Only 0 ms minimum value is allowed.","handshake-timeout-ms":"Handshake timeout (ms)","min-handshake-timeout-ms-message":"Only 0 ms minimum value is allowed.","client-properties":"Client properties","queue-url-pattern":"Queue URL pattern","queue-url-pattern-required":"Queue URL pattern is required","delay-seconds":"Delay (seconds)","min-delay-seconds-message":"Only 0 seconds minimum value is allowed.","max-delay-seconds-message":"Only 900 seconds maximum value is allowed.",name:"Name","name-required":"Name is required","queue-type":"Queue type","sqs-queue-standard":"Standard","sqs-queue-fifo":"FIFO","gcp-project-id":"GCP project ID","gcp-project-id-required":"GCP project ID is required","gcp-service-account-key":"GCP service account key file","gcp-service-account-key-required":"GCP service account key file is required","pubsub-topic-name":"Topic name","pubsub-topic-name-required":"Topic name is required","message-attributes":"Message attributes","message-attributes-hint":'Use ${metadataKey} for value from metadata, $[messageKey] for value from message body in name/value fields',"connect-timeout":"Connection timeout (sec)","connect-timeout-required":"Connection timeout is required.","connect-timeout-range":"Connection timeout should be in a range from 1 to 200.","client-id":"Client ID","client-id-hint":'Hint: Optional. Leave empty for auto-generated Client ID. Be careful when specifying the Client ID. Majority of the MQTT brokers will not allow multiple connections with the same Client ID. To connect to such brokers, your mqtt Client ID must be unique. When platform is running in a micro-services mode, the copy of rule node is launched in each micro-service. This will automatically lead to multiple mqtt clients with the same ID and may cause failures of the rule node. To avoid such failures enable "Add Service ID as suffix to Client ID" option below.',"append-client-id-suffix":"Add Service ID as suffix to Client ID","client-id-suffix-hint":'Hint: Optional. Applied when "Client ID" specified explicitly. If selected then Service ID will be added to Client ID as a suffix. Helps to avoid failures when platform is running in a micro-services mode.',"device-id":"Device ID","device-id-required":"Device ID is required.","clean-session":"Clean session","enable-ssl":"Enable SSL",credentials:"Credentials","credentials-type":"Credentials type","credentials-type-required":"Credentials type is required.","credentials-anonymous":"Anonymous","credentials-basic":"Basic","credentials-pem":"PEM","credentials-pem-hint":"At least Server CA certificate file or a pair of Client certificate and Client private key files are required","credentials-sas":"Shared Access Signature","sas-key":"SAS Key","sas-key-required":"SAS Key is required.",hostname:"Hostname","hostname-required":"Hostname is required.","azure-ca-cert":"CA certificate file","username-required":"Username is required.","password-required":"Password is required.","ca-cert":"Server CA certificate file *","private-key":"Client private key file *",cert:"Client certificate file *","no-file":"No file selected.","drop-file":"Drop a file or click to select a file to upload.","private-key-password":"Private key password","use-system-smtp-settings":"Use system SMTP settings","use-metadata-interval-patterns":"Use interval patterns","use-metadata-interval-patterns-hint":"If selected, rule node use start and end interval patterns from message metadata or data assuming that intervals are in the milliseconds.","use-message-alarm-data":"Use message alarm data","overwrite-alarm-details":"Overwrite alarm details","use-alarm-severity-pattern":"Use alarm severity pattern","check-all-keys":"Check that all selected keys are present","check-all-keys-hint":"If selected, checks that all specified keys are present in the message data and metadata.","check-relation-to-specific-entity":"Check relation to specific entity","check-relation-hint":"Checks existence of relation to specific entity or to any entity based on direction and relation type.","delete-relation-to-specific-entity":"Delete relation to specific entity","delete-relation-hint":"Deletes relation from the originator of the incoming message to the specified entity or list of entities based on direction and type.","remove-current-relations":"Remove current relations","remove-current-relations-hint":"Removes current relations from the originator of the incoming message based on direction and type.","change-originator-to-related-entity":"Change originator to related entity","change-originator-to-related-entity-hint":"Used to process submitted message as a message from another entity.","start-interval-pattern":"Start interval pattern","end-interval-pattern":"End interval pattern","start-interval-pattern-required":"Start interval pattern is required","end-interval-pattern-required":"End interval pattern is required","smtp-protocol":"Protocol","smtp-host":"SMTP host","smtp-host-required":"SMTP host is required.","smtp-port":"SMTP port","smtp-port-required":"You must supply a smtp port.","smtp-port-range":"SMTP port should be in a range from 1 to 65535.","timeout-msec":"Timeout ms","min-timeout-msec-message":"Only 0 ms minimum value is allowed.","enter-username":"Enter username","enter-password":"Enter password","enable-tls":"Enable TLS","tls-version":"TLS version","enable-proxy":"Enable proxy","use-system-proxy-properties":"Use system proxy properties","proxy-host":"Proxy host","proxy-host-required":"Proxy host is required.","proxy-port":"Proxy port","proxy-port-required":"Proxy port is required.","proxy-port-range":"Proxy port should be in a range from 1 to 65535.","proxy-user":"Proxy user","proxy-password":"Proxy password","proxy-scheme":"Proxy scheme","numbers-to-template":"Phone Numbers To Template","numbers-to-template-required":"Phone Numbers To Template is required","numbers-to-template-hint":'Comma separated Phone Numbers, use ${metadataKey} for value from metadata, $[messageKey] for value from message body',"sms-message-template":"SMS message Template","sms-message-template-required":"SMS message Template is required","use-system-sms-settings":"Use system SMS provider settings","min-period-0-seconds-message":"Only 0 second minimum period is allowed.","max-pending-messages":"Maximum pending messages","max-pending-messages-required":"Maximum pending messages is required.","max-pending-messages-range":"Maximum pending messages should be in a range from 1 to 100000.","originator-types-filter":"Originator types filter","interval-seconds":"Interval in seconds","interval-seconds-required":"Interval is required.","min-interval-seconds-message":"Only 1 second minimum interval is allowed.","output-timeseries-key-prefix":"Output timeseries key prefix","output-timeseries-key-prefix-required":"Output timeseries key prefix required.","separator-hint":'You should press "enter" to complete field input.',"entity-details":"Select entity details:","entity-details-title":"Title","entity-details-country":"Country","entity-details-state":"State","entity-details-city":"City","entity-details-zip":"Zip","entity-details-address":"Address","entity-details-address2":"Address2","entity-details-additional_info":"Additional Info","entity-details-phone":"Phone","entity-details-email":"Email","add-to-metadata":"Add selected details to message metadata","add-to-metadata-hint":"If selected, adds the selected details keys to the message metadata instead of message data.","entity-details-list-empty":"No entity details selected.","no-entity-details-matching":"No entity details matching were found.","custom-table-name":"Custom table name","custom-table-name-required":"Table Name is required","custom-table-hint":"You should enter the table name without prefix 'cs_tb_'.","message-field":"Message field","message-field-required":"Message field is required.","table-col":"Table column","table-col-required":"Table column is required.","latitude-key-name":"Latitude key name","longitude-key-name":"Longitude key name","latitude-key-name-required":"Latitude key name is required.","longitude-key-name-required":"Longitude key name is required.","fetch-perimeter-info-from-message-metadata":"Fetch perimeter information from message metadata","perimeter-key-name":"Perimeter key name","perimeter-key-name-required":"Perimeter key name is required.","perimeter-circle":"Circle","perimeter-polygon":"Polygon","perimeter-type":"Perimeter type","circle-center-latitude":"Center latitude","circle-center-latitude-required":"Center latitude is required.","circle-center-longitude":"Center longitude","circle-center-longitude-required":"Center longitude is required.","range-unit-meter":"Meter","range-unit-kilometer":"Kilometer","range-unit-foot":"Foot","range-unit-mile":"Mile","range-unit-nautical-mile":"Nautical mile","range-units":"Range units",range:"Range","range-required":"Range is required.","polygon-definition":"Polygon definition","polygon-definition-required":"Polygon definition is required.","polygon-definition-hint":"Please, use the following format for manual definition of polygon: [[lat1,lon1],[lat2,lon2], ... ,[latN,lonN]].","min-inside-duration":"Minimal inside duration","min-inside-duration-value-required":"Minimal inside duration is required","min-inside-duration-time-unit":"Minimal inside duration time unit","min-outside-duration":"Minimal outside duration","min-outside-duration-value-required":"Minimal outside duration is required","min-outside-duration-time-unit":"Minimal outside duration time unit","tell-failure-if-absent":"Tell Failure","tell-failure-if-absent-hint":'If at least one selected key doesn\'t exist the outbound message will report "Failure".',"get-latest-value-with-ts":"Fetch Latest telemetry with Timestamp","get-latest-value-with-ts-hint":'If selected, latest telemetry values will be added to the outbound message metadata with timestamp, e.g: "temp": "{"ts":1574329385897, "value":42}"',"use-redis-queue":"Use redis queue for message persistence","trim-redis-queue":"Trim redis queue","redis-queue-max-size":"Redis queue max size","add-metadata-key-values-as-kafka-headers":"Add Message metadata key-value pairs to Kafka record headers","add-metadata-key-values-as-kafka-headers-hint":"If selected, key-value pairs from message metadata will be added to the outgoing records headers as byte arrays with predefined charset encoding.","charset-encoding":"Charset encoding","charset-encoding-required":"Charset encoding is required.","charset-us-ascii":"US-ASCII","charset-iso-8859-1":"ISO-8859-1","charset-utf-8":"UTF-8","charset-utf-16be":"UTF-16BE","charset-utf-16le":"UTF-16LE","charset-utf-16":"UTF-16","select-queue-hint":"The queue name can be selected from a drop-down list or add a custom name.","persist-alarm-rules":"Persist state of alarm rules","fetch-alarm-rules":"Fetch state of alarm rules","input-value-key":"Input value key","input-value-key-required":"Input value key is required.","output-value-key":"Output value key","output-value-key-required":"Output value key is required.",round:"Decimals","round-range":"Decimals should be in a range from 0 to 15.","use-cache":"Use cache for latest value","tell-failure-if-delta-is-negative":"Tell Failure if delta is negative","add-period-between-msgs":"Add period between messages","period-value-key":"Period value key","period-value-key-required":"Period value key is required.","general-pattern-hint":'Hint: use ${metadataKey} for value from metadata, $[messageKey] for value from message body',"alarm-severity-pattern-hint":'Hint: use ${metadataKey} for value from metadata, $[messageKey] for value from message body. Alarm severity should be system (CRITICAL, MAJOR etc.)',"output-node-name-hint":"The rule node name corresponds to the relation type of the output message, and it is used to forward messages to other rule nodes in the caller rule chain.","skip-latest-persistence":"Skip latest persistence","use-server-ts":"Use server ts","use-server-ts-hint":"Enable this setting to use the timestamp of the message processing instead of the timestamp from the message. Useful for all sorts of sequential processing if you merge messages from multiple sources (devices, assets, etc).","kv-map-pattern-hint":'Hint: use ${metadataKey} for value from metadata, $[messageKey] for value from message body to substitute "Source" and "Target" key names'},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"},"mail-body-type":{"plain-text":"Plain Text",html:"HTML",dynamic:"Dynamic"}}},!0)}(e)}}e("RuleNodeCoreConfigModule",ro),ro.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ro,deps:[{token:P.TranslateService}],target:t.ɵɵFactoryTarget.NgModule}),ro.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ro,declarations:[Ne],imports:[O,F],exports:[St,Wt,Ht,Xt,oo,Ne]}),ro.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ro,imports:[[O,F],St,Wt,Ht,Xt,oo]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ro,decorators:[{type:i,args:[{declarations:[Ne],imports:[O,F],exports:[St,Wt,Ht,Xt,oo,Ne]}]}],ctorParameters:function(){return[{type:P.TranslateService}]}})}}}));//# sourceMappingURL=rulenode-core-config.js.map +System.register(["@angular/core","@shared/public-api","@ngrx/store","@angular/forms","@angular/material/form-field","@angular/material/checkbox","@angular/flex-layout/flex","@ngx-translate/core","@angular/material/input","@angular/common","@angular/platform-browser","@angular/material/select","@angular/material/core","@angular/material/expansion","@shared/components/button/toggle-password.component","@shared/components/file-input.component","@shared/components/queue/queue-autocomplete.component","@core/public-api","@shared/components/js-func.component","@angular/material/button","@angular/cdk/keycodes","@angular/material/chips","@angular/material/icon","@angular/flex-layout/extended","@shared/components/entity/entity-type-select.component","@shared/components/entity/entity-select.component","@angular/cdk/coercion","@shared/components/tb-error.component","@angular/material/tooltip","rxjs/operators","@shared/components/tb-checkbox.component","@home/components/sms/sms-provider-configuration.component","@home/components/public-api","@shared/components/relation/relation-type-autocomplete.component","@shared/components/entity/entity-subtype-list.component","@home/components/relation/relation-filters.component","rxjs","@angular/material/autocomplete","@shared/pipe/highlight.pipe","@shared/components/entity/entity-autocomplete.component","@shared/components/entity/entity-type-list.component"],(function(e){"use strict";var t,o,r,a,n,l,i,s,m,u,p,d,f,c,g,x,y,b,h,C,F,L,v,I,N,T,q,k,M,S,A,G,D,V,E,P,R,w,O,H,U,K,B,j,z,_,Q,$,W,Y,J,Z,X,ee,te,oe,re,ae,ne,le,ie,se,me,ue,pe,de,fe,ce,ge,xe,ye,be,he,Ce,Fe,Le,ve,Ie;return{setters:[function(e){t=e,o=e.Component,r=e.Pipe,a=e.ViewChild,n=e.forwardRef,l=e.Input,i=e.NgModule},function(e){s=e.RuleNodeConfigurationComponent,m=e.AttributeScope,u=e.telemetryTypeTranslations,p=e.ServiceType,d=e.AlarmSeverity,f=e.alarmSeverityTranslations,c=e.EntitySearchDirection,g=e.entitySearchDirectionTranslations,x=e.EntityType,y=e.PageComponent,b=e.MessageType,h=e.messageTypeNames,C=e,F=e.SharedModule,L=e.AggregationType,v=e.aggregationTranslations,I=e.alarmStatusTranslations,N=e.AlarmStatus},function(e){T=e},function(e){q=e,k=e.Validators,M=e.NgControl,S=e.NG_VALUE_ACCESSOR,A=e.NG_VALIDATORS,G=e.FormControl},function(e){D=e},function(e){V=e},function(e){E=e},function(e){P=e},function(e){R=e},function(e){w=e,O=e.CommonModule},function(e){H=e},function(e){U=e},function(e){K=e},function(e){B=e},function(e){j=e},function(e){z=e},function(e){_=e},function(e){Q=e,$=e.isDefinedAndNotNull,W=e.isNotEmptyStr},function(e){Y=e},function(e){J=e},function(e){Z=e.ENTER,X=e.COMMA,ee=e.SEMICOLON},function(e){te=e},function(e){oe=e},function(e){re=e},function(e){ae=e},function(e){ne=e},function(e){le=e.coerceBooleanProperty},function(e){ie=e},function(e){se=e},function(e){me=e.distinctUntilChanged,ue=e.startWith,pe=e.map,de=e.mergeMap,fe=e.share},function(e){ce=e},function(e){ge=e},function(e){xe=e.HomeComponentsModule},function(e){ye=e},function(e){be=e},function(e){he=e},function(e){Ce=e.of},function(e){Fe=e},function(e){Le=e},function(e){ve=e},function(e){Ie=e}],execute:function(){class Ne extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.emptyConfigForm}onConfigurationSet(e){this.emptyConfigForm=this.fb.group({})}}e("EmptyConfigComponent",Ne),Ne.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ne,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Ne.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Ne,selector:"tb-node-empty-config",usesInheritance:!0,ngImport:t,template:"
",isInline:!0}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ne,decorators:[{type:o,args:[{selector:"tb-node-empty-config",template:"
",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Te{constructor(e){this.sanitizer=e}transform(e){return this.sanitizer.bypassSecurityTrustHtml(e)}}e("SafeHtmlPipe",Te),Te.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Te,deps:[{token:H.DomSanitizer}],target:t.ɵɵFactoryTarget.Pipe}),Te.ɵpipe=t.ɵɵngDeclarePipe({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Te,name:"safeHtml"}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Te,decorators:[{type:r,args:[{name:"safeHtml"}]}],ctorParameters:function(){return[{type:H.DomSanitizer}]}});class qe extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.assignCustomerConfigForm}onConfigurationSet(e){this.assignCustomerConfigForm=this.fb.group({customerNamePattern:[e?e.customerNamePattern:null,[k.required,k.pattern(/.*\S.*/)]],createCustomerIfNotExists:[!!e&&e.createCustomerIfNotExists,[]],customerCacheExpiration:[e?e.customerCacheExpiration:null,[k.required,k.min(0)]]})}prepareOutputConfig(e){return e.customerNamePattern=e.customerNamePattern.trim(),e}}e("AssignCustomerConfigComponent",qe),qe.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:qe,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),qe.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:qe,selector:"tb-action-node-assign-to-customer-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.customer-name-pattern\n \n \n {{ \'tb.rulenode.customer-name-pattern-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.create-customer-if-not-exists\' | translate }}\n \n \n tb.rulenode.customer-cache-expiration\n \n \n {{ \'tb.rulenode.customer-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.customer-cache-expiration-range\' | translate }}\n \n tb.rulenode.customer-cache-expiration-hint\n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:qe,decorators:[{type:o,args:[{selector:"tb-action-node-assign-to-customer-config",templateUrl:"./assign-customer-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class ke extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.attributeScopes=Object.keys(m),this.telemetryTypeTranslationsMap=u}configForm(){return this.attributesConfigForm}onConfigurationSet(e){this.attributesConfigForm=this.fb.group({scope:[e?e.scope:null,[k.required]],notifyDevice:[!e||e.notifyDevice,[]]})}}var Me;e("AttributesConfigComponent",ke),ke.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ke,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),ke.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:ke,selector:"tb-action-node-attributes-config",usesInheritance:!0,ngImport:t,template:'
\n \n attribute.attributes-scope\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n
\n \n {{ \'tb.rulenode.notify-device\' | translate }}\n \n
tb.rulenode.notify-device-hint
\n
\n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ke,decorators:[{type:o,args:[{selector:"tb-action-node-attributes-config",templateUrl:"./attributes-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}}),function(e){e.CUSTOMER="CUSTOMER",e.TENANT="TENANT",e.RELATED="RELATED",e.ALARM_ORIGINATOR="ALARM_ORIGINATOR"}(Me||(Me={}));const Se=new Map([[Me.CUSTOMER,"tb.rulenode.originator-customer"],[Me.TENANT,"tb.rulenode.originator-tenant"],[Me.RELATED,"tb.rulenode.originator-related"],[Me.ALARM_ORIGINATOR,"tb.rulenode.originator-alarm-originator"]]);var Ae;!function(e){e.CIRCLE="CIRCLE",e.POLYGON="POLYGON"}(Ae||(Ae={}));const Ge=new Map([[Ae.CIRCLE,"tb.rulenode.perimeter-circle"],[Ae.POLYGON,"tb.rulenode.perimeter-polygon"]]);var De;!function(e){e.MILLISECONDS="MILLISECONDS",e.SECONDS="SECONDS",e.MINUTES="MINUTES",e.HOURS="HOURS",e.DAYS="DAYS"}(De||(De={}));const Ve=new Map([[De.MILLISECONDS,"tb.rulenode.time-unit-milliseconds"],[De.SECONDS,"tb.rulenode.time-unit-seconds"],[De.MINUTES,"tb.rulenode.time-unit-minutes"],[De.HOURS,"tb.rulenode.time-unit-hours"],[De.DAYS,"tb.rulenode.time-unit-days"]]);var Ee;!function(e){e.METER="METER",e.KILOMETER="KILOMETER",e.FOOT="FOOT",e.MILE="MILE",e.NAUTICAL_MILE="NAUTICAL_MILE"}(Ee||(Ee={}));const Pe=new Map([[Ee.METER,"tb.rulenode.range-unit-meter"],[Ee.KILOMETER,"tb.rulenode.range-unit-kilometer"],[Ee.FOOT,"tb.rulenode.range-unit-foot"],[Ee.MILE,"tb.rulenode.range-unit-mile"],[Ee.NAUTICAL_MILE,"tb.rulenode.range-unit-nautical-mile"]]);var Re;!function(e){e.TITLE="TITLE",e.COUNTRY="COUNTRY",e.STATE="STATE",e.CITY="CITY",e.ZIP="ZIP",e.ADDRESS="ADDRESS",e.ADDRESS2="ADDRESS2",e.PHONE="PHONE",e.EMAIL="EMAIL",e.ADDITIONAL_INFO="ADDITIONAL_INFO"}(Re||(Re={}));const we=new Map([[Re.TITLE,"tb.rulenode.entity-details-title"],[Re.COUNTRY,"tb.rulenode.entity-details-country"],[Re.STATE,"tb.rulenode.entity-details-state"],[Re.CITY,"tb.rulenode.entity-details-city"],[Re.ZIP,"tb.rulenode.entity-details-zip"],[Re.ADDRESS,"tb.rulenode.entity-details-address"],[Re.ADDRESS2,"tb.rulenode.entity-details-address2"],[Re.PHONE,"tb.rulenode.entity-details-phone"],[Re.EMAIL,"tb.rulenode.entity-details-email"],[Re.ADDITIONAL_INFO,"tb.rulenode.entity-details-additional_info"]]);var Oe,He,Ue;!function(e){e.FIRST="FIRST",e.LAST="LAST",e.ALL="ALL"}(Oe||(Oe={})),function(e){e.ASC="ASC",e.DESC="DESC"}(He||(He={})),function(e){e.STANDARD="STANDARD",e.FIFO="FIFO"}(Ue||(Ue={}));const Ke=new Map([[Ue.STANDARD,"tb.rulenode.sqs-queue-standard"],[Ue.FIFO,"tb.rulenode.sqs-queue-fifo"]]),Be=["anonymous","basic","cert.PEM"],je=new Map([["anonymous","tb.rulenode.credentials-anonymous"],["basic","tb.rulenode.credentials-basic"],["cert.PEM","tb.rulenode.credentials-pem"]]),ze=["sas","cert.PEM"],_e=new Map([["sas","tb.rulenode.credentials-sas"],["cert.PEM","tb.rulenode.credentials-pem"]]);var Qe;!function(e){e.GET="GET",e.POST="POST",e.PUT="PUT",e.DELETE="DELETE"}(Qe||(Qe={}));const $e=["US-ASCII","ISO-8859-1","UTF-8","UTF-16BE","UTF-16LE","UTF-16"],We=new Map([["US-ASCII","tb.rulenode.charset-us-ascii"],["ISO-8859-1","tb.rulenode.charset-iso-8859-1"],["UTF-8","tb.rulenode.charset-utf-8"],["UTF-16BE","tb.rulenode.charset-utf-16be"],["UTF-16LE","tb.rulenode.charset-utf-16le"],["UTF-16","tb.rulenode.charset-utf-16"]]);class Ye extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.allAzureIotHubCredentialsTypes=ze,this.azureIotHubCredentialsTypeTranslationsMap=_e}configForm(){return this.azureIotHubConfigForm}onConfigurationSet(e){this.azureIotHubConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[k.required]],host:[e?e.host:null,[k.required]],port:[e?e.port:null,[k.required,k.min(1),k.max(65535)]],connectTimeoutSec:[e?e.connectTimeoutSec:null,[k.required,k.min(1),k.max(200)]],clientId:[e?e.clientId:null,[k.required]],cleanSession:[!!e&&e.cleanSession,[]],ssl:[!!e&&e.ssl,[]],credentials:this.fb.group({type:[e&&e.credentials?e.credentials.type:null,[k.required]],sasKey:[e&&e.credentials?e.credentials.sasKey:null,[]],caCert:[e&&e.credentials?e.credentials.caCert:null,[]],caCertFileName:[e&&e.credentials?e.credentials.caCertFileName:null,[]],privateKey:[e&&e.credentials?e.credentials.privateKey:null,[]],privateKeyFileName:[e&&e.credentials?e.credentials.privateKeyFileName:null,[]],cert:[e&&e.credentials?e.credentials.cert:null,[]],certFileName:[e&&e.credentials?e.credentials.certFileName:null,[]],password:[e&&e.credentials?e.credentials.password:null,[]]})})}prepareOutputConfig(e){const t=e.credentials.type;return"sas"===t&&(e.credentials={type:t,sasKey:e.credentials.sasKey,caCert:e.credentials.caCert,caCertFileName:e.credentials.caCertFileName}),e}validatorTriggers(){return["credentials.type"]}updateValidators(e){const t=this.azureIotHubConfigForm.get("credentials"),o=t.get("type").value;switch(e&&t.reset({type:o},{emitEvent:!1}),t.get("sasKey").setValidators([]),t.get("privateKey").setValidators([]),t.get("privateKeyFileName").setValidators([]),t.get("cert").setValidators([]),t.get("certFileName").setValidators([]),o){case"sas":t.get("sasKey").setValidators([k.required]);break;case"cert.PEM":t.get("privateKey").setValidators([k.required]),t.get("privateKeyFileName").setValidators([k.required]),t.get("cert").setValidators([k.required]),t.get("certFileName").setValidators([k.required])}t.get("sasKey").updateValueAndValidity({emitEvent:e}),t.get("privateKey").updateValueAndValidity({emitEvent:e}),t.get("privateKeyFileName").updateValueAndValidity({emitEvent:e}),t.get("cert").updateValueAndValidity({emitEvent:e}),t.get("certFileName").updateValueAndValidity({emitEvent:e})}}e("AzureIotHubConfigComponent",Ye),Ye.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ye,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Ye.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Ye,selector:"tb-action-node-azure-iot-hub-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.topic\n \n \n {{ \'tb.rulenode.topic-required\' | translate }}\n \n \n \n \n tb.rulenode.hostname\n \n \n {{ \'tb.rulenode.hostname-required\' | translate }}\n \n \n \n tb.rulenode.device-id\n \n \n {{ \'tb.rulenode.device-id-required\' | translate }}\n \n \n \n \n \n tb.rulenode.credentials\n \n {{ azureIotHubCredentialsTypeTranslationsMap.get(azureIotHubConfigForm.get(\'credentials.type\').value) | translate }}\n \n \n
\n \n tb.rulenode.credentials-type\n \n \n {{ azureIotHubCredentialsTypeTranslationsMap.get(credentialsType) | translate }}\n \n \n \n {{ \'tb.rulenode.credentials-type-required\' | translate }}\n \n \n
\n \n \n \n \n tb.rulenode.sas-key\n \n \n \n {{ \'tb.rulenode.sas-key-required\' | translate }}\n \n \n \n \n \n \n \n \n \n \n \n \n \n tb.rulenode.private-key-password\n \n \n \n \n
\n
\n
\n
\n
\n',styles:[":host .tb-mqtt-credentials-panel-group{margin:0 6px}:host .tb-hint.client-id{margin-top:-1.25em;max-width:-moz-fit-content;max-width:fit-content}:host mat-checkbox{padding-bottom:16px}\n"],components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:B.MatExpansionPanel,selector:"mat-expansion-panel",inputs:["disabled","expanded","hideToggle","togglePosition"],outputs:["opened","closed","expandedChange","afterExpand","afterCollapse"],exportAs:["matExpansionPanel"]},{type:B.MatExpansionPanelHeader,selector:"mat-expansion-panel-header",inputs:["tabIndex","expandedHeight","collapsedHeight"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:j.TogglePasswordComponent,selector:"tb-toggle-password"},{type:z.FileInputComponent,selector:"tb-file-input",inputs:["label","accept","noFileText","inputId","allowedExtensions","dropLabel","contentConvertFunction","required","requiredAsError","disabled","existingFileName","readAsBinary","workFromFileObj","multipleFile"],outputs:["fileNameChanged"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]},{type:B.MatAccordion,selector:"mat-accordion",inputs:["multi","displayMode","togglePosition","hideToggle"],exportAs:["matAccordion"]},{type:B.MatExpansionPanelTitle,selector:"mat-panel-title"},{type:B.MatExpansionPanelDescription,selector:"mat-panel-description"},{type:q.FormGroupName,selector:"[formGroupName]",inputs:["formGroupName"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:w.NgSwitch,selector:"[ngSwitch]",inputs:["ngSwitch"]},{type:w.NgSwitchCase,selector:"[ngSwitchCase]",inputs:["ngSwitchCase"]},{type:D.MatSuffix,selector:"[matSuffix]"}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ye,decorators:[{type:o,args:[{selector:"tb-action-node-azure-iot-hub-config",templateUrl:"./azure-iot-hub-config.component.html",styleUrls:["./mqtt-config.component.scss"]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Je extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.serviceType=p.TB_RULE_ENGINE}configForm(){return this.checkPointConfigForm}onConfigurationSet(e){this.checkPointConfigForm=this.fb.group({queueId:[e?e.queueId:null,[k.required]]})}}e("CheckPointConfigComponent",Je),Je.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Je,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Je.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Je,selector:"tb-action-node-check-point-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n
\n',components:[{type:_.QueueAutocompleteComponent,selector:"tb-queue-autocomplete",inputs:["labelText","requiredText","required","queueType","disabled"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Je,decorators:[{type:o,args:[{selector:"tb-action-node-check-point-config",templateUrl:"./check-point-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Ze extends s{constructor(e,t,o,r){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=o,this.translate=r}configForm(){return this.clearAlarmConfigForm}onConfigurationSet(e){this.clearAlarmConfigForm=this.fb.group({alarmDetailsBuildJs:[e?e.alarmDetailsBuildJs:null,[k.required]],alarmType:[e?e.alarmType:null,[k.required]]})}testScript(){const e=this.clearAlarmConfigForm.get("alarmDetailsBuildJs").value;this.nodeScriptTestService.testNodeScript(e,"json",this.translate.instant("tb.rulenode.details"),"Details",["msg","metadata","msgType"],this.ruleNodeId,"rulenode/clear_alarm_node_script_fn").subscribe((e=>{e&&this.clearAlarmConfigForm.get("alarmDetailsBuildJs").setValue(e)}))}onValidate(){this.jsFuncComponent.validateOnSubmit()}}e("ClearAlarmConfigComponent",Ze),Ze.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ze,deps:[{token:T.Store},{token:q.FormBuilder},{token:Q.NodeScriptTestService},{token:P.TranslateService}],target:t.ɵɵFactoryTarget.Component}),Ze.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Ze,selector:"tb-action-node-clear-alarm-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0,static:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n \n \n
\n \n
\n \n tb.rulenode.alarm-type\n \n \n {{ \'tb.rulenode.alarm-type-required\' | translate }}\n \n \n \n
\n',components:[{type:Y.JsFuncComponent,selector:"tb-js-func",inputs:["functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","noValidate","required"]},{type:J.MatButton,selector:"button[mat-button], button[mat-raised-button], button[mat-icon-button], button[mat-fab], button[mat-mini-fab], button[mat-stroked-button], button[mat-flat-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:D.MatLabel,selector:"mat-label"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ze,decorators:[{type:o,args:[{selector:"tb-action-node-clear-alarm-config",templateUrl:"./clear-alarm-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder},{type:Q.NodeScriptTestService},{type:P.TranslateService}]},propDecorators:{jsFuncComponent:[{type:a,args:["jsFuncComponent",{static:!0}]}]}});class Xe extends s{constructor(e,t,o,r){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=o,this.translate=r,this.alarmSeverities=Object.keys(d),this.alarmSeverityTranslationMap=f,this.separatorKeysCodes=[Z,X,ee]}configForm(){return this.createAlarmConfigForm}onConfigurationSet(e){this.createAlarmConfigForm=this.fb.group({alarmDetailsBuildJs:[e?e.alarmDetailsBuildJs:null,[k.required]],useMessageAlarmData:[!!e&&e.useMessageAlarmData,[]],overwriteAlarmDetails:[!!e&&e.overwriteAlarmDetails,[]],alarmType:[e?e.alarmType:null,[]],severity:[e?e.severity:null,[]],propagate:[!!e&&e.propagate,[]],relationTypes:[e?e.relationTypes:null,[]],propagateToOwner:[!!e&&e.propagateToOwner,[]],propagateToTenant:[!!e&&e.propagateToTenant,[]],dynamicSeverity:!1}),this.createAlarmConfigForm.get("dynamicSeverity").valueChanges.subscribe((e=>{e?this.createAlarmConfigForm.get("severity").patchValue("",{emitEvent:!1}):this.createAlarmConfigForm.get("severity").patchValue(this.alarmSeverities[0],{emitEvent:!1})}))}validatorTriggers(){return["useMessageAlarmData"]}updateValidators(e){this.createAlarmConfigForm.get("useMessageAlarmData").value?(this.createAlarmConfigForm.get("alarmType").setValidators([]),this.createAlarmConfigForm.get("severity").setValidators([])):(this.createAlarmConfigForm.get("alarmType").setValidators([k.required]),this.createAlarmConfigForm.get("severity").setValidators([k.required])),this.createAlarmConfigForm.get("alarmType").updateValueAndValidity({emitEvent:e}),this.createAlarmConfigForm.get("severity").updateValueAndValidity({emitEvent:e})}testScript(){const e=this.createAlarmConfigForm.get("alarmDetailsBuildJs").value;this.nodeScriptTestService.testNodeScript(e,"json",this.translate.instant("tb.rulenode.details"),"Details",["msg","metadata","msgType"],this.ruleNodeId,"rulenode/create_alarm_node_script_fn").subscribe((e=>{e&&this.createAlarmConfigForm.get("alarmDetailsBuildJs").setValue(e)}))}removeKey(e,t){const o=this.createAlarmConfigForm.get(t).value,r=o.indexOf(e);r>=0&&(o.splice(r,1),this.createAlarmConfigForm.get(t).setValue(o,{emitEvent:!0}))}addKey(e,t){const o=e.input;let r=e.value;if((r||"").trim()){r=r.trim();let e=this.createAlarmConfigForm.get(t).value;e&&-1!==e.indexOf(r)||(e||(e=[]),e.push(r),this.createAlarmConfigForm.get(t).setValue(e,{emitEvent:!0}))}o&&(o.value="")}onValidate(){const e=this.createAlarmConfigForm.get("useMessageAlarmData").value,t=this.createAlarmConfigForm.get("overwriteAlarmDetails").value;e&&!t||this.jsFuncComponent.validateOnSubmit()}}e("CreateAlarmConfigComponent",Xe),Xe.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Xe,deps:[{token:T.Store},{token:q.FormBuilder},{token:Q.NodeScriptTestService},{token:P.TranslateService}],target:t.ɵɵFactoryTarget.Component}),Xe.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Xe,selector:"tb-action-node-create-alarm-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0,static:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'tb.rulenode.use-message-alarm-data\' | translate }}\n \n \n {{ \'tb.rulenode.overwrite-alarm-details\' | translate }}\n \n
\n \n \n \n
\n \n
\n
\n
\n \n tb.rulenode.alarm-type\n \n \n {{ \'tb.rulenode.alarm-type-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.use-alarm-severity-pattern\' | translate }}\n \n \n tb.rulenode.alarm-severity\n \n \n {{ alarmSeverityTranslationMap.get(severity) | translate }}\n \n \n \n {{ \'tb.rulenode.alarm-severity-required\' | translate }}\n \n \n \n tb.rulenode.alarm-severity-pattern\n \n \n {{ \'tb.rulenode.alarm-severity-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.propagate\' | translate }}\n \n
\n \n tb.rulenode.relation-types-list\n \n \n {{key}}\n close\n \n \n \n tb.rulenode.relation-types-list-hint\n \n
\n \n {{ \'tb.rulenode.propagate-to-owner\' | translate }}\n \n \n {{ \'tb.rulenode.propagate-to-tenant\' | translate }}\n \n
\n
\n',components:[{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:Y.JsFuncComponent,selector:"tb-js-func",inputs:["functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","noValidate","required"]},{type:J.MatButton,selector:"button[mat-button], button[mat-raised-button], button[mat-icon-button], button[mat-fab], button[mat-mini-fab], button[mat-stroked-button], button[mat-flat-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:te.MatChipList,selector:"mat-chip-list",inputs:["aria-orientation","multiple","compareWith","value","required","placeholder","disabled","selectable","tabIndex","errorStateMatcher"],outputs:["change","valueChange"],exportAs:["matChipList"]},{type:oe.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:re.DefaultShowHideDirective,selector:" [fxShow], [fxShow.print], [fxShow.xs], [fxShow.sm], [fxShow.md], [fxShow.lg], [fxShow.xl], [fxShow.lt-sm], [fxShow.lt-md], [fxShow.lt-lg], [fxShow.lt-xl], [fxShow.gt-xs], [fxShow.gt-sm], [fxShow.gt-md], [fxShow.gt-lg], [fxHide], [fxHide.print], [fxHide.xs], [fxHide.sm], [fxHide.md], [fxHide.lg], [fxHide.xl], [fxHide.lt-sm], [fxHide.lt-md], [fxHide.lt-lg], [fxHide.lt-xl], [fxHide.gt-xs], [fxHide.gt-sm], [fxHide.gt-md], [fxHide.gt-lg]",inputs:["fxShow","fxShow.print","fxShow.xs","fxShow.sm","fxShow.md","fxShow.lg","fxShow.xl","fxShow.lt-sm","fxShow.lt-md","fxShow.lt-lg","fxShow.lt-xl","fxShow.gt-xs","fxShow.gt-sm","fxShow.gt-md","fxShow.gt-lg","fxHide","fxHide.print","fxHide.xs","fxHide.sm","fxHide.md","fxHide.lg","fxHide.xl","fxHide.lt-sm","fxHide.lt-md","fxHide.lt-lg","fxHide.lt-xl","fxHide.gt-xs","fxHide.gt-sm","fxHide.gt-md","fxHide.gt-lg"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:D.MatLabel,selector:"mat-label"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:te.MatChip,selector:"mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]",inputs:["color","disableRipple","tabIndex","selected","value","selectable","disabled","removable"],outputs:["selectionChange","destroyed","removed"],exportAs:["matChip"]},{type:te.MatChipRemove,selector:"[matChipRemove]"},{type:te.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputSeparatorKeyCodes","placeholder","id","matChipInputFor","matChipInputAddOnBlur","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Xe,decorators:[{type:o,args:[{selector:"tb-action-node-create-alarm-config",templateUrl:"./create-alarm-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder},{type:Q.NodeScriptTestService},{type:P.TranslateService}]},propDecorators:{jsFuncComponent:[{type:a,args:["jsFuncComponent",{static:!0}]}]}});class et extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.directionTypes=Object.keys(c),this.directionTypeTranslations=g,this.entityType=x}configForm(){return this.createRelationConfigForm}onConfigurationSet(e){this.createRelationConfigForm=this.fb.group({direction:[e?e.direction:null,[k.required]],entityType:[e?e.entityType:null,[k.required]],entityNamePattern:[e?e.entityNamePattern:null,[]],entityTypePattern:[e?e.entityTypePattern:null,[]],relationType:[e?e.relationType:null,[k.required]],createEntityIfNotExists:[!!e&&e.createEntityIfNotExists,[]],removeCurrentRelations:[!!e&&e.removeCurrentRelations,[]],changeOriginatorToRelatedEntity:[!!e&&e.changeOriginatorToRelatedEntity,[]],entityCacheExpiration:[e?e.entityCacheExpiration:null,[k.required,k.min(0)]]})}validatorTriggers(){return["entityType"]}updateValidators(e){const t=this.createRelationConfigForm.get("entityType").value;t?this.createRelationConfigForm.get("entityNamePattern").setValidators([k.required,k.pattern(/.*\S.*/)]):this.createRelationConfigForm.get("entityNamePattern").setValidators([]),!t||t!==x.DEVICE&&t!==x.ASSET?this.createRelationConfigForm.get("entityTypePattern").setValidators([]):this.createRelationConfigForm.get("entityTypePattern").setValidators([k.required,k.pattern(/.*\S.*/)]),this.createRelationConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e}),this.createRelationConfigForm.get("entityTypePattern").updateValueAndValidity({emitEvent:e})}prepareOutputConfig(e){return e.entityNamePattern=e.entityNamePattern?e.entityNamePattern.trim():null,e.entityTypePattern=e.entityTypePattern?e.entityTypePattern.trim():null,e}}e("CreateRelationConfigComponent",et),et.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:et,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),et.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:et,selector:"tb-action-node-create-relation-config",usesInheritance:!0,ngImport:t,template:'
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.entity-type-pattern\n \n \n {{ \'tb.rulenode.entity-type-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.relation-type-pattern\n \n \n {{ \'tb.rulenode.relation-type-pattern-required\' | translate }}\n \n \n \n
\n \n {{ \'tb.rulenode.create-entity-if-not-exists\' | translate }}\n \n
tb.rulenode.create-entity-if-not-exists-hint
\n
\n \n {{ \'tb.rulenode.remove-current-relations\' | translate }}\n \n
tb.rulenode.remove-current-relations-hint
\n \n {{ \'tb.rulenode.change-originator-to-related-entity\' | translate }}\n \n
tb.rulenode.change-originator-to-related-entity-hint
\n \n tb.rulenode.entity-cache-expiration\n \n \n {{ \'tb.rulenode.entity-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.entity-cache-expiration-range\' | translate }}\n \n tb.rulenode.entity-cache-expiration-hint\n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:ae.EntityTypeSelectComponent,selector:"tb-entity-type-select",inputs:["allowedEntityTypes","useAliasEntityTypes","showLabel","required","disabled"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:E.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:et,decorators:[{type:o,args:[{selector:"tb-action-node-create-relation-config",templateUrl:"./create-relation-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class tt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.directionTypes=Object.keys(c),this.directionTypeTranslations=g,this.entityType=x}configForm(){return this.deleteRelationConfigForm}onConfigurationSet(e){this.deleteRelationConfigForm=this.fb.group({deleteForSingleEntity:[!!e&&e.deleteForSingleEntity,[]],direction:[e?e.direction:null,[k.required]],entityType:[e?e.entityType:null,[]],entityNamePattern:[e?e.entityNamePattern:null,[]],relationType:[e?e.relationType:null,[k.required]],entityCacheExpiration:[e?e.entityCacheExpiration:null,[k.required,k.min(0)]]})}validatorTriggers(){return["deleteForSingleEntity","entityType"]}updateValidators(e){const t=this.deleteRelationConfigForm.get("deleteForSingleEntity").value,o=this.deleteRelationConfigForm.get("entityType").value;t?this.deleteRelationConfigForm.get("entityType").setValidators([k.required]):this.deleteRelationConfigForm.get("entityType").setValidators([]),t&&o?this.deleteRelationConfigForm.get("entityNamePattern").setValidators([k.required,k.pattern(/.*\S.*/)]):this.deleteRelationConfigForm.get("entityNamePattern").setValidators([]),this.deleteRelationConfigForm.get("entityType").updateValueAndValidity({emitEvent:!1}),this.deleteRelationConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e})}prepareOutputConfig(e){return e.entityNamePattern=e.entityNamePattern?e.entityNamePattern.trim():null,e}}e("DeleteRelationConfigComponent",tt),tt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:tt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),tt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:tt,selector:"tb-action-node-delete-relation-config",usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'tb.rulenode.delete-relation-to-specific-entity\' | translate }}\n \n
tb.rulenode.delete-relation-hint
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.relation-type-pattern\n \n \n {{ \'tb.rulenode.relation-type-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.entity-cache-expiration\n \n \n {{ \'tb.rulenode.entity-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.entity-cache-expiration-range\' | translate }}\n \n tb.rulenode.entity-cache-expiration-hint\n \n
\n',components:[{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:ae.EntityTypeSelectComponent,selector:"tb-entity-type-select",inputs:["allowedEntityTypes","useAliasEntityTypes","showLabel","required","disabled"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:D.MatLabel,selector:"mat-label"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:E.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:tt,decorators:[{type:o,args:[{selector:"tb-action-node-delete-relation-config",templateUrl:"./delete-relation-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class ot extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.deviceProfile}onConfigurationSet(e){this.deviceProfile=this.fb.group({persistAlarmRulesState:[!!e&&e.persistAlarmRulesState,k.required],fetchAlarmRulesStateOnStart:[!!e&&e.fetchAlarmRulesStateOnStart,k.required]})}}e("DeviceProfileConfigComponent",ot),ot.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ot,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),ot.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:ot,selector:"tb-device-profile-config",usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'tb.rulenode.persist-alarm-rules\' | translate }}\n \n \n {{ \'tb.rulenode.fetch-alarm-rules\' | translate }}\n \n
\n',components:[{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ot,decorators:[{type:o,args:[{selector:"tb-device-profile-config",templateUrl:"./device-profile-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class rt extends s{constructor(e,t,o,r){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=o,this.translate=r}configForm(){return this.generatorConfigForm}onConfigurationSet(e){this.generatorConfigForm=this.fb.group({msgCount:[e?e.msgCount:null,[k.required,k.min(0)]],periodInSeconds:[e?e.periodInSeconds:null,[k.required,k.min(1)]],originator:[e?e.originator:null,[]],jsScript:[e?e.jsScript:null,[k.required]]})}prepareInputConfig(e){return e&&(e.originatorId&&e.originatorType?e.originator={id:e.originatorId,entityType:e.originatorType}:e.originator=null,delete e.originatorId,delete e.originatorType),e}prepareOutputConfig(e){return e.originator?(e.originatorId=e.originator.id,e.originatorType=e.originator.entityType):(e.originatorId=null,e.originatorType=null),delete e.originator,e}testScript(){const e=this.generatorConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(e,"generate",this.translate.instant("tb.rulenode.generator"),"Generate",["prevMsg","prevMetadata","prevMsgType"],this.ruleNodeId,"rulenode/generator_node_script_fn").subscribe((e=>{e&&this.generatorConfigForm.get("jsScript").setValue(e)}))}onValidate(){this.jsFuncComponent.validateOnSubmit()}}e("GeneratorConfigComponent",rt),rt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:rt,deps:[{token:T.Store},{token:q.FormBuilder},{token:Q.NodeScriptTestService},{token:P.TranslateService}],target:t.ɵɵFactoryTarget.Component}),rt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:rt,selector:"tb-action-node-generator-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0,static:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.message-count\n \n \n {{ \'tb.rulenode.message-count-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-message-count-message\' | translate }}\n \n \n \n tb.rulenode.period-seconds\n \n \n {{ \'tb.rulenode.period-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-period-seconds-message\' | translate }}\n \n \n
\n \n \n \n
\n \n \n \n
\n \n
\n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:ne.EntitySelectComponent,selector:"tb-entity-select",inputs:["allowedEntityTypes","useAliasEntityTypes","required","disabled"]},{type:Y.JsFuncComponent,selector:"tb-js-func",inputs:["functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","noValidate","required"]},{type:J.MatButton,selector:"button[mat-button], button[mat-raised-button], button[mat-icon-button], button[mat-fab], button[mat-mini-fab], button[mat-stroked-button], button[mat-flat-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:rt,decorators:[{type:o,args:[{selector:"tb-action-node-generator-config",templateUrl:"./generator-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder},{type:Q.NodeScriptTestService},{type:P.TranslateService}]},propDecorators:{jsFuncComponent:[{type:a,args:["jsFuncComponent",{static:!0}]}]}});class at extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.perimeterType=Ae,this.perimeterTypes=Object.keys(Ae),this.perimeterTypeTranslationMap=Ge,this.rangeUnits=Object.keys(Ee),this.rangeUnitTranslationMap=Pe,this.timeUnits=Object.keys(De),this.timeUnitsTranslationMap=Ve}configForm(){return this.geoActionConfigForm}onConfigurationSet(e){this.geoActionConfigForm=this.fb.group({latitudeKeyName:[e?e.latitudeKeyName:null,[k.required]],longitudeKeyName:[e?e.longitudeKeyName:null,[k.required]],perimeterType:[e?e.perimeterType:null,[k.required]],fetchPerimeterInfoFromMessageMetadata:[!!e&&e.fetchPerimeterInfoFromMessageMetadata,[]],perimeterKeyName:[e?e.perimeterKeyName:null,[]],centerLatitude:[e?e.centerLatitude:null,[]],centerLongitude:[e?e.centerLatitude:null,[]],range:[e?e.range:null,[]],rangeUnit:[e?e.rangeUnit:null,[]],polygonsDefinition:[e?e.polygonsDefinition:null,[]],minInsideDuration:[e?e.minInsideDuration:null,[k.required,k.min(1),k.max(2147483647)]],minInsideDurationTimeUnit:[e?e.minInsideDurationTimeUnit:null,[k.required]],minOutsideDuration:[e?e.minOutsideDuration:null,[k.required,k.min(1),k.max(2147483647)]],minOutsideDurationTimeUnit:[e?e.minOutsideDurationTimeUnit:null,[k.required]]})}validatorTriggers(){return["fetchPerimeterInfoFromMessageMetadata","perimeterType"]}updateValidators(e){const t=this.geoActionConfigForm.get("fetchPerimeterInfoFromMessageMetadata").value,o=this.geoActionConfigForm.get("perimeterType").value;t?this.geoActionConfigForm.get("perimeterKeyName").setValidators([k.required]):this.geoActionConfigForm.get("perimeterKeyName").setValidators([]),t||o!==Ae.CIRCLE?(this.geoActionConfigForm.get("centerLatitude").setValidators([]),this.geoActionConfigForm.get("centerLongitude").setValidators([]),this.geoActionConfigForm.get("range").setValidators([]),this.geoActionConfigForm.get("rangeUnit").setValidators([])):(this.geoActionConfigForm.get("centerLatitude").setValidators([k.required,k.min(-90),k.max(90)]),this.geoActionConfigForm.get("centerLongitude").setValidators([k.required,k.min(-180),k.max(180)]),this.geoActionConfigForm.get("range").setValidators([k.required,k.min(0)]),this.geoActionConfigForm.get("rangeUnit").setValidators([k.required])),t||o!==Ae.POLYGON?this.geoActionConfigForm.get("polygonsDefinition").setValidators([]):this.geoActionConfigForm.get("polygonsDefinition").setValidators([k.required]),this.geoActionConfigForm.get("perimeterKeyName").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("centerLatitude").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("centerLongitude").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("range").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("rangeUnit").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("polygonsDefinition").updateValueAndValidity({emitEvent:e})}}e("GpsGeoActionConfigComponent",at),at.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:at,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),at.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:at,selector:"tb-action-node-gps-geofencing-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.latitude-key-name\n \n \n {{ \'tb.rulenode.latitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.longitude-key-name\n \n \n {{ \'tb.rulenode.longitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \n \n \n {{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}\n \n \n tb.rulenode.perimeter-key-name\n \n \n {{ \'tb.rulenode.perimeter-key-name-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.circle-center-latitude\n \n \n {{ \'tb.rulenode.circle-center-latitude-required\' | translate }}\n \n \n \n tb.rulenode.circle-center-longitude\n \n \n {{ \'tb.rulenode.circle-center-longitude-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.range\n \n \n {{ \'tb.rulenode.range-required\' | translate }}\n \n \n \n tb.rulenode.range-units\n \n \n {{ rangeUnitTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n \n tb.rulenode.polygon-definition\n \n \n {{ \'tb.rulenode.polygon-definition-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.min-inside-duration\n \n \n {{ \'tb.rulenode.min-inside-duration-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.min-inside-duration-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n tb.rulenode.min-outside-duration\n \n \n {{ \'tb.rulenode.min-outside-duration-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.min-outside-duration-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:E.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:at,decorators:[{type:o,args:[{selector:"tb-action-node-gps-geofencing-config",templateUrl:"./gps-geo-action-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class nt extends y{constructor(e,t,o,r){super(e),this.store=e,this.translate=t,this.injector=o,this.fb=r,this.propagateChange=null,this.valueChangeSubscription=null}get required(){return this.requiredValue}set required(e){this.requiredValue=le(e)}ngOnInit(){this.ngControl=this.injector.get(M),null!=this.ngControl&&(this.ngControl.valueAccessor=this),this.kvListFormGroup=this.fb.group({}),this.kvListFormGroup.addControl("keyVals",this.fb.array([]))}keyValsFormArray(){return this.kvListFormGroup.get("keyVals")}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}setDisabledState(e){this.disabled=e,this.disabled?this.kvListFormGroup.disable({emitEvent:!1}):this.kvListFormGroup.enable({emitEvent:!1})}writeValue(e){this.valueChangeSubscription&&this.valueChangeSubscription.unsubscribe();const t=[];if(e)for(const o of Object.keys(e))Object.prototype.hasOwnProperty.call(e,o)&&t.push(this.fb.group({key:[o,[k.required]],value:[e[o],[k.required]]}));this.kvListFormGroup.setControl("keyVals",this.fb.array(t)),this.valueChangeSubscription=this.kvListFormGroup.valueChanges.subscribe((()=>{this.updateModel()}))}removeKeyVal(e){this.kvListFormGroup.get("keyVals").removeAt(e)}addKeyVal(){this.kvListFormGroup.get("keyVals").push(this.fb.group({key:["",[k.required]],value:["",[k.required]]}))}validate(e){return!this.kvListFormGroup.get("keyVals").value.length&&this.required?{kvMapRequired:!0}:this.kvListFormGroup.valid?null:{kvFieldsRequired:!0}}updateModel(){const e=this.kvListFormGroup.get("keyVals").value;if(this.required&&!e.length||!this.kvListFormGroup.valid)this.propagateChange(null);else{const t={};e.forEach((e=>{t[e.key]=e.value})),this.propagateChange(t)}}}e("KvMapConfigComponent",nt),nt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:nt,deps:[{token:T.Store},{token:P.TranslateService},{token:t.Injector},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),nt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:nt,selector:"tb-kv-map-config",inputs:{disabled:"disabled",requiredText:"requiredText",keyText:"keyText",keyRequiredText:"keyRequiredText",valText:"valText",valRequiredText:"valRequiredText",hintText:"hintText",required:"required"},providers:[{provide:S,useExisting:n((()=>nt)),multi:!0},{provide:A,useExisting:n((()=>nt)),multi:!0}],usesInheritance:!0,ngImport:t,template:'
\n
\n {{ keyText | translate }}\n {{ valText | translate }}\n \n
\n
\n
\n \n \n \n \n {{ keyRequiredText | translate }}\n \n \n \n \n \n \n {{ valRequiredText | translate }}\n \n \n \n
\n
\n
\n \n
\n \n
\n
\n',styles:[":host .tb-kv-map-config{margin-bottom:16px}:host .tb-kv-map-config .header{padding-left:5px;padding-right:5px;padding-bottom:5px}:host .tb-kv-map-config .header .cell{padding-left:5px;padding-right:5px;color:#0000008a;font-size:12px;font-weight:700;white-space:nowrap}:host .tb-kv-map-config .body{padding-left:5px;padding-right:5px;padding-bottom:20px;max-height:300px;overflow:auto}:host .tb-kv-map-config .body .cell{padding-left:5px;padding-right:5px}:host ::ng-deep .tb-kv-map-config .body mat-form-field.cell{margin:0}:host ::ng-deep .tb-kv-map-config .body mat-form-field.cell .mat-form-field-infix{border-top:0}:host ::ng-deep .tb-kv-map-config .body button.mat-button{margin:0;align-self:baseline}\n"],components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:J.MatButton,selector:"button[mat-button], button[mat-raised-button], button[mat-icon-button], button[mat-fab], button[mat-mini-fab], button[mat-stroked-button], button[mat-flat-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]},{type:oe.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{type:ie.TbErrorComponent,selector:"tb-error",inputs:["error"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:E.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{type:re.DefaultShowHideDirective,selector:" [fxShow], [fxShow.print], [fxShow.xs], [fxShow.sm], [fxShow.md], [fxShow.lg], [fxShow.xl], [fxShow.lt-sm], [fxShow.lt-md], [fxShow.lt-lg], [fxShow.lt-xl], [fxShow.gt-xs], [fxShow.gt-sm], [fxShow.gt-md], [fxShow.gt-lg], [fxHide], [fxHide.print], [fxHide.xs], [fxHide.sm], [fxHide.md], [fxHide.lg], [fxHide.xl], [fxHide.lt-sm], [fxHide.lt-md], [fxHide.lt-lg], [fxHide.lt-xl], [fxHide.gt-xs], [fxHide.gt-sm], [fxHide.gt-md], [fxHide.gt-lg]",inputs:["fxShow","fxShow.print","fxShow.xs","fxShow.sm","fxShow.md","fxShow.lg","fxShow.xl","fxShow.lt-sm","fxShow.lt-md","fxShow.lt-lg","fxShow.lt-xl","fxShow.gt-xs","fxShow.gt-sm","fxShow.gt-md","fxShow.gt-lg","fxHide","fxHide.print","fxHide.xs","fxHide.sm","fxHide.md","fxHide.lg","fxHide.xl","fxHide.lt-sm","fxHide.lt-md","fxHide.lt-lg","fxHide.lt-xl","fxHide.gt-xs","fxHide.gt-sm","fxHide.gt-md","fxHide.gt-lg"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:E.DefaultLayoutAlignDirective,selector:" [fxLayoutAlign], [fxLayoutAlign.xs], [fxLayoutAlign.sm], [fxLayoutAlign.md], [fxLayoutAlign.lg], [fxLayoutAlign.xl], [fxLayoutAlign.lt-sm], [fxLayoutAlign.lt-md], [fxLayoutAlign.lt-lg], [fxLayoutAlign.lt-xl], [fxLayoutAlign.gt-xs], [fxLayoutAlign.gt-sm], [fxLayoutAlign.gt-md], [fxLayoutAlign.gt-lg]",inputs:["fxLayoutAlign","fxLayoutAlign.xs","fxLayoutAlign.sm","fxLayoutAlign.md","fxLayoutAlign.lg","fxLayoutAlign.xl","fxLayoutAlign.lt-sm","fxLayoutAlign.lt-md","fxLayoutAlign.lt-lg","fxLayoutAlign.lt-xl","fxLayoutAlign.gt-xs","fxLayoutAlign.gt-sm","fxLayoutAlign.gt-md","fxLayoutAlign.gt-lg"]},{type:q.FormArrayName,selector:"[formArrayName]",inputs:["formArrayName"]},{type:D.MatLabel,selector:"mat-label"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlDirective,selector:"[formControl]",inputs:["disabled","formControl","ngModel"],outputs:["ngModelChange"],exportAs:["ngForm"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:se.MatTooltip,selector:"[matTooltip]",exportAs:["matTooltip"]}],pipes:{translate:P.TranslatePipe,async:w.AsyncPipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:nt,decorators:[{type:o,args:[{selector:"tb-kv-map-config",templateUrl:"./kv-map-config.component.html",styleUrls:["./kv-map-config.component.scss"],providers:[{provide:S,useExisting:n((()=>nt)),multi:!0},{provide:A,useExisting:n((()=>nt)),multi:!0}]}]}],ctorParameters:function(){return[{type:T.Store},{type:P.TranslateService},{type:t.Injector},{type:q.FormBuilder}]},propDecorators:{disabled:[{type:l}],requiredText:[{type:l}],keyText:[{type:l}],keyRequiredText:[{type:l}],valText:[{type:l}],valRequiredText:[{type:l}],hintText:[{type:l}],required:[{type:l}]}});class lt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.ackValues=["all","-1","0","1"],this.ToByteStandartCharsetTypesValues=$e,this.ToByteStandartCharsetTypeTranslationMap=We}configForm(){return this.kafkaConfigForm}onConfigurationSet(e){this.kafkaConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[k.required]],bootstrapServers:[e?e.bootstrapServers:null,[k.required]],retries:[e?e.retries:null,[k.min(0)]],batchSize:[e?e.batchSize:null,[k.min(0)]],linger:[e?e.linger:null,[k.min(0)]],bufferMemory:[e?e.bufferMemory:null,[k.min(0)]],acks:[e?e.acks:null,[k.required]],keySerializer:[e?e.keySerializer:null,[k.required]],valueSerializer:[e?e.valueSerializer:null,[k.required]],otherProperties:[e?e.otherProperties:null,[]],addMetadataKeyValuesAsKafkaHeaders:[!!e&&e.addMetadataKeyValuesAsKafkaHeaders,[]],kafkaHeadersCharset:[e?e.kafkaHeadersCharset:null,[]]})}validatorTriggers(){return["addMetadataKeyValuesAsKafkaHeaders"]}updateValidators(e){this.kafkaConfigForm.get("addMetadataKeyValuesAsKafkaHeaders").value?this.kafkaConfigForm.get("kafkaHeadersCharset").setValidators([k.required]):this.kafkaConfigForm.get("kafkaHeadersCharset").setValidators([]),this.kafkaConfigForm.get("kafkaHeadersCharset").updateValueAndValidity({emitEvent:e})}}e("KafkaConfigComponent",lt),lt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:lt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),lt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:lt,selector:"tb-action-node-kafka-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.topic-pattern\n \n \n {{ \'tb.rulenode.topic-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.bootstrap-servers\n \n \n {{ \'tb.rulenode.bootstrap-servers-required\' | translate }}\n \n \n \n tb.rulenode.retries\n \n \n {{ \'tb.rulenode.min-retries-message\' | translate }}\n \n \n \n tb.rulenode.batch-size-bytes\n \n \n {{ \'tb.rulenode.min-batch-size-bytes-message\' | translate }}\n \n \n \n tb.rulenode.linger-ms\n \n \n {{ \'tb.rulenode.min-linger-ms-message\' | translate }}\n \n \n \n tb.rulenode.buffer-memory-bytes\n \n \n {{ \'tb.rulenode.min-buffer-memory-bytes-message\' | translate }}\n \n \n \n tb.rulenode.acks\n \n \n {{ ackValue }}\n \n \n \n \n tb.rulenode.key-serializer\n \n \n {{ \'tb.rulenode.key-serializer-required\' | translate }}\n \n \n \n tb.rulenode.value-serializer\n \n \n {{ \'tb.rulenode.value-serializer-required\' | translate }}\n \n \n \n \n \n \n {{ \'tb.rulenode.add-metadata-key-values-as-kafka-headers\' | translate }}\n \n
tb.rulenode.add-metadata-key-values-as-kafka-headers-hint
\n \n tb.rulenode.charset-encoding\n \n \n {{ ToByteStandartCharsetTypeTranslationMap.get(charset) | translate }}\n \n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:nt,selector:"tb-kv-map-config",inputs:["disabled","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:lt,decorators:[{type:o,args:[{selector:"tb-action-node-kafka-config",templateUrl:"./kafka-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class it extends s{constructor(e,t,o,r){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=o,this.translate=r}configForm(){return this.logConfigForm}onConfigurationSet(e){this.logConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[k.required]]})}testScript(){const e=this.logConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(e,"string",this.translate.instant("tb.rulenode.to-string"),"ToString",["msg","metadata","msgType"],this.ruleNodeId,"rulenode/log_node_script_fn").subscribe((e=>{e&&this.logConfigForm.get("jsScript").setValue(e)}))}onValidate(){this.jsFuncComponent.validateOnSubmit()}}e("LogConfigComponent",it),it.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:it,deps:[{token:T.Store},{token:q.FormBuilder},{token:Q.NodeScriptTestService},{token:P.TranslateService}],target:t.ɵɵFactoryTarget.Component}),it.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:it,selector:"tb-action-node-log-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0,static:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n \n \n
\n \n
\n
\n',components:[{type:Y.JsFuncComponent,selector:"tb-js-func",inputs:["functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","noValidate","required"]},{type:J.MatButton,selector:"button[mat-button], button[mat-raised-button], button[mat-icon-button], button[mat-fab], button[mat-mini-fab], button[mat-stroked-button], button[mat-flat-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:it,decorators:[{type:o,args:[{selector:"tb-action-node-log-config",templateUrl:"./log-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder},{type:Q.NodeScriptTestService},{type:P.TranslateService}]},propDecorators:{jsFuncComponent:[{type:a,args:["jsFuncComponent",{static:!0}]}]}});class st extends y{constructor(e,t){super(e),this.store=e,this.fb=t,this.subscriptions=[],this.disableCertPemCredentials=!1,this.passwordFieldRquired=!0,this.allCredentialsTypes=Be,this.credentialsTypeTranslationsMap=je,this.propagateChange=null}get required(){return this.requiredValue}set required(e){this.requiredValue=le(e)}ngOnInit(){this.credentialsConfigFormGroup=this.fb.group({type:[null,[k.required]],username:[null,[]],password:[null,[]],caCert:[null,[]],caCertFileName:[null,[]],privateKey:[null,[]],privateKeyFileName:[null,[]],cert:[null,[]],certFileName:[null,[]]}),this.subscriptions.push(this.credentialsConfigFormGroup.valueChanges.pipe(me()).subscribe((()=>{this.updateView()}))),this.subscriptions.push(this.credentialsConfigFormGroup.get("type").valueChanges.subscribe((()=>{this.credentialsTypeChanged()})))}ngOnChanges(e){for(const t of Object.keys(e)){const o=e[t];if(!o.firstChange&&o.currentValue!==o.previousValue&&o.currentValue&&"disableCertPemCredentials"===t){"cert.PEM"===this.credentialsConfigFormGroup.get("type").value&&setTimeout((()=>{this.credentialsConfigFormGroup.get("type").patchValue("anonymous",{emitEvent:!0})}))}}}ngOnDestroy(){this.subscriptions.forEach((e=>e.unsubscribe()))}writeValue(e){$(e)&&(this.credentialsConfigFormGroup.reset(e,{emitEvent:!1}),this.updateValidators(!1))}setDisabledState(e){e?this.credentialsConfigFormGroup.disable():(this.credentialsConfigFormGroup.enable(),this.updateValidators())}updateView(){let e=this.credentialsConfigFormGroup.value;const t=e.type;switch(t){case"anonymous":e={type:t};break;case"basic":e={type:t,username:e.username,password:e.password};break;case"cert.PEM":delete e.username}this.propagateChange(e)}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}validate(e){return this.credentialsConfigFormGroup.valid?null:{credentialsConfig:{valid:!1}}}credentialsTypeChanged(){this.credentialsConfigFormGroup.patchValue({username:null,password:null,caCert:null,caCertFileName:null,privateKey:null,privateKeyFileName:null,cert:null,certFileName:null}),this.updateValidators()}updateValidators(e=!1){const t=this.credentialsConfigFormGroup.get("type").value;switch(e&&this.credentialsConfigFormGroup.reset({type:t},{emitEvent:!1}),this.credentialsConfigFormGroup.setValidators([]),this.credentialsConfigFormGroup.get("username").setValidators([]),this.credentialsConfigFormGroup.get("password").setValidators([]),t){case"anonymous":break;case"basic":this.credentialsConfigFormGroup.get("username").setValidators([k.required]),this.credentialsConfigFormGroup.get("password").setValidators(this.passwordFieldRquired?[k.required]:[]);break;case"cert.PEM":this.credentialsConfigFormGroup.setValidators([this.requiredFilesSelected(k.required,[["caCert","caCertFileName"],["privateKey","privateKeyFileName","cert","certFileName"]])])}this.credentialsConfigFormGroup.get("username").updateValueAndValidity({emitEvent:e}),this.credentialsConfigFormGroup.get("password").updateValueAndValidity({emitEvent:e}),this.credentialsConfigFormGroup.updateValueAndValidity({emitEvent:e})}requiredFilesSelected(e,t=null){return o=>{t||(t=[Object.keys(o.controls)]);return(null==o?void 0:o.controls)&&t.some((t=>t.every((t=>!e(o.controls[t])))))?null:{notAllRequiredFilesSelected:!0}}}}e("CredentialsConfigComponent",st),st.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:st,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),st.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:st,selector:"tb-credentials-config",inputs:{required:"required",disableCertPemCredentials:"disableCertPemCredentials",passwordFieldRquired:"passwordFieldRquired"},providers:[{provide:S,useExisting:n((()=>st)),multi:!0},{provide:A,useExisting:n((()=>st)),multi:!0}],usesInheritance:!0,usesOnChanges:!0,ngImport:t,template:'
\n \n \n tb.rulenode.credentials\n \n {{ credentialsTypeTranslationsMap.get(credentialsConfigFormGroup.get(\'type\').value) | translate }}\n \n \n \n \n tb.rulenode.credentials-type\n \n \n {{ credentialsTypeTranslationsMap.get(credentialsType) | translate }}\n \n \n \n {{ \'tb.rulenode.credentials-type-required\' | translate }}\n \n \n
\n \n \n \n \n tb.rulenode.username\n \n \n {{ \'tb.rulenode.username-required\' | translate }}\n \n \n \n tb.rulenode.password\n \n \n \n {{ \'tb.rulenode.password-required\' | translate }}\n \n \n \n \n
{{ \'tb.rulenode.credentials-pem-hint\' | translate }}
\n \n \n \n \n \n \n \n tb.rulenode.private-key-password\n \n \n \n
\n
\n
\n
\n
\n',components:[{type:B.MatExpansionPanel,selector:"mat-expansion-panel",inputs:["disabled","expanded","hideToggle","togglePosition"],outputs:["opened","closed","expandedChange","afterExpand","afterCollapse"],exportAs:["matExpansionPanel"]},{type:B.MatExpansionPanelHeader,selector:"mat-expansion-panel-header",inputs:["tabIndex","expandedHeight","collapsedHeight"]},{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:j.TogglePasswordComponent,selector:"tb-toggle-password"},{type:z.FileInputComponent,selector:"tb-file-input",inputs:["label","accept","noFileText","inputId","allowedExtensions","dropLabel","contentConvertFunction","required","requiredAsError","disabled","existingFileName","readAsBinary","workFromFileObj","multipleFile"],outputs:["fileNameChanged"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:B.MatExpansionPanelTitle,selector:"mat-panel-title"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:B.MatExpansionPanelDescription,selector:"mat-panel-description"},{type:B.MatExpansionPanelContent,selector:"ng-template[matExpansionPanelContent]"},{type:D.MatLabel,selector:"mat-label"},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:w.NgSwitch,selector:"[ngSwitch]",inputs:["ngSwitch"]},{type:w.NgSwitchCase,selector:"[ngSwitchCase]",inputs:["ngSwitchCase"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:D.MatSuffix,selector:"[matSuffix]"}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:st,decorators:[{type:o,args:[{selector:"tb-credentials-config",templateUrl:"./credentials-config.component.html",styleUrls:[],providers:[{provide:S,useExisting:n((()=>st)),multi:!0},{provide:A,useExisting:n((()=>st)),multi:!0}]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]},propDecorators:{required:[{type:l}],disableCertPemCredentials:[{type:l}],passwordFieldRquired:[{type:l}]}});class mt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.subscriptions=[]}configForm(){return this.mqttConfigForm}onConfigurationSet(e){this.mqttConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[k.required]],host:[e?e.host:null,[k.required]],port:[e?e.port:null,[k.required,k.min(1),k.max(65535)]],connectTimeoutSec:[e?e.connectTimeoutSec:null,[k.required,k.min(1),k.max(200)]],clientId:[e?e.clientId:null,[]],appendClientIdSuffix:[{value:!!e&&e.appendClientIdSuffix,disabled:!(e&&W(e.clientId))},[]],cleanSession:[!!e&&e.cleanSession,[]],ssl:[!!e&&e.ssl,[]],credentials:[e?e.credentials:null,[]]}),this.subscriptions.push(this.mqttConfigForm.get("clientId").valueChanges.subscribe((e=>{W(e)?this.mqttConfigForm.get("appendClientIdSuffix").enable({emitEvent:!1}):this.mqttConfigForm.get("appendClientIdSuffix").disable({emitEvent:!1})})))}ngOnDestroy(){this.subscriptions.forEach((e=>e.unsubscribe()))}}e("MqttConfigComponent",mt),mt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:mt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),mt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:mt,selector:"tb-action-node-mqtt-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.topic-pattern\n \n \n {{ \'tb.rulenode.topic-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.host\n \n \n {{ \'tb.rulenode.host-required\' | translate }}\n \n \n \n tb.rulenode.port\n \n \n {{ \'tb.rulenode.port-required\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n \n tb.rulenode.connect-timeout\n \n \n {{ \'tb.rulenode.connect-timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.connect-timeout-range\' | translate }}\n \n \n {{ \'tb.rulenode.connect-timeout-range\' | translate }}\n \n \n
\n \n tb.rulenode.client-id\n \n \n
tb.rulenode.client-id-hint
\n \n {{ \'tb.rulenode.append-client-id-suffix\' | translate }}\n \n
{{ "tb.rulenode.client-id-suffix-hint" | translate }}
\n \n {{ \'tb.rulenode.clean-session\' | translate }}\n \n \n {{ \'tb.rulenode.enable-ssl\' | translate }}\n \n \n
\n',styles:[":host .tb-mqtt-credentials-panel-group{margin:0 6px}:host .tb-hint.client-id{margin-top:-1.25em;max-width:-moz-fit-content;max-width:fit-content}:host mat-checkbox{padding-bottom:16px}\n"],components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:st,selector:"tb-credentials-config",inputs:["required","disableCertPemCredentials","passwordFieldRquired"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:E.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:mt,decorators:[{type:o,args:[{selector:"tb-action-node-mqtt-config",templateUrl:"./mqtt-config.component.html",styleUrls:["./mqtt-config.component.scss"]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class ut extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.msgCountConfigForm}onConfigurationSet(e){this.msgCountConfigForm=this.fb.group({interval:[e?e.interval:null,[k.required,k.min(1)]],telemetryPrefix:[e?e.telemetryPrefix:null,[k.required]]})}}e("MsgCountConfigComponent",ut),ut.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ut,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),ut.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:ut,selector:"tb-action-node-msg-count-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.interval-seconds\n \n \n {{ \'tb.rulenode.interval-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-interval-seconds-message\' | translate }}\n \n \n \n tb.rulenode.output-timeseries-key-prefix\n \n \n {{ \'tb.rulenode.output-timeseries-key-prefix-required\' | translate }}\n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ut,decorators:[{type:o,args:[{selector:"tb-action-node-msg-count-config",templateUrl:"./msg-count-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class pt extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.msgDelayConfigForm}onConfigurationSet(e){this.msgDelayConfigForm=this.fb.group({useMetadataPeriodInSecondsPatterns:[!!e&&e.useMetadataPeriodInSecondsPatterns,[]],periodInSeconds:[e?e.periodInSeconds:null,[]],periodInSecondsPattern:[e?e.periodInSecondsPattern:null,[]],maxPendingMsgs:[e?e.maxPendingMsgs:null,[k.required,k.min(1),k.max(1e5)]]})}validatorTriggers(){return["useMetadataPeriodInSecondsPatterns"]}updateValidators(e){this.msgDelayConfigForm.get("useMetadataPeriodInSecondsPatterns").value?(this.msgDelayConfigForm.get("periodInSecondsPattern").setValidators([k.required]),this.msgDelayConfigForm.get("periodInSeconds").setValidators([])):(this.msgDelayConfigForm.get("periodInSecondsPattern").setValidators([]),this.msgDelayConfigForm.get("periodInSeconds").setValidators([k.required,k.min(0)])),this.msgDelayConfigForm.get("periodInSecondsPattern").updateValueAndValidity({emitEvent:e}),this.msgDelayConfigForm.get("periodInSeconds").updateValueAndValidity({emitEvent:e})}}e("MsgDelayConfigComponent",pt),pt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:pt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),pt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:pt,selector:"tb-action-node-msg-delay-config",usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'tb.rulenode.use-metadata-period-in-seconds-patterns\' | translate }}\n \n
tb.rulenode.use-metadata-period-in-seconds-patterns-hint
\n \n tb.rulenode.period-seconds\n \n \n {{ \'tb.rulenode.period-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-period-0-seconds-message\' | translate }}\n \n \n \n \n tb.rulenode.period-in-seconds-pattern\n \n \n {{ \'tb.rulenode.period-in-seconds-pattern-required\' | translate }}\n \n \n \n \n \n tb.rulenode.max-pending-messages\n \n \n {{ \'tb.rulenode.max-pending-messages-required\' | translate }}\n \n \n {{ \'tb.rulenode.max-pending-messages-range\' | translate }}\n \n \n {{ \'tb.rulenode.max-pending-messages-range\' | translate }}\n \n \n
\n',components:[{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatLabel,selector:"mat-label"},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]},{type:q.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:pt,decorators:[{type:o,args:[{selector:"tb-action-node-msg-delay-config",templateUrl:"./msg-delay-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class dt extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.pubSubConfigForm}onConfigurationSet(e){this.pubSubConfigForm=this.fb.group({projectId:[e?e.projectId:null,[k.required]],topicName:[e?e.topicName:null,[k.required]],serviceAccountKey:[e?e.serviceAccountKey:null,[k.required]],serviceAccountKeyFileName:[e?e.serviceAccountKeyFileName:null,[k.required]],messageAttributes:[e?e.messageAttributes:null,[]]})}}e("PubSubConfigComponent",dt),dt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:dt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),dt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:dt,selector:"tb-action-node-pub-sub-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.gcp-project-id\n \n \n {{ \'tb.rulenode.gcp-project-id-required\' | translate }}\n \n \n \n tb.rulenode.pubsub-topic-name\n \n \n {{ \'tb.rulenode.pubsub-topic-name-required\' | translate }}\n \n \n \n \n \n
\n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:z.FileInputComponent,selector:"tb-file-input",inputs:["label","accept","noFileText","inputId","allowedExtensions","dropLabel","contentConvertFunction","required","requiredAsError","disabled","existingFileName","readAsBinary","workFromFileObj","multipleFile"],outputs:["fileNameChanged"]},{type:nt,selector:"tb-kv-map-config",inputs:["disabled","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:dt,decorators:[{type:o,args:[{selector:"tb-action-node-pub-sub-config",templateUrl:"./pubsub-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class ft extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.attributeScopes=Object.keys(m),this.telemetryTypeTranslationsMap=u}configForm(){return this.pushToCloudConfigForm}onConfigurationSet(e){this.pushToCloudConfigForm=this.fb.group({scope:[e?e.scope:null,[k.required]]})}}e("PushToCloudConfigComponent",ft),ft.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ft,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),ft.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:ft,selector:"tb-action-node-push-to-cloud-config",usesInheritance:!0,ngImport:t,template:'
\n \n attribute.attributes-scope\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ft,decorators:[{type:o,args:[{selector:"tb-action-node-push-to-cloud-config",templateUrl:"./push-to-cloud-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class ct extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.attributeScopes=Object.keys(m),this.telemetryTypeTranslationsMap=u}configForm(){return this.pushToEdgeConfigForm}onConfigurationSet(e){this.pushToEdgeConfigForm=this.fb.group({scope:[e?e.scope:null,[k.required]]})}}e("PushToEdgeConfigComponent",ct),ct.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ct,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),ct.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:ct,selector:"tb-action-node-push-to-edge-config",usesInheritance:!0,ngImport:t,template:'
\n \n attribute.attributes-scope\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ct,decorators:[{type:o,args:[{selector:"tb-action-node-push-to-edge-config",templateUrl:"./push-to-edge-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class gt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.messageProperties=[null,"BASIC","TEXT_PLAIN","MINIMAL_BASIC","MINIMAL_PERSISTENT_BASIC","PERSISTENT_BASIC","PERSISTENT_TEXT_PLAIN"]}configForm(){return this.rabbitMqConfigForm}onConfigurationSet(e){this.rabbitMqConfigForm=this.fb.group({exchangeNamePattern:[e?e.exchangeNamePattern:null,[]],routingKeyPattern:[e?e.routingKeyPattern:null,[]],messageProperties:[e?e.messageProperties:null,[]],host:[e?e.host:null,[k.required]],port:[e?e.port:null,[k.required,k.min(1),k.max(65535)]],virtualHost:[e?e.virtualHost:null,[]],username:[e?e.username:null,[]],password:[e?e.password:null,[]],automaticRecoveryEnabled:[!!e&&e.automaticRecoveryEnabled,[]],connectionTimeout:[e?e.connectionTimeout:null,[k.min(0)]],handshakeTimeout:[e?e.handshakeTimeout:null,[k.min(0)]],clientProperties:[e?e.clientProperties:null,[]]})}}e("RabbitMqConfigComponent",gt),gt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:gt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),gt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:gt,selector:"tb-action-node-rabbit-mq-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.exchange-name-pattern\n \n \n \n tb.rulenode.routing-key-pattern\n \n \n \n tb.rulenode.message-properties\n \n \n {{ property }}\n \n \n \n
\n \n tb.rulenode.host\n \n \n {{ \'tb.rulenode.host-required\' | translate }}\n \n \n \n tb.rulenode.port\n \n \n {{ \'tb.rulenode.port-required\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n
\n \n tb.rulenode.virtual-host\n \n \n \n tb.rulenode.username\n \n \n \n tb.rulenode.password\n \n \n \n \n {{ \'tb.rulenode.automatic-recovery\' | translate }}\n \n \n tb.rulenode.connection-timeout-ms\n \n \n {{ \'tb.rulenode.min-connection-timeout-ms-message\' | translate }}\n \n \n \n tb.rulenode.handshake-timeout-ms\n \n \n {{ \'tb.rulenode.min-handshake-timeout-ms-message\' | translate }}\n \n \n \n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:j.TogglePasswordComponent,selector:"tb-toggle-password"},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:nt,selector:"tb-kv-map-config",inputs:["disabled","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:E.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{type:D.MatSuffix,selector:"[matSuffix]"}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:gt,decorators:[{type:o,args:[{selector:"tb-action-node-rabbit-mq-config",templateUrl:"./rabbit-mq-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class xt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.proxySchemes=["http","https"],this.httpRequestTypes=Object.keys(Qe)}configForm(){return this.restApiCallConfigForm}onConfigurationSet(e){this.restApiCallConfigForm=this.fb.group({restEndpointUrlPattern:[e?e.restEndpointUrlPattern:null,[k.required]],requestMethod:[e?e.requestMethod:null,[k.required]],useSimpleClientHttpFactory:[!!e&&e.useSimpleClientHttpFactory,[]],ignoreRequestBody:[!!e&&e.ignoreRequestBody,[]],enableProxy:[!!e&&e.enableProxy,[]],useSystemProxyProperties:[!!e&&e.enableProxy,[]],proxyScheme:[e?e.proxyHost:null,[]],proxyHost:[e?e.proxyHost:null,[]],proxyPort:[e?e.proxyPort:null,[]],proxyUser:[e?e.proxyUser:null,[]],proxyPassword:[e?e.proxyPassword:null,[]],readTimeoutMs:[e?e.readTimeoutMs:null,[]],maxParallelRequestsCount:[e?e.maxParallelRequestsCount:null,[k.min(0)]],headers:[e?e.headers:null,[]],useRedisQueueForMsgPersistence:[!!e&&e.useRedisQueueForMsgPersistence,[]],trimQueue:[!!e&&e.trimQueue,[]],maxQueueSize:[e?e.maxQueueSize:null,[]],credentials:[e?e.credentials:null,[]]})}validatorTriggers(){return["useSimpleClientHttpFactory","useRedisQueueForMsgPersistence","enableProxy","useSystemProxyProperties"]}updateValidators(e){const t=this.restApiCallConfigForm.get("useSimpleClientHttpFactory").value,o=this.restApiCallConfigForm.get("useRedisQueueForMsgPersistence").value,r=this.restApiCallConfigForm.get("enableProxy").value,a=this.restApiCallConfigForm.get("useSystemProxyProperties").value;r&&!a?(this.restApiCallConfigForm.get("proxyHost").setValidators(r?[k.required]:[]),this.restApiCallConfigForm.get("proxyPort").setValidators(r?[k.required,k.min(1),k.max(65535)]:[])):(this.restApiCallConfigForm.get("proxyHost").setValidators([]),this.restApiCallConfigForm.get("proxyPort").setValidators([]),t?this.restApiCallConfigForm.get("readTimeoutMs").setValidators([]):this.restApiCallConfigForm.get("readTimeoutMs").setValidators([k.min(0)])),o?this.restApiCallConfigForm.get("maxQueueSize").setValidators([k.min(0)]):this.restApiCallConfigForm.get("maxQueueSize").setValidators([]),this.restApiCallConfigForm.get("readTimeoutMs").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("maxQueueSize").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("proxyHost").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("proxyPort").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("credentials").updateValueAndValidity({emitEvent:e})}}e("RestApiCallConfigComponent",xt),xt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:xt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),xt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:xt,selector:"tb-action-node-rest-api-call-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.endpoint-url-pattern\n \n \n {{ \'tb.rulenode.endpoint-url-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.request-method\n \n \n {{ requestType }}\n \n \n \n \n {{ \'tb.rulenode.enable-proxy\' | translate }}\n \n \n {{ \'tb.rulenode.use-simple-client-http-factory\' | translate }}\n \n \n {{ \'tb.rulenode.ignore-request-body\' | translate }}\n \n
\n \n {{ \'tb.rulenode.use-system-proxy-properties\' | translate }}\n \n
\n
\n \n tb.rulenode.proxy-scheme\n \n \n {{ proxyScheme }}\n \n \n \n \n tb.rulenode.proxy-host\n \n \n {{ \'tb.rulenode.proxy-host-required\' | translate }}\n \n \n \n tb.rulenode.proxy-port\n \n \n {{ \'tb.rulenode.proxy-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.proxy-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.proxy-user\n \n \n \n tb.rulenode.proxy-password\n \n \n
\n
\n \n tb.rulenode.read-timeout\n \n tb.rulenode.read-timeout-hint\n \n \n tb.rulenode.max-parallel-requests-count\n \n tb.rulenode.max-parallel-requests-count-hint\n \n \n
\n \n \n \n {{ \'tb.rulenode.use-redis-queue\' | translate }}\n \n
\n \n {{ \'tb.rulenode.trim-redis-queue\' | translate }}\n \n \n tb.rulenode.redis-queue-max-size\n \n \n
\n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:nt,selector:"tb-kv-map-config",inputs:["disabled","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]},{type:st,selector:"tb-credentials-config",inputs:["required","disableCertPemCredentials","passwordFieldRquired"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:E.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:xt,decorators:[{type:o,args:[{selector:"tb-action-node-rest-api-call-config",templateUrl:"./rest-api-call-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class yt extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.rpcReplyConfigForm}onConfigurationSet(e){this.rpcReplyConfigForm=this.fb.group({requestIdMetaDataAttribute:[e?e.requestIdMetaDataAttribute:null,[]]})}}e("RpcReplyConfigComponent",yt),yt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:yt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),yt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:yt,selector:"tb-action-node-rpc-reply-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.request-id-metadata-attribute\n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:yt,decorators:[{type:o,args:[{selector:"tb-action-node-rpc-reply-config",templateUrl:"./rpc-reply-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class bt extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.rpcRequestConfigForm}onConfigurationSet(e){this.rpcRequestConfigForm=this.fb.group({timeoutInSeconds:[e?e.timeoutInSeconds:null,[k.required,k.min(0)]]})}}e("RpcRequestConfigComponent",bt),bt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:bt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),bt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:bt,selector:"tb-action-node-rpc-request-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.timeout-sec\n \n \n {{ \'tb.rulenode.timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-timeout-message\' | translate }}\n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:bt,decorators:[{type:o,args:[{selector:"tb-action-node-rpc-request-config",templateUrl:"./rpc-request-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class ht extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.saveToCustomTableConfigForm}onConfigurationSet(e){this.saveToCustomTableConfigForm=this.fb.group({tableName:[e?e.tableName:null,[k.required,k.pattern(/.*\S.*/)]],fieldsMapping:[e?e.fieldsMapping:null,[k.required]]})}prepareOutputConfig(e){return e.tableName=e.tableName.trim(),e}}e("SaveToCustomTableConfigComponent",ht),ht.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ht,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),ht.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:ht,selector:"tb-action-node-custom-table-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.custom-table-name\n \n \n {{ \'tb.rulenode.custom-table-name-required\' | translate }}\n \n tb.rulenode.custom-table-hint\n \n \n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:nt,selector:"tb-kv-map-config",inputs:["disabled","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ht,decorators:[{type:o,args:[{selector:"tb-action-node-custom-table-config",templateUrl:"./save-to-custom-table-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Ct extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.smtpProtocols=["smtp","smtps"],this.tlsVersions=["TLSv1","TLSv1.1","TLSv1.2","TLSv1.3"]}configForm(){return this.sendEmailConfigForm}onConfigurationSet(e){this.sendEmailConfigForm=this.fb.group({useSystemSmtpSettings:[!!e&&e.useSystemSmtpSettings,[]],smtpProtocol:[e?e.smtpProtocol:null,[]],smtpHost:[e?e.smtpHost:null,[]],smtpPort:[e?e.smtpPort:null,[]],timeout:[e?e.timeout:null,[]],enableTls:[!!e&&e.enableTls,[]],tlsVersion:[e?e.tlsVersion:null,[]],enableProxy:[!!e&&e.enableProxy,[]],proxyHost:[e?e.proxyHost:null,[]],proxyPort:[e?e.proxyPort:null,[]],proxyUser:[e?e.proxyUser:null,[]],proxyPassword:[e?e.proxyPassword:null,[]],username:[e?e.username:null,[]],password:[e?e.password:null,[]]})}validatorTriggers(){return["useSystemSmtpSettings","enableProxy"]}updateValidators(e){const t=this.sendEmailConfigForm.get("useSystemSmtpSettings").value,o=this.sendEmailConfigForm.get("enableProxy").value;t?(this.sendEmailConfigForm.get("smtpProtocol").setValidators([]),this.sendEmailConfigForm.get("smtpHost").setValidators([]),this.sendEmailConfigForm.get("smtpPort").setValidators([]),this.sendEmailConfigForm.get("timeout").setValidators([]),this.sendEmailConfigForm.get("proxyHost").setValidators([]),this.sendEmailConfigForm.get("proxyPort").setValidators([])):(this.sendEmailConfigForm.get("smtpProtocol").setValidators([k.required]),this.sendEmailConfigForm.get("smtpHost").setValidators([k.required]),this.sendEmailConfigForm.get("smtpPort").setValidators([k.required,k.min(1),k.max(65535)]),this.sendEmailConfigForm.get("timeout").setValidators([k.required,k.min(0)]),this.sendEmailConfigForm.get("proxyHost").setValidators(o?[k.required]:[]),this.sendEmailConfigForm.get("proxyPort").setValidators(o?[k.required,k.min(1),k.max(65535)]:[])),this.sendEmailConfigForm.get("smtpProtocol").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("smtpHost").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("smtpPort").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("timeout").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("proxyHost").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("proxyPort").updateValueAndValidity({emitEvent:e})}}e("SendEmailConfigComponent",Ct),Ct.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ct,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Ct.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Ct,selector:"tb-action-node-send-email-config",usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'tb.rulenode.use-system-smtp-settings\' | translate }}\n \n
\n \n tb.rulenode.smtp-protocol\n \n \n {{ smtpProtocol.toUpperCase() }}\n \n \n \n
\n \n tb.rulenode.smtp-host\n \n \n {{ \'tb.rulenode.smtp-host-required\' | translate }}\n \n \n \n tb.rulenode.smtp-port\n \n \n {{ \'tb.rulenode.smtp-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.smtp-port-range\' | translate }}\n \n \n {{ \'tb.rulenode.smtp-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.timeout-msec\n \n \n {{ \'tb.rulenode.timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-timeout-msec-message\' | translate }}\n \n \n \n {{ \'tb.rulenode.enable-tls\' | translate }}\n \n \n tb.rulenode.tls-version\n \n \n {{ tlsVersion }}\n \n \n \n \n {{ \'tb.rulenode.enable-proxy\' | translate }}\n \n
\n
\n \n tb.rulenode.proxy-host\n \n \n {{ \'tb.rulenode.proxy-host-required\' | translate }}\n \n \n \n tb.rulenode.proxy-port\n \n \n {{ \'tb.rulenode.proxy-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.proxy-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.proxy-user\n \n \n \n tb.rulenode.proxy-password\n \n \n
\n \n tb.rulenode.username\n \n \n \n tb.rulenode.password\n \n \n \n
\n
\n',components:[{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:ce.TbCheckboxComponent,selector:"tb-checkbox",inputs:["disabled","trueValue","falseValue"],outputs:["valueChange"]},{type:j.TogglePasswordComponent,selector:"tb-toggle-password"}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:E.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{type:D.MatSuffix,selector:"[matSuffix]"}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ct,decorators:[{type:o,args:[{selector:"tb-action-node-send-email-config",templateUrl:"./send-email-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Ft extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.sendSmsConfigForm}onConfigurationSet(e){this.sendSmsConfigForm=this.fb.group({numbersToTemplate:[e?e.numbersToTemplate:null,[k.required]],smsMessageTemplate:[e?e.smsMessageTemplate:null,[k.required]],useSystemSmsSettings:[!!e&&e.useSystemSmsSettings,[]],smsProviderConfiguration:[e?e.smsProviderConfiguration:null,[]]})}validatorTriggers(){return["useSystemSmsSettings"]}updateValidators(e){this.sendSmsConfigForm.get("useSystemSmsSettings").value?this.sendSmsConfigForm.get("smsProviderConfiguration").setValidators([]):this.sendSmsConfigForm.get("smsProviderConfiguration").setValidators([k.required]),this.sendSmsConfigForm.get("smsProviderConfiguration").updateValueAndValidity({emitEvent:e})}}e("SendSmsConfigComponent",Ft),Ft.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ft,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Ft.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Ft,selector:"tb-action-node-send-sms-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.numbers-to-template\n \n \n {{ \'tb.rulenode.numbers-to-template-required\' | translate }}\n \n \n \n \n tb.rulenode.sms-message-template\n \n \n {{ \'tb.rulenode.sms-message-template-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.use-system-sms-settings\' | translate }}\n \n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:ge.SmsProviderConfigurationComponent,selector:"tb-sms-provider-configuration",inputs:["required","disabled"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ft,decorators:[{type:o,args:[{selector:"tb-action-node-send-sms-config",templateUrl:"./send-sms-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Lt extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.snsConfigForm}onConfigurationSet(e){this.snsConfigForm=this.fb.group({topicArnPattern:[e?e.topicArnPattern:null,[k.required]],accessKeyId:[e?e.accessKeyId:null,[k.required]],secretAccessKey:[e?e.secretAccessKey:null,[k.required]],region:[e?e.region:null,[k.required]]})}}e("SnsConfigComponent",Lt),Lt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Lt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Lt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Lt,selector:"tb-action-node-sns-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.topic-arn-pattern\n \n \n {{ \'tb.rulenode.topic-arn-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.aws-access-key-id\n \n \n {{ \'tb.rulenode.aws-access-key-id-required\' | translate }}\n \n \n \n tb.rulenode.aws-secret-access-key\n \n \n {{ \'tb.rulenode.aws-secret-access-key-required\' | translate }}\n \n \n \n tb.rulenode.aws-region\n \n \n {{ \'tb.rulenode.aws-region-required\' | translate }}\n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Lt,decorators:[{type:o,args:[{selector:"tb-action-node-sns-config",templateUrl:"./sns-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class vt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.sqsQueueType=Ue,this.sqsQueueTypes=Object.keys(Ue),this.sqsQueueTypeTranslationsMap=Ke}configForm(){return this.sqsConfigForm}onConfigurationSet(e){this.sqsConfigForm=this.fb.group({queueType:[e?e.queueType:null,[k.required]],queueUrlPattern:[e?e.queueUrlPattern:null,[k.required]],delaySeconds:[e?e.delaySeconds:null,[k.min(0),k.max(900)]],messageAttributes:[e?e.messageAttributes:null,[]],accessKeyId:[e?e.accessKeyId:null,[k.required]],secretAccessKey:[e?e.secretAccessKey:null,[k.required]],region:[e?e.region:null,[k.required]]})}}e("SqsConfigComponent",vt),vt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:vt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),vt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:vt,selector:"tb-action-node-sqs-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.queue-type\n \n \n {{ sqsQueueTypeTranslationsMap.get(type) | translate }}\n \n \n \n \n tb.rulenode.queue-url-pattern\n \n \n {{ \'tb.rulenode.queue-url-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.delay-seconds\n \n \n {{ \'tb.rulenode.min-delay-seconds-message\' | translate }}\n \n \n {{ \'tb.rulenode.max-delay-seconds-message\' | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.aws-access-key-id\n \n \n {{ \'tb.rulenode.aws-access-key-id-required\' | translate }}\n \n \n \n tb.rulenode.aws-secret-access-key\n \n \n {{ \'tb.rulenode.aws-secret-access-key-required\' | translate }}\n \n \n \n tb.rulenode.aws-region\n \n \n {{ \'tb.rulenode.aws-region-required\' | translate }}\n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:nt,selector:"tb-kv-map-config",inputs:["disabled","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:vt,decorators:[{type:o,args:[{selector:"tb-action-node-sqs-config",templateUrl:"./sqs-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class It extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.timeseriesConfigForm}onConfigurationSet(e){this.timeseriesConfigForm=this.fb.group({defaultTTL:[e?e.defaultTTL:null,[k.required,k.min(0)]],skipLatestPersistence:[!!e&&e.skipLatestPersistence,[]],useServerTs:[!!e&&e.useServerTs,[]]})}}e("TimeseriesConfigComponent",It),It.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:It,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),It.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:It,selector:"tb-action-node-timeseries-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.default-ttl\n \n \n {{ \'tb.rulenode.default-ttl-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-default-ttl-message\' | translate }}\n \n \n \n {{ \'tb.rulenode.skip-latest-persistence\' | translate }}\n \n \n {{ \'tb.rulenode.use-server-ts\' | translate }}\n \n
tb.rulenode.use-server-ts-hint
\n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:It,decorators:[{type:o,args:[{selector:"tb-action-node-timeseries-config",templateUrl:"./timeseries-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Nt extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.unassignCustomerConfigForm}onConfigurationSet(e){this.unassignCustomerConfigForm=this.fb.group({customerNamePattern:[e?e.customerNamePattern:null,[k.required,k.pattern(/.*\S.*/)]],customerCacheExpiration:[e?e.customerCacheExpiration:null,[k.required,k.min(0)]]})}prepareOutputConfig(e){return e.customerNamePattern=e.customerNamePattern.trim(),e}}e("UnassignCustomerConfigComponent",Nt),Nt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Nt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Nt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Nt,selector:"tb-action-node-un-assign-to-customer-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.customer-name-pattern\n \n \n {{ \'tb.rulenode.customer-name-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.customer-cache-expiration\n \n \n {{ \'tb.rulenode.customer-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.customer-cache-expiration-range\' | translate }}\n \n tb.rulenode.customer-cache-expiration-hint\n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Nt,decorators:[{type:o,args:[{selector:"tb-action-node-un-assign-to-customer-config",templateUrl:"./unassign-customer-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Tt extends y{constructor(e,t){super(e),this.store=e,this.fb=t,this.directionTypes=Object.keys(c),this.directionTypeTranslations=g,this.entityType=x,this.propagateChange=null}get required(){return this.requiredValue}set required(e){this.requiredValue=le(e)}ngOnInit(){this.deviceRelationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[k.required]],maxLevel:[null,[]],relationType:[null],deviceTypes:[null,[k.required]]}),this.deviceRelationsQueryFormGroup.valueChanges.subscribe((e=>{this.deviceRelationsQueryFormGroup.valid?this.propagateChange(e):this.propagateChange(null)}))}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}setDisabledState(e){this.disabled=e,this.disabled?this.deviceRelationsQueryFormGroup.disable({emitEvent:!1}):this.deviceRelationsQueryFormGroup.enable({emitEvent:!1})}writeValue(e){this.deviceRelationsQueryFormGroup.reset(e,{emitEvent:!1})}}e("DeviceRelationsQueryConfigComponent",Tt),Tt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Tt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Tt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Tt,selector:"tb-device-relations-query-config",inputs:{disabled:"disabled",required:"required"},providers:[{provide:S,useExisting:n((()=>Tt)),multi:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'alias.last-level-relation\' | translate }}\n \n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n
\n
relation.relation-type
\n \n \n
device.device-types
\n \n \n
\n',components:[{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:ye.RelationTypeAutocompleteComponent,selector:"tb-relation-type-autocomplete",inputs:["required","disabled"]},{type:be.EntitySubTypeListComponent,selector:"tb-entity-subtype-list",inputs:["required","disabled","entityType"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:E.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Tt,decorators:[{type:o,args:[{selector:"tb-device-relations-query-config",templateUrl:"./device-relations-query-config.component.html",styleUrls:[],providers:[{provide:S,useExisting:n((()=>Tt)),multi:!0}]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]},propDecorators:{disabled:[{type:l}],required:[{type:l}]}});class qt extends y{constructor(e,t){super(e),this.store=e,this.fb=t,this.directionTypes=Object.keys(c),this.directionTypeTranslations=g,this.propagateChange=null}get required(){return this.requiredValue}set required(e){this.requiredValue=le(e)}ngOnInit(){this.relationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[k.required]],maxLevel:[null,[]],filters:[null]}),this.relationsQueryFormGroup.valueChanges.subscribe((e=>{this.relationsQueryFormGroup.valid?this.propagateChange(e):this.propagateChange(null)}))}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}setDisabledState(e){this.disabled=e,this.disabled?this.relationsQueryFormGroup.disable({emitEvent:!1}):this.relationsQueryFormGroup.enable({emitEvent:!1})}writeValue(e){this.relationsQueryFormGroup.reset(e||{},{emitEvent:!1})}}e("RelationsQueryConfigComponent",qt),qt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:qt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),qt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:qt,selector:"tb-relations-query-config",inputs:{disabled:"disabled",required:"required"},providers:[{provide:S,useExisting:n((()=>qt)),multi:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'alias.last-level-relation\' | translate }}\n \n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n
\n
relation.relation-filters
\n \n
\n',components:[{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:he.RelationFiltersComponent,selector:"tb-relation-filters",inputs:["disabled","allowedEntityTypes"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:E.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:qt,decorators:[{type:o,args:[{selector:"tb-relations-query-config",templateUrl:"./relations-query-config.component.html",styleUrls:[],providers:[{provide:S,useExisting:n((()=>qt)),multi:!0}]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]},propDecorators:{disabled:[{type:l}],required:[{type:l}]}});class kt extends y{constructor(e,t,o,r){super(e),this.store=e,this.translate=t,this.truncate=o,this.fb=r,this.placeholder="tb.rulenode.message-type",this.separatorKeysCodes=[Z,X,ee],this.messageTypes=[],this.messageTypesList=[],this.searchText="",this.propagateChange=e=>{},this.messageTypeConfigForm=this.fb.group({messageType:[null]});for(const e of Object.keys(b))this.messageTypesList.push({name:h.get(b[e]),value:e})}get required(){return this.requiredValue}set required(e){this.requiredValue=le(e)}registerOnChange(e){this.propagateChange=e}registerOnTouched(e){}ngOnInit(){this.filteredMessageTypes=this.messageTypeConfigForm.get("messageType").valueChanges.pipe(ue(""),pe((e=>e||"")),de((e=>this.fetchMessageTypes(e))),fe())}ngAfterViewInit(){}setDisabledState(e){this.disabled=e,this.disabled?this.messageTypeConfigForm.disable({emitEvent:!1}):this.messageTypeConfigForm.enable({emitEvent:!1})}writeValue(e){this.searchText="",this.messageTypes.length=0,e&&e.forEach((e=>{const t=this.messageTypesList.find((t=>t.value===e));t?this.messageTypes.push({name:t.name,value:t.value}):this.messageTypes.push({name:e,value:e})}))}displayMessageTypeFn(e){return e?e.name:void 0}textIsNotEmpty(e){return!!(e&&null!=e&&e.length>0)}createMessageType(e,t){e.preventDefault(),this.transformMessageType(t)}add(e){this.transformMessageType(e.value)}fetchMessageTypes(e){if(this.searchText=e,this.searchText&&this.searchText.length){const e=this.searchText.toUpperCase();return Ce(this.messageTypesList.filter((t=>t.name.toUpperCase().includes(e))))}return Ce(this.messageTypesList)}transformMessageType(e){if((e||"").trim()){let t=null;const o=e.trim(),r=this.messageTypesList.find((e=>e.name===o));t=r?{name:r.name,value:r.value}:{name:o,value:o},t&&this.addMessageType(t)}this.clear("")}remove(e){const t=this.messageTypes.indexOf(e);t>=0&&(this.messageTypes.splice(t,1),this.updateModel())}selected(e){this.addMessageType(e.option.value),this.clear("")}addMessageType(e){-1===this.messageTypes.findIndex((t=>t.value===e.value))&&(this.messageTypes.push(e),this.updateModel())}onFocus(){this.messageTypeConfigForm.get("messageType").updateValueAndValidity({onlySelf:!0,emitEvent:!0})}clear(e=""){this.messageTypeInput.nativeElement.value=e,this.messageTypeConfigForm.get("messageType").patchValue(null,{emitEvent:!0}),setTimeout((()=>{this.messageTypeInput.nativeElement.blur(),this.messageTypeInput.nativeElement.focus()}),0)}updateModel(){const e=this.messageTypes.map((e=>e.value));this.required?(this.chipList.errorState=!e.length,this.propagateChange(e.length>0?e:null)):(this.chipList.errorState=!1,this.propagateChange(e))}}e("MessageTypesConfigComponent",kt),kt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:kt,deps:[{token:T.Store},{token:P.TranslateService},{token:C.TruncatePipe},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),kt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:kt,selector:"tb-message-types-config",inputs:{required:"required",label:"label",placeholder:"placeholder",disabled:"disabled"},providers:[{provide:S,useExisting:n((()=>kt)),multi:!0}],viewQueries:[{propertyName:"chipList",first:!0,predicate:["chipList"],descendants:!0},{propertyName:"matAutocomplete",first:!0,predicate:["messageTypeAutocomplete"],descendants:!0},{propertyName:"messageTypeInput",first:!0,predicate:["messageTypeInput"],descendants:!0}],usesInheritance:!0,ngImport:t,template:'\n {{ label }}\n \n \n {{messageType.name}}\n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-message-types-found\n
\n \n \n {{ translate.get(\'tb.rulenode.no-message-type-matching\',\n {messageType: truncate.transform(searchText, true, 6, '...')}) | async }}\n \n \n \n tb.rulenode.create-new-message-type\n \n
\n
\n
\n \n {{ \'tb.rulenode.message-types-required\' | translate }}\n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:te.MatChipList,selector:"mat-chip-list",inputs:["aria-orientation","multiple","compareWith","value","required","placeholder","disabled","selectable","tabIndex","errorStateMatcher"],outputs:["change","valueChange"],exportAs:["matChipList"]},{type:oe.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{type:Fe.MatAutocomplete,selector:"mat-autocomplete",inputs:["disableRipple"],exportAs:["matAutocomplete"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]}],directives:[{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:te.MatChip,selector:"mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]",inputs:["color","disableRipple","tabIndex","selected","value","selectable","disabled","removable"],outputs:["selectionChange","destroyed","removed"],exportAs:["matChip"]},{type:te.MatChipRemove,selector:"[matChipRemove]"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:Fe.MatAutocompleteTrigger,selector:"input[matAutocomplete], textarea[matAutocomplete]",exportAs:["matAutocompleteTrigger"]},{type:te.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputSeparatorKeyCodes","placeholder","id","matChipInputFor","matChipInputAddOnBlur","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:Fe.MatAutocompleteOrigin,selector:"[matAutocompleteOrigin]",exportAs:["matAutocompleteOrigin"]},{type:D.MatError,selector:"mat-error",inputs:["id"]}],pipes:{translate:P.TranslatePipe,async:w.AsyncPipe,highlight:Le.HighlightPipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:kt,decorators:[{type:o,args:[{selector:"tb-message-types-config",templateUrl:"./message-types-config.component.html",styleUrls:[],providers:[{provide:S,useExisting:n((()=>kt)),multi:!0}]}]}],ctorParameters:function(){return[{type:T.Store},{type:P.TranslateService},{type:C.TruncatePipe},{type:q.FormBuilder}]},propDecorators:{required:[{type:l}],label:[{type:l}],placeholder:[{type:l}],disabled:[{type:l}],chipList:[{type:a,args:["chipList",{static:!1}]}],matAutocomplete:[{type:a,args:["messageTypeAutocomplete",{static:!1}]}],messageTypeInput:[{type:a,args:["messageTypeInput",{static:!1}]}]}});class Mt{}e("RulenodeCoreConfigCommonModule",Mt),Mt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Mt,deps:[],target:t.ɵɵFactoryTarget.NgModule}),Mt.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Mt,declarations:[nt,Tt,qt,kt,st,Te],imports:[O,F,xe],exports:[nt,Tt,qt,kt,st,Te]}),Mt.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Mt,imports:[[O,F,xe]]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Mt,decorators:[{type:i,args:[{declarations:[nt,Tt,qt,kt,st,Te],imports:[O,F,xe],exports:[nt,Tt,qt,kt,st,Te]}]}]});class St{}e("RuleNodeCoreConfigActionModule",St),St.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:St,deps:[],target:t.ɵɵFactoryTarget.NgModule}),St.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:St,declarations:[ke,It,bt,it,qe,Ze,Xe,et,pt,tt,rt,at,ut,yt,ht,Nt,Lt,vt,dt,lt,mt,gt,xt,Ct,Je,Ye,ot,Ft,ct,ft],imports:[O,F,xe,Mt],exports:[ke,It,bt,it,qe,Ze,Xe,et,pt,tt,rt,at,ut,yt,ht,Nt,Lt,vt,dt,lt,mt,gt,xt,Ct,Je,Ye,ot,Ft,ct,ft]}),St.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:St,imports:[[O,F,xe,Mt]]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:St,decorators:[{type:i,args:[{declarations:[ke,It,bt,it,qe,Ze,Xe,et,pt,tt,rt,at,ut,yt,ht,Nt,Lt,vt,dt,lt,mt,gt,xt,Ct,Je,Ye,ot,Ft,ct,ft],imports:[O,F,xe,Mt],exports:[ke,It,bt,it,qe,Ze,Xe,et,pt,tt,rt,at,ut,yt,ht,Nt,Lt,vt,dt,lt,mt,gt,xt,Ct,Je,Ye,ot,Ft,ct,ft]}]}]});class At extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.separatorKeysCodes=[Z,X,ee]}configForm(){return this.calculateDeltaConfigForm}onConfigurationSet(e){this.calculateDeltaConfigForm=this.fb.group({inputValueKey:[e?e.inputValueKey:null,[k.required]],outputValueKey:[e?e.outputValueKey:null,[k.required]],useCache:[e?e.useCache:null,[]],addPeriodBetweenMsgs:[!!e&&e.addPeriodBetweenMsgs,[]],periodValueKey:[e?e.periodValueKey:null,[]],round:[e?e.round:null,[k.min(0),k.max(15)]],tellFailureIfDeltaIsNegative:[e?e.tellFailureIfDeltaIsNegative:null,[]]})}updateValidators(e){this.calculateDeltaConfigForm.get("addPeriodBetweenMsgs").value?this.calculateDeltaConfigForm.get("periodValueKey").setValidators([k.required]):this.calculateDeltaConfigForm.get("periodValueKey").setValidators([]),this.calculateDeltaConfigForm.get("periodValueKey").updateValueAndValidity({emitEvent:e})}validatorTriggers(){return["addPeriodBetweenMsgs"]}}e("CalculateDeltaConfigComponent",At),At.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:At,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),At.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:At,selector:"tb-enrichment-node-calculate-delta-config",usesInheritance:!0,ngImport:t,template:'
\n
\n \n tb.rulenode.input-value-key\n \n \n {{ \'tb.rulenode.input-value-key-required\' | translate }}\n \n \n \n tb.rulenode.output-value-key\n \n \n {{ \'tb.rulenode.output-value-key-required\' | translate }}\n \n \n \n tb.rulenode.round\n \n \n {{ \'tb.rulenode.round-range\' | translate }}\n \n \n {{ \'tb.rulenode.round-range\' | translate }}\n \n \n
\n \n {{ \'tb.rulenode.use-cache\' | translate }}\n \n \n {{ \'tb.rulenode.tell-failure-if-delta-is-negative\' | translate }}\n \n \n {{ \'tb.rulenode.add-period-between-msgs\' | translate }}\n \n \n tb.rulenode.period-value-key\n \n \n {{ \'tb.rulenode.period-value-key-required\' | translate }}\n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:E.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:At,decorators:[{type:o,args:[{selector:"tb-enrichment-node-calculate-delta-config",templateUrl:"./calculate-delta-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Gt extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.customerAttributesConfigForm}onConfigurationSet(e){this.customerAttributesConfigForm=this.fb.group({telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[k.required]]})}}e("CustomerAttributesConfigComponent",Gt),Gt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Gt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Gt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Gt,selector:"tb-enrichment-node-customer-attributes-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n',components:[{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:nt,selector:"tb-kv-map-config",inputs:["disabled","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Gt,decorators:[{type:o,args:[{selector:"tb-enrichment-node-customer-attributes-config",templateUrl:"./customer-attributes-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Dt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.separatorKeysCodes=[Z,X,ee]}configForm(){return this.deviceAttributesConfigForm}onConfigurationSet(e){this.deviceAttributesConfigForm=this.fb.group({deviceRelationsQuery:[e?e.deviceRelationsQuery:null,[k.required]],tellFailureIfAbsent:[!!e&&e.tellFailureIfAbsent,[]],clientAttributeNames:[e?e.clientAttributeNames:null,[]],sharedAttributeNames:[e?e.sharedAttributeNames:null,[]],serverAttributeNames:[e?e.serverAttributeNames:null,[]],latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],getLatestValueWithTs:[!!e&&e.getLatestValueWithTs,[]]})}removeKey(e,t){const o=this.deviceAttributesConfigForm.get(t).value,r=o.indexOf(e);r>=0&&(o.splice(r,1),this.deviceAttributesConfigForm.get(t).setValue(o,{emitEvent:!0}))}addKey(e,t){const o=e.input;let r=e.value;if((r||"").trim()){r=r.trim();let e=this.deviceAttributesConfigForm.get(t).value;e&&-1!==e.indexOf(r)||(e||(e=[]),e.push(r),this.deviceAttributesConfigForm.get(t).setValue(e,{emitEvent:!0}))}o&&(o.value="")}}e("DeviceAttributesConfigComponent",Dt),Dt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Dt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Dt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Dt,selector:"tb-enrichment-node-device-attributes-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n \n \n {{ \'tb.rulenode.tell-failure-if-absent\' | translate }}\n \n
tb.rulenode.tell-failure-if-absent-hint
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n {{ \'tb.rulenode.get-latest-value-with-ts\' | translate }}\n \n
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}\n"],components:[{type:Tt,selector:"tb-device-relations-query-config",inputs:["disabled","required"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:te.MatChipList,selector:"mat-chip-list",inputs:["aria-orientation","multiple","compareWith","value","required","placeholder","disabled","selectable","tabIndex","errorStateMatcher"],outputs:["change","valueChange"],exportAs:["matChipList"]},{type:oe.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:D.MatLabel,selector:"mat-label"},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:te.MatChip,selector:"mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]",inputs:["color","disableRipple","tabIndex","selected","value","selectable","disabled","removable"],outputs:["selectionChange","destroyed","removed"],exportAs:["matChip"]},{type:te.MatChipRemove,selector:"[matChipRemove]"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:te.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputSeparatorKeyCodes","placeholder","id","matChipInputFor","matChipInputAddOnBlur","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Dt,decorators:[{type:o,args:[{selector:"tb-enrichment-node-device-attributes-config",templateUrl:"./device-attributes-config.component.html",styleUrls:["./device-attributes-config.component.scss"]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Vt extends s{constructor(e,t,o){super(e),this.store=e,this.translate=t,this.fb=o,this.entityDetailsTranslationsMap=we,this.entityDetailsList=[],this.searchText="",this.displayDetailsFn=this.displayDetails.bind(this);for(const e of Object.keys(Re))this.entityDetailsList.push(Re[e]);this.detailsFormControl=new G(""),this.filteredEntityDetails=this.detailsFormControl.valueChanges.pipe(ue(""),pe((e=>e||"")),de((e=>this.fetchEntityDetails(e))),fe())}ngOnInit(){super.ngOnInit()}configForm(){return this.entityDetailsConfigForm}prepareInputConfig(e){return this.searchText="",this.detailsFormControl.patchValue("",{emitEvent:!0}),e}onConfigurationSet(e){this.entityDetailsConfigForm=this.fb.group({detailsList:[e?e.detailsList:null,[k.required]],addToMetadata:[!!e&&e.addToMetadata,[]]})}displayDetails(e){return e?this.translate.instant(we.get(e)):void 0}fetchEntityDetails(e){if(this.searchText=e,this.searchText&&this.searchText.length){const e=this.searchText.toUpperCase();return Ce(this.entityDetailsList.filter((t=>this.translate.instant(we.get(Re[t])).toUpperCase().includes(e))))}return Ce(this.entityDetailsList)}detailsFieldSelected(e){this.addDetailsField(e.option.value),this.clear("")}removeDetailsField(e){const t=this.entityDetailsConfigForm.get("detailsList").value;if(t){const o=t.indexOf(e);o>=0&&(t.splice(o,1),this.entityDetailsConfigForm.get("detailsList").setValue(t))}}addDetailsField(e){let t=this.entityDetailsConfigForm.get("detailsList").value;t||(t=[]);-1===t.indexOf(e)&&(t.push(e),this.entityDetailsConfigForm.get("detailsList").setValue(t))}onEntityDetailsInputFocus(){this.detailsFormControl.updateValueAndValidity({onlySelf:!0,emitEvent:!0})}clear(e=""){this.detailsInput.nativeElement.value=e,this.detailsFormControl.patchValue(null,{emitEvent:!0}),setTimeout((()=>{this.detailsInput.nativeElement.blur(),this.detailsInput.nativeElement.focus()}),0)}}e("EntityDetailsConfigComponent",Vt),Vt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Vt,deps:[{token:T.Store},{token:P.TranslateService},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Vt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Vt,selector:"tb-enrichment-node-entity-details-config",viewQueries:[{propertyName:"detailsInput",first:!0,predicate:["detailsInput"],descendants:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.entity-details\n \n \n \n {{entityDetailsTranslationsMap.get(details) | translate}}\n \n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-entity-details-matching\n
\n
\n
\n
\n
\n \n \n {{ \'tb.rulenode.add-to-metadata\' | translate }}\n \n
tb.rulenode.add-to-metadata-hint
\n
\n',styles:[":host ::ng-deep mat-form-field.entity-fields-list .mat-form-field-wrapper{margin-bottom:-1.25em}\n"],components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:te.MatChipList,selector:"mat-chip-list",inputs:["aria-orientation","multiple","compareWith","value","required","placeholder","disabled","selectable","tabIndex","errorStateMatcher"],outputs:["change","valueChange"],exportAs:["matChipList"]},{type:oe.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{type:Fe.MatAutocomplete,selector:"mat-autocomplete",inputs:["disableRipple"],exportAs:["matAutocomplete"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:ie.TbErrorComponent,selector:"tb-error",inputs:["error"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:te.MatChip,selector:"mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]",inputs:["color","disableRipple","tabIndex","selected","value","selectable","disabled","removable"],outputs:["selectionChange","destroyed","removed"],exportAs:["matChip"]},{type:te.MatChipRemove,selector:"[matChipRemove]"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:Fe.MatAutocompleteTrigger,selector:"input[matAutocomplete], textarea[matAutocomplete]",exportAs:["matAutocompleteTrigger"]},{type:te.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputSeparatorKeyCodes","placeholder","id","matChipInputFor","matChipInputAddOnBlur","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]},{type:Fe.MatAutocompleteOrigin,selector:"[matAutocompleteOrigin]",exportAs:["matAutocompleteOrigin"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlDirective,selector:"[formControl]",inputs:["disabled","formControl","ngModel"],outputs:["ngModelChange"],exportAs:["ngForm"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]}],pipes:{translate:P.TranslatePipe,async:w.AsyncPipe,highlight:Le.HighlightPipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Vt,decorators:[{type:o,args:[{selector:"tb-enrichment-node-entity-details-config",templateUrl:"./entity-details-config.component.html",styleUrls:["./entity-details-config.component.scss"]}]}],ctorParameters:function(){return[{type:T.Store},{type:P.TranslateService},{type:q.FormBuilder}]},propDecorators:{detailsInput:[{type:a,args:["detailsInput",{static:!1}]}]}});class Et extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.separatorKeysCodes=[Z,X,ee],this.aggregationTypes=L,this.aggregations=Object.keys(L),this.aggregationTypesTranslations=v,this.fetchMode=Oe,this.fetchModes=Object.keys(Oe),this.samplingOrders=Object.keys(He),this.timeUnits=Object.values(De),this.timeUnitsTranslationMap=Ve}configForm(){return this.getTelemetryFromDatabaseConfigForm}onConfigurationSet(e){this.getTelemetryFromDatabaseConfigForm=this.fb.group({latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],aggregation:[e?e.aggregation:null,[k.required]],fetchMode:[e?e.fetchMode:null,[k.required]],orderBy:[e?e.orderBy:null,[]],limit:[e?e.limit:null,[]],useMetadataIntervalPatterns:[!!e&&e.useMetadataIntervalPatterns,[]],startInterval:[e?e.startInterval:null,[]],startIntervalTimeUnit:[e?e.startIntervalTimeUnit:null,[]],endInterval:[e?e.endInterval:null,[]],endIntervalTimeUnit:[e?e.endIntervalTimeUnit:null,[]],startIntervalPattern:[e?e.startIntervalPattern:null,[]],endIntervalPattern:[e?e.endIntervalPattern:null,[]]})}validatorTriggers(){return["fetchMode","useMetadataIntervalPatterns"]}updateValidators(e){const t=this.getTelemetryFromDatabaseConfigForm.get("fetchMode").value,o=this.getTelemetryFromDatabaseConfigForm.get("useMetadataIntervalPatterns").value;t&&t===Oe.ALL?(this.getTelemetryFromDatabaseConfigForm.get("aggregation").setValidators([k.required]),this.getTelemetryFromDatabaseConfigForm.get("orderBy").setValidators([k.required]),this.getTelemetryFromDatabaseConfigForm.get("limit").setValidators([k.required,k.min(2),k.max(1e3)])):(this.getTelemetryFromDatabaseConfigForm.get("aggregation").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("orderBy").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("limit").setValidators([])),o?(this.getTelemetryFromDatabaseConfigForm.get("startInterval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endInterval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").setValidators([k.required]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").setValidators([k.required])):(this.getTelemetryFromDatabaseConfigForm.get("startInterval").setValidators([k.required,k.min(1),k.max(2147483647)]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").setValidators([k.required]),this.getTelemetryFromDatabaseConfigForm.get("endInterval").setValidators([k.required,k.min(1),k.max(2147483647)]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").setValidators([k.required]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").setValidators([])),this.getTelemetryFromDatabaseConfigForm.get("aggregation").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("orderBy").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("limit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startInterval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endInterval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").updateValueAndValidity({emitEvent:e})}removeKey(e,t){const o=this.getTelemetryFromDatabaseConfigForm.get(t).value,r=o.indexOf(e);r>=0&&(o.splice(r,1),this.getTelemetryFromDatabaseConfigForm.get(t).setValue(o,{emitEvent:!0}))}addKey(e,t){const o=e.input;let r=e.value;if((r||"").trim()){r=r.trim();let e=this.getTelemetryFromDatabaseConfigForm.get(t).value;e&&-1!==e.indexOf(r)||(e||(e=[]),e.push(r),this.getTelemetryFromDatabaseConfigForm.get(t).setValue(e,{emitEvent:!0}))}o&&(o.value="")}}e("GetTelemetryFromDatabaseConfigComponent",Et),Et.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Et,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Et.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Et,selector:"tb-enrichment-node-get-telemetry-from-database",usesInheritance:!0,ngImport:t,template:'
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n tb.rulenode.fetch-mode\n \n \n {{ mode }}\n \n \n tb.rulenode.fetch-mode-hint\n \n
\n \n aggregation.function\n \n \n {{ aggregationTypesTranslations.get(aggregationTypes[aggregation]) | translate }}\n \n \n \n \n tb.rulenode.order-by\n \n \n {{ order }}\n \n \n tb.rulenode.order-by-hint\n \n \n tb.rulenode.limit\n \n tb.rulenode.limit-hint\n \n
\n \n {{ \'tb.rulenode.use-metadata-interval-patterns\' | translate }}\n \n
tb.rulenode.use-metadata-interval-patterns-hint
\n
\n
\n \n tb.rulenode.start-interval\n \n \n {{ \'tb.rulenode.start-interval-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.start-interval-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n tb.rulenode.end-interval\n \n \n {{ \'tb.rulenode.end-interval-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.end-interval-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n \n tb.rulenode.start-interval-pattern\n \n \n {{ \'tb.rulenode.start-interval-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.end-interval-pattern\n \n \n {{ \'tb.rulenode.end-interval-pattern-required\' | translate }}\n \n \n \n \n
\n',styles:[":host label.tb-title{margin-bottom:-10px}\n"],components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:te.MatChipList,selector:"mat-chip-list",inputs:["aria-orientation","multiple","compareWith","value","required","placeholder","disabled","selectable","tabIndex","errorStateMatcher"],outputs:["change","valueChange"],exportAs:["matChipList"]},{type:oe.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:D.MatLabel,selector:"mat-label"},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:te.MatChip,selector:"mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]",inputs:["color","disableRipple","tabIndex","selected","value","selectable","disabled","removable"],outputs:["selectionChange","destroyed","removed"],exportAs:["matChip"]},{type:te.MatChipRemove,selector:"[matChipRemove]"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:te.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputSeparatorKeyCodes","placeholder","id","matChipInputFor","matChipInputAddOnBlur","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:E.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:D.MatError,selector:"mat-error",inputs:["id"]}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Et,decorators:[{type:o,args:[{selector:"tb-enrichment-node-get-telemetry-from-database",templateUrl:"./get-telemetry-from-database-config.component.html",styleUrls:["./get-telemetry-from-database-config.component.scss"]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Pt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.separatorKeysCodes=[Z,X,ee]}configForm(){return this.originatorAttributesConfigForm}onConfigurationSet(e){this.originatorAttributesConfigForm=this.fb.group({tellFailureIfAbsent:[!!e&&e.tellFailureIfAbsent,[]],clientAttributeNames:[e?e.clientAttributeNames:null,[]],sharedAttributeNames:[e?e.sharedAttributeNames:null,[]],serverAttributeNames:[e?e.serverAttributeNames:null,[]],latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],getLatestValueWithTs:[!!e&&e.getLatestValueWithTs,[]]})}removeKey(e,t){const o=this.originatorAttributesConfigForm.get(t).value,r=o.indexOf(e);r>=0&&(o.splice(r,1),this.originatorAttributesConfigForm.get(t).setValue(o,{emitEvent:!0}))}addKey(e,t){const o=e.input;let r=e.value;if((r||"").trim()){r=r.trim();let e=this.originatorAttributesConfigForm.get(t).value;e&&-1!==e.indexOf(r)||(e||(e=[]),e.push(r),this.originatorAttributesConfigForm.get(t).setValue(e,{emitEvent:!0}))}o&&(o.value="")}}e("OriginatorAttributesConfigComponent",Pt),Pt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Pt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Pt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Pt,selector:"tb-enrichment-node-originator-attributes-config",usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'tb.rulenode.tell-failure-if-absent\' | translate }}\n \n
tb.rulenode.tell-failure-if-absent-hint
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n {{ \'tb.rulenode.get-latest-value-with-ts\' | translate }}\n \n
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}\n"],components:[{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:te.MatChipList,selector:"mat-chip-list",inputs:["aria-orientation","multiple","compareWith","value","required","placeholder","disabled","selectable","tabIndex","errorStateMatcher"],outputs:["change","valueChange"],exportAs:["matChipList"]},{type:oe.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:D.MatLabel,selector:"mat-label"},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:te.MatChip,selector:"mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]",inputs:["color","disableRipple","tabIndex","selected","value","selectable","disabled","removable"],outputs:["selectionChange","destroyed","removed"],exportAs:["matChip"]},{type:te.MatChipRemove,selector:"[matChipRemove]"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:te.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputSeparatorKeyCodes","placeholder","id","matChipInputFor","matChipInputAddOnBlur","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Pt,decorators:[{type:o,args:[{selector:"tb-enrichment-node-originator-attributes-config",templateUrl:"./originator-attributes-config.component.html",styleUrls:["./originator-attributes-config.component.scss"]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Rt extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.originatorFieldsConfigForm}onConfigurationSet(e){this.originatorFieldsConfigForm=this.fb.group({fieldsMapping:[e?e.fieldsMapping:null,[k.required]]})}}e("OriginatorFieldsConfigComponent",Rt),Rt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Rt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Rt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Rt,selector:"tb-enrichment-node-originator-fields-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n \n
\n',components:[{type:nt,selector:"tb-kv-map-config",inputs:["disabled","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Rt,decorators:[{type:o,args:[{selector:"tb-enrichment-node-originator-fields-config",templateUrl:"./originator-fields-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class wt extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.relatedAttributesConfigForm}onConfigurationSet(e){this.relatedAttributesConfigForm=this.fb.group({relationsQuery:[e?e.relationsQuery:null,[k.required]],telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[k.required]]})}}e("RelatedAttributesConfigComponent",wt),wt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:wt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),wt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:wt,selector:"tb-enrichment-node-related-attributes-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n \n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n',components:[{type:qt,selector:"tb-relations-query-config",inputs:["disabled","required"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:nt,selector:"tb-kv-map-config",inputs:["disabled","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:wt,decorators:[{type:o,args:[{selector:"tb-enrichment-node-related-attributes-config",templateUrl:"./related-attributes-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Ot extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.tenantAttributesConfigForm}onConfigurationSet(e){this.tenantAttributesConfigForm=this.fb.group({telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[k.required]]})}}e("TenantAttributesConfigComponent",Ot),Ot.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ot,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Ot.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Ot,selector:"tb-enrichment-node-tenant-attributes-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n',components:[{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:nt,selector:"tb-kv-map-config",inputs:["disabled","requiredText","keyText","keyRequiredText","valText","valRequiredText","hintText","required"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ot,decorators:[{type:o,args:[{selector:"tb-enrichment-node-tenant-attributes-config",templateUrl:"./tenant-attributes-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Ht{}e("RulenodeCoreConfigEnrichmentModule",Ht),Ht.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ht,deps:[],target:t.ɵɵFactoryTarget.NgModule}),Ht.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ht,declarations:[Gt,Vt,Dt,Pt,Rt,Et,wt,Ot,At],imports:[O,F,Mt],exports:[Gt,Vt,Dt,Pt,Rt,Et,wt,Ot,At]}),Ht.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ht,imports:[[O,F,Mt]]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ht,decorators:[{type:i,args:[{declarations:[Gt,Vt,Dt,Pt,Rt,Et,wt,Ot,At],imports:[O,F,Mt],exports:[Gt,Vt,Dt,Pt,Rt,Et,wt,Ot,At]}]}]});class Ut extends s{constructor(e,t,o){super(e),this.store=e,this.translate=t,this.fb=o,this.alarmStatusTranslationsMap=I,this.alarmStatusList=[],this.searchText="",this.displayStatusFn=this.displayStatus.bind(this);for(const e of Object.keys(N))this.alarmStatusList.push(N[e]);this.statusFormControl=new G(""),this.filteredAlarmStatus=this.statusFormControl.valueChanges.pipe(ue(""),pe((e=>e||"")),de((e=>this.fetchAlarmStatus(e))),fe())}ngOnInit(){super.ngOnInit()}configForm(){return this.alarmStatusConfigForm}prepareInputConfig(e){return this.searchText="",this.statusFormControl.patchValue("",{emitEvent:!0}),e}onConfigurationSet(e){this.alarmStatusConfigForm=this.fb.group({alarmStatusList:[e?e.alarmStatusList:null,[k.required]]})}displayStatus(e){return e?this.translate.instant(I.get(e)):void 0}fetchAlarmStatus(e){const t=this.getAlarmStatusList();if(this.searchText=e,this.searchText&&this.searchText.length){const e=this.searchText.toUpperCase();return Ce(t.filter((t=>this.translate.instant(I.get(N[t])).toUpperCase().includes(e))))}return Ce(t)}alarmStatusSelected(e){this.addAlarmStatus(e.option.value),this.clear("")}removeAlarmStatus(e){const t=this.alarmStatusConfigForm.get("alarmStatusList").value;if(t){const o=t.indexOf(e);o>=0&&(t.splice(o,1),this.alarmStatusConfigForm.get("alarmStatusList").setValue(t))}}addAlarmStatus(e){let t=this.alarmStatusConfigForm.get("alarmStatusList").value;t||(t=[]);-1===t.indexOf(e)&&(t.push(e),this.alarmStatusConfigForm.get("alarmStatusList").setValue(t))}getAlarmStatusList(){return this.alarmStatusList.filter((e=>-1===this.alarmStatusConfigForm.get("alarmStatusList").value.indexOf(e)))}onAlarmStatusInputFocus(){this.statusFormControl.updateValueAndValidity({onlySelf:!0,emitEvent:!0})}clear(e=""){this.alarmStatusInput.nativeElement.value=e,this.statusFormControl.patchValue(null,{emitEvent:!0}),setTimeout((()=>{this.alarmStatusInput.nativeElement.blur(),this.alarmStatusInput.nativeElement.focus()}),0)}}e("CheckAlarmStatusComponent",Ut),Ut.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ut,deps:[{token:T.Store},{token:P.TranslateService},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Ut.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Ut,selector:"tb-filter-node-check-alarm-status-config",viewQueries:[{propertyName:"alarmStatusInput",first:!0,predicate:["alarmStatusInput"],descendants:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.alarm-status-filter\n \n \n \n {{alarmStatusTranslationsMap.get(alarmStatus) | translate}}\n \n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-alarm-status-matching\n
\n
\n
\n
\n
\n \n
\n\n\n\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:te.MatChipList,selector:"mat-chip-list",inputs:["aria-orientation","multiple","compareWith","value","required","placeholder","disabled","selectable","tabIndex","errorStateMatcher"],outputs:["change","valueChange"],exportAs:["matChipList"]},{type:oe.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{type:Fe.MatAutocomplete,selector:"mat-autocomplete",inputs:["disableRipple"],exportAs:["matAutocomplete"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:ie.TbErrorComponent,selector:"tb-error",inputs:["error"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:te.MatChip,selector:"mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]",inputs:["color","disableRipple","tabIndex","selected","value","selectable","disabled","removable"],outputs:["selectionChange","destroyed","removed"],exportAs:["matChip"]},{type:te.MatChipRemove,selector:"[matChipRemove]"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:Fe.MatAutocompleteTrigger,selector:"input[matAutocomplete], textarea[matAutocomplete]",exportAs:["matAutocompleteTrigger"]},{type:te.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputSeparatorKeyCodes","placeholder","id","matChipInputFor","matChipInputAddOnBlur","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]},{type:Fe.MatAutocompleteOrigin,selector:"[matAutocompleteOrigin]",exportAs:["matAutocompleteOrigin"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlDirective,selector:"[formControl]",inputs:["disabled","formControl","ngModel"],outputs:["ngModelChange"],exportAs:["ngForm"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]}],pipes:{translate:P.TranslatePipe,async:w.AsyncPipe,highlight:Le.HighlightPipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Ut,decorators:[{type:o,args:[{selector:"tb-filter-node-check-alarm-status-config",templateUrl:"./check-alarm-status.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:P.TranslateService},{type:q.FormBuilder}]},propDecorators:{alarmStatusInput:[{type:a,args:["alarmStatusInput",{static:!1}]}]}});class Kt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.separatorKeysCodes=[Z,X,ee]}configForm(){return this.checkMessageConfigForm}onConfigurationSet(e){this.checkMessageConfigForm=this.fb.group({messageNames:[e?e.messageNames:null,[]],metadataNames:[e?e.metadataNames:null,[]],checkAllKeys:[!!e&&e.checkAllKeys,[]]})}validateConfig(){const e=this.checkMessageConfigForm.get("messageNames").value,t=this.checkMessageConfigForm.get("metadataNames").value;return e.length>0||t.length>0}removeMessageName(e){const t=this.checkMessageConfigForm.get("messageNames").value,o=t.indexOf(e);o>=0&&(t.splice(o,1),this.checkMessageConfigForm.get("messageNames").setValue(t,{emitEvent:!0}))}removeMetadataName(e){const t=this.checkMessageConfigForm.get("metadataNames").value,o=t.indexOf(e);o>=0&&(t.splice(o,1),this.checkMessageConfigForm.get("metadataNames").setValue(t,{emitEvent:!0}))}addMessageName(e){const t=e.input;let o=e.value;if((o||"").trim()){o=o.trim();let e=this.checkMessageConfigForm.get("messageNames").value;e&&-1!==e.indexOf(o)||(e||(e=[]),e.push(o),this.checkMessageConfigForm.get("messageNames").setValue(e,{emitEvent:!0}))}t&&(t.value="")}addMetadataName(e){const t=e.input;let o=e.value;if((o||"").trim()){o=o.trim();let e=this.checkMessageConfigForm.get("metadataNames").value;e&&-1!==e.indexOf(o)||(e||(e=[]),e.push(o),this.checkMessageConfigForm.get("metadataNames").setValue(e,{emitEvent:!0}))}t&&(t.value="")}}e("CheckMessageConfigComponent",Kt),Kt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Kt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Kt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Kt,selector:"tb-filter-node-check-message-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n \n \n \n {{messageName}}\n close\n \n \n \n \n
tb.rulenode.separator-hint
\n \n \n \n \n \n {{metadataName}}\n close\n \n \n \n \n
tb.rulenode.separator-hint
\n \n {{ \'tb.rulenode.check-all-keys\' | translate }}\n \n
tb.rulenode.check-all-keys-hint
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}\n"],components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:te.MatChipList,selector:"mat-chip-list",inputs:["aria-orientation","multiple","compareWith","value","required","placeholder","disabled","selectable","tabIndex","errorStateMatcher"],outputs:["change","valueChange"],exportAs:["matChipList"]},{type:oe.MatIcon,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:D.MatLabel,selector:"mat-label"},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:te.MatChip,selector:"mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]",inputs:["color","disableRipple","tabIndex","selected","value","selectable","disabled","removable"],outputs:["selectionChange","destroyed","removed"],exportAs:["matChip"]},{type:te.MatChipRemove,selector:"[matChipRemove]"},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:te.MatChipInput,selector:"input[matChipInputFor]",inputs:["matChipInputSeparatorKeyCodes","placeholder","id","matChipInputFor","matChipInputAddOnBlur","disabled"],outputs:["matChipInputTokenEnd"],exportAs:["matChipInput","matChipInputFor"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Kt,decorators:[{type:o,args:[{selector:"tb-filter-node-check-message-config",templateUrl:"./check-message-config.component.html",styleUrls:["./check-message-config.component.scss"]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Bt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.entitySearchDirection=Object.keys(c),this.entitySearchDirectionTranslationsMap=g}configForm(){return this.checkRelationConfigForm}onConfigurationSet(e){this.checkRelationConfigForm=this.fb.group({checkForSingleEntity:[!!e&&e.checkForSingleEntity,[]],direction:[e?e.direction:null,[]],entityType:[e?e.entityType:null,e&&e.checkForSingleEntity?[k.required]:[]],entityId:[e?e.entityId:null,e&&e.checkForSingleEntity?[k.required]:[]],relationType:[e?e.relationType:null,[k.required]]})}validatorTriggers(){return["checkForSingleEntity"]}updateValidators(e){const t=this.checkRelationConfigForm.get("checkForSingleEntity").value;this.checkRelationConfigForm.get("entityType").setValidators(t?[k.required]:[]),this.checkRelationConfigForm.get("entityType").updateValueAndValidity({emitEvent:e}),this.checkRelationConfigForm.get("entityId").setValidators(t?[k.required]:[]),this.checkRelationConfigForm.get("entityId").updateValueAndValidity({emitEvent:e})}}e("CheckRelationConfigComponent",Bt),Bt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Bt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Bt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Bt,selector:"tb-filter-node-check-relation-config",usesInheritance:!0,ngImport:t,template:'
\n \n {{ \'tb.rulenode.check-relation-to-specific-entity\' | translate }}\n \n
tb.rulenode.check-relation-hint
\n \n relation.direction\n \n \n {{ entitySearchDirectionTranslationsMap.get(direction) | translate }}\n \n \n \n
\n \n \n \n \n
\n \n \n
\n',components:[{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]},{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:ae.EntityTypeSelectComponent,selector:"tb-entity-type-select",inputs:["allowedEntityTypes","useAliasEntityTypes","showLabel","required","disabled"]},{type:ve.EntityAutocompleteComponent,selector:"tb-entity-autocomplete",inputs:["entityType","entitySubtype","excludeEntityIds","labelText","requiredText","required","disabled"],outputs:["entityChanged"]},{type:ye.RelationTypeAutocompleteComponent,selector:"tb-relation-type-autocomplete",inputs:["required","disabled"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:D.MatLabel,selector:"mat-label"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Bt,decorators:[{type:o,args:[{selector:"tb-filter-node-check-relation-config",templateUrl:"./check-relation-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class jt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.perimeterType=Ae,this.perimeterTypes=Object.keys(Ae),this.perimeterTypeTranslationMap=Ge,this.rangeUnits=Object.keys(Ee),this.rangeUnitTranslationMap=Pe}configForm(){return this.geoFilterConfigForm}onConfigurationSet(e){this.geoFilterConfigForm=this.fb.group({latitudeKeyName:[e?e.latitudeKeyName:null,[k.required]],longitudeKeyName:[e?e.longitudeKeyName:null,[k.required]],perimeterType:[e?e.perimeterType:null,[k.required]],fetchPerimeterInfoFromMessageMetadata:[!!e&&e.fetchPerimeterInfoFromMessageMetadata,[]],perimeterKeyName:[e?e.perimeterKeyName:null,[]],centerLatitude:[e?e.centerLatitude:null,[]],centerLongitude:[e?e.centerLatitude:null,[]],range:[e?e.range:null,[]],rangeUnit:[e?e.rangeUnit:null,[]],polygonsDefinition:[e?e.polygonsDefinition:null,[]]})}validatorTriggers(){return["fetchPerimeterInfoFromMessageMetadata","perimeterType"]}updateValidators(e){const t=this.geoFilterConfigForm.get("fetchPerimeterInfoFromMessageMetadata").value,o=this.geoFilterConfigForm.get("perimeterType").value;t?this.geoFilterConfigForm.get("perimeterKeyName").setValidators([k.required]):this.geoFilterConfigForm.get("perimeterKeyName").setValidators([]),t||o!==Ae.CIRCLE?(this.geoFilterConfigForm.get("centerLatitude").setValidators([]),this.geoFilterConfigForm.get("centerLongitude").setValidators([]),this.geoFilterConfigForm.get("range").setValidators([]),this.geoFilterConfigForm.get("rangeUnit").setValidators([])):(this.geoFilterConfigForm.get("centerLatitude").setValidators([k.required,k.min(-90),k.max(90)]),this.geoFilterConfigForm.get("centerLongitude").setValidators([k.required,k.min(-180),k.max(180)]),this.geoFilterConfigForm.get("range").setValidators([k.required,k.min(0)]),this.geoFilterConfigForm.get("rangeUnit").setValidators([k.required])),t||o!==Ae.POLYGON?this.geoFilterConfigForm.get("polygonsDefinition").setValidators([]):this.geoFilterConfigForm.get("polygonsDefinition").setValidators([k.required]),this.geoFilterConfigForm.get("perimeterKeyName").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("centerLatitude").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("centerLongitude").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("range").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("rangeUnit").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("polygonsDefinition").updateValueAndValidity({emitEvent:e})}}e("GpsGeoFilterConfigComponent",jt),jt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:jt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),jt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:jt,selector:"tb-filter-node-gps-geofencing-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.latitude-key-name\n \n \n {{ \'tb.rulenode.latitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.longitude-key-name\n \n \n {{ \'tb.rulenode.longitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \n \n \n {{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}\n \n \n tb.rulenode.perimeter-key-name\n \n \n {{ \'tb.rulenode.perimeter-key-name-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.circle-center-latitude\n \n \n {{ \'tb.rulenode.circle-center-latitude-required\' | translate }}\n \n \n \n tb.rulenode.circle-center-longitude\n \n \n {{ \'tb.rulenode.circle-center-longitude-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.range\n \n \n {{ \'tb.rulenode.range-required\' | translate }}\n \n \n \n tb.rulenode.range-units\n \n \n {{ rangeUnitTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n
\n \n tb.rulenode.polygon-definition\n \n \n {{ \'tb.rulenode.polygon-definition-required\' | translate }}\n \n \n
\n
\n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:V.MatCheckbox,selector:"mat-checkbox",inputs:["disableRipple","color","tabIndex","aria-label","aria-labelledby","id","labelPosition","name","required","checked","disabled","indeterminate","aria-describedby","value"],outputs:["change","indeterminateChange"],exportAs:["matCheckbox"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:E.DefaultLayoutGapDirective,selector:" [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]",inputs:["fxLayoutGap","fxLayoutGap.xs","fxLayoutGap.sm","fxLayoutGap.md","fxLayoutGap.lg","fxLayoutGap.xl","fxLayoutGap.lt-sm","fxLayoutGap.lt-md","fxLayoutGap.lt-lg","fxLayoutGap.lt-xl","fxLayoutGap.gt-xs","fxLayoutGap.gt-sm","fxLayoutGap.gt-md","fxLayoutGap.gt-lg"]},{type:q.MinValidator,selector:"input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",inputs:["min"]},{type:q.MaxValidator,selector:"input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",inputs:["max"]},{type:q.NumberValueAccessor,selector:"input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]"}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:jt,decorators:[{type:o,args:[{selector:"tb-filter-node-gps-geofencing-config",templateUrl:"./gps-geo-filter-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class zt extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.messageTypeConfigForm}onConfigurationSet(e){this.messageTypeConfigForm=this.fb.group({messageTypes:[e?e.messageTypes:null,[k.required]]})}}e("MessageTypeConfigComponent",zt),zt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:zt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),zt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:zt,selector:"tb-filter-node-message-type-config",usesInheritance:!0,ngImport:t,template:'
\n \n
\n',components:[{type:kt,selector:"tb-message-types-config",inputs:["required","label","placeholder","disabled"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:zt,decorators:[{type:o,args:[{selector:"tb-filter-node-message-type-config",templateUrl:"./message-type-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class _t extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.allowedEntityTypes=[x.DEVICE,x.ASSET,x.ENTITY_VIEW,x.TENANT,x.CUSTOMER,x.USER,x.DASHBOARD,x.RULE_CHAIN,x.RULE_NODE]}configForm(){return this.originatorTypeConfigForm}onConfigurationSet(e){this.originatorTypeConfigForm=this.fb.group({originatorTypes:[e?e.originatorTypes:null,[k.required]]})}}e("OriginatorTypeConfigComponent",_t),_t.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:_t,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),_t.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:_t,selector:"tb-filter-node-originator-type-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n \n
\n',styles:[":host ::ng-deep tb-entity-type-list .mat-form-field-flex{padding-top:0}:host ::ng-deep tb-entity-type-list .mat-form-field-infix{border-top:0}\n"],components:[{type:Ie.EntityTypeListComponent,selector:"tb-entity-type-list",inputs:["required","disabled","allowedEntityTypes","ignoreAuthorityFilter"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:E.DefaultFlexDirective,selector:" [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]",inputs:["fxFlex","fxFlex.xs","fxFlex.sm","fxFlex.md","fxFlex.lg","fxFlex.xl","fxFlex.lt-sm","fxFlex.lt-md","fxFlex.lt-lg","fxFlex.lt-xl","fxFlex.gt-xs","fxFlex.gt-sm","fxFlex.gt-md","fxFlex.gt-lg"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:_t,decorators:[{type:o,args:[{selector:"tb-filter-node-originator-type-config",templateUrl:"./originator-type-config.component.html",styleUrls:["./originator-type-config.component.scss"]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Qt extends s{constructor(e,t,o,r){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=o,this.translate=r}configForm(){return this.scriptConfigForm}onConfigurationSet(e){this.scriptConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[k.required]]})}testScript(){const e=this.scriptConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(e,"filter",this.translate.instant("tb.rulenode.filter"),"Filter",["msg","metadata","msgType"],this.ruleNodeId,"rulenode/filter_node_script_fn").subscribe((e=>{e&&this.scriptConfigForm.get("jsScript").setValue(e)}))}onValidate(){this.jsFuncComponent.validateOnSubmit()}}e("ScriptConfigComponent",Qt),Qt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Qt,deps:[{token:T.Store},{token:q.FormBuilder},{token:Q.NodeScriptTestService},{token:P.TranslateService}],target:t.ɵɵFactoryTarget.Component}),Qt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Qt,selector:"tb-filter-node-script-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0,static:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n \n \n
\n \n
\n
\n',components:[{type:Y.JsFuncComponent,selector:"tb-js-func",inputs:["functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","noValidate","required"]},{type:J.MatButton,selector:"button[mat-button], button[mat-raised-button], button[mat-icon-button], button[mat-fab], button[mat-mini-fab], button[mat-stroked-button], button[mat-flat-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Qt,decorators:[{type:o,args:[{selector:"tb-filter-node-script-config",templateUrl:"./script-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder},{type:Q.NodeScriptTestService},{type:P.TranslateService}]},propDecorators:{jsFuncComponent:[{type:a,args:["jsFuncComponent",{static:!0}]}]}});class $t extends s{constructor(e,t,o,r){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=o,this.translate=r}configForm(){return this.switchConfigForm}onConfigurationSet(e){this.switchConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[k.required]]})}testScript(){const e=this.switchConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(e,"switch",this.translate.instant("tb.rulenode.switch"),"Switch",["msg","metadata","msgType"],this.ruleNodeId,"rulenode/switch_node_script_fn").subscribe((e=>{e&&this.switchConfigForm.get("jsScript").setValue(e)}))}onValidate(){this.jsFuncComponent.validateOnSubmit()}}e("SwitchConfigComponent",$t),$t.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:$t,deps:[{token:T.Store},{token:q.FormBuilder},{token:Q.NodeScriptTestService},{token:P.TranslateService}],target:t.ɵɵFactoryTarget.Component}),$t.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:$t,selector:"tb-filter-node-switch-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0,static:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n \n \n
\n \n
\n
\n',components:[{type:Y.JsFuncComponent,selector:"tb-js-func",inputs:["functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","noValidate","required"]},{type:J.MatButton,selector:"button[mat-button], button[mat-raised-button], button[mat-icon-button], button[mat-fab], button[mat-mini-fab], button[mat-stroked-button], button[mat-flat-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:$t,decorators:[{type:o,args:[{selector:"tb-filter-node-switch-config",templateUrl:"./switch-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder},{type:Q.NodeScriptTestService},{type:P.TranslateService}]},propDecorators:{jsFuncComponent:[{type:a,args:["jsFuncComponent",{static:!0}]}]}});class Wt{}e("RuleNodeCoreConfigFilterModule",Wt),Wt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Wt,deps:[],target:t.ɵɵFactoryTarget.NgModule}),Wt.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Wt,declarations:[Kt,Bt,jt,zt,_t,Qt,$t,Ut],imports:[O,F,Mt],exports:[Kt,Bt,jt,zt,_t,Qt,$t,Ut]}),Wt.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Wt,imports:[[O,F,Mt]]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Wt,decorators:[{type:i,args:[{declarations:[Kt,Bt,jt,zt,_t,Qt,$t,Ut],imports:[O,F,Mt],exports:[Kt,Bt,jt,zt,_t,Qt,$t,Ut]}]}]});class Yt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.originatorSource=Me,this.originatorSources=Object.keys(Me),this.originatorSourceTranslationMap=Se}configForm(){return this.changeOriginatorConfigForm}onConfigurationSet(e){this.changeOriginatorConfigForm=this.fb.group({originatorSource:[e?e.originatorSource:null,[k.required]],relationsQuery:[e?e.relationsQuery:null,[]]})}validatorTriggers(){return["originatorSource"]}updateValidators(e){const t=this.changeOriginatorConfigForm.get("originatorSource").value;t&&t===Me.RELATED?this.changeOriginatorConfigForm.get("relationsQuery").setValidators([k.required]):this.changeOriginatorConfigForm.get("relationsQuery").setValidators([]),this.changeOriginatorConfigForm.get("relationsQuery").updateValueAndValidity({emitEvent:e})}}e("ChangeOriginatorConfigComponent",Yt),Yt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Yt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Yt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Yt,selector:"tb-transformation-node-change-originator-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.originator-source\n \n \n {{ originatorSourceTranslationMap.get(source) | translate }}\n \n \n \n
\n \n \n \n
\n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]},{type:qt,selector:"tb-relations-query-config",inputs:["disabled","required"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Yt,decorators:[{type:o,args:[{selector:"tb-transformation-node-change-originator-config",templateUrl:"./change-originator-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Jt extends s{constructor(e,t,o,r){super(e),this.store=e,this.fb=t,this.nodeScriptTestService=o,this.translate=r}configForm(){return this.scriptConfigForm}onConfigurationSet(e){this.scriptConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[k.required]]})}testScript(){const e=this.scriptConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(e,"update",this.translate.instant("tb.rulenode.transformer"),"Transform",["msg","metadata","msgType"],this.ruleNodeId,"rulenode/transformation_node_script_fn").subscribe((e=>{e&&this.scriptConfigForm.get("jsScript").setValue(e)}))}onValidate(){this.jsFuncComponent.validateOnSubmit()}}e("TransformScriptConfigComponent",Jt),Jt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Jt,deps:[{token:T.Store},{token:q.FormBuilder},{token:Q.NodeScriptTestService},{token:P.TranslateService}],target:t.ɵɵFactoryTarget.Component}),Jt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Jt,selector:"tb-transformation-node-script-config",viewQueries:[{propertyName:"jsFuncComponent",first:!0,predicate:["jsFuncComponent"],descendants:!0,static:!0}],usesInheritance:!0,ngImport:t,template:'
\n \n \n \n
\n \n
\n
\n',components:[{type:Y.JsFuncComponent,selector:"tb-js-func",inputs:["functionName","functionArgs","validationArgs","resultType","disabled","fillHeight","editorCompleter","globalVariables","disableUndefinedCheck","helpId","noValidate","required"]},{type:J.MatButton,selector:"button[mat-button], button[mat-raised-button], button[mat-icon-button], button[mat-fab], button[mat-mini-fab], button[mat-stroked-button], button[mat-flat-button]",inputs:["disabled","disableRipple","color"],exportAs:["matButton"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Jt,decorators:[{type:o,args:[{selector:"tb-transformation-node-script-config",templateUrl:"./script-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder},{type:Q.NodeScriptTestService},{type:P.TranslateService}]},propDecorators:{jsFuncComponent:[{type:a,args:["jsFuncComponent",{static:!0}]}]}});class Zt extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.mailBodyTypes=[{name:"tb.mail-body-type.plain-text",value:"false"},{name:"tb.mail-body-type.html",value:"true"},{name:"tb.mail-body-type.dynamic",value:"dynamic"}]}configForm(){return this.toEmailConfigForm}onConfigurationSet(e){this.toEmailConfigForm=this.fb.group({fromTemplate:[e?e.fromTemplate:null,[k.required]],toTemplate:[e?e.toTemplate:null,[k.required]],ccTemplate:[e?e.ccTemplate:null,[]],bccTemplate:[e?e.bccTemplate:null,[]],subjectTemplate:[e?e.subjectTemplate:null,[k.required]],mailBodyType:[e?e.mailBodyType:null],isHtmlTemplate:[e?e.isHtmlTemplate:null],bodyTemplate:[e?e.bodyTemplate:null,[k.required]]}),this.toEmailConfigForm.get("mailBodyType").valueChanges.pipe(ue([null==e?void 0:e.subjectTemplate])).subscribe((e=>{"dynamic"===e?(this.toEmailConfigForm.get("isHtmlTemplate").patchValue("",{emitEvent:!1}),this.toEmailConfigForm.get("isHtmlTemplate").setValidators(k.required)):this.toEmailConfigForm.get("isHtmlTemplate").clearValidators(),this.toEmailConfigForm.get("isHtmlTemplate").updateValueAndValidity()}))}}e("ToEmailConfigComponent",Zt),Zt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Zt,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),Zt.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:Zt,selector:"tb-transformation-node-to-email-config",usesInheritance:!0,ngImport:t,template:'
\n \n tb.rulenode.from-template\n \n \n {{ \'tb.rulenode.from-template-required\' | translate }}\n \n \n \n \n tb.rulenode.to-template\n \n \n {{ \'tb.rulenode.to-template-required\' | translate }}\n \n \n \n \n tb.rulenode.cc-template\n \n \n \n \n tb.rulenode.bcc-template\n \n \n \n \n tb.rulenode.subject-template\n \n \n {{ \'tb.rulenode.subject-template-required\' | translate }}\n \n \n \n \n tb.rulenode.mail-body-type\n \n \n {{ type.name | translate }}\n \n \n \n \n tb.rulenode.dynamic-mail-body-type\n \n \n \n \n tb.rulenode.body-template\n \n \n {{ \'tb.rulenode.body-template-required\' | translate }}\n \n \n \n
\n',components:[{type:D.MatFormField,selector:"mat-form-field",inputs:["color","floatLabel","appearance","hideRequiredMarker","hintLabel"],exportAs:["matFormField"]},{type:U.MatSelect,selector:"mat-select",inputs:["disabled","disableRipple","tabIndex"],exportAs:["matSelect"]},{type:K.MatOption,selector:"mat-option",exportAs:["matOption"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:D.MatLabel,selector:"mat-label"},{type:P.TranslateDirective,selector:"[translate],[ngx-translate]",inputs:["translate","translateParams"]},{type:R.MatInput,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["id","disabled","required","type","value","readonly","placeholder","errorStateMatcher","aria-describedby"],exportAs:["matInput"]},{type:q.DefaultValueAccessor,selector:"input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]"},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]},{type:w.NgIf,selector:"[ngIf]",inputs:["ngIf","ngIfThen","ngIfElse"]},{type:D.MatError,selector:"mat-error",inputs:["id"]},{type:D.MatHint,selector:"mat-hint",inputs:["align","id"]},{type:w.NgForOf,selector:"[ngFor][ngForOf]",inputs:["ngForOf","ngForTrackBy","ngForTemplate"]}],pipes:{translate:P.TranslatePipe,safeHtml:Te}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Zt,decorators:[{type:o,args:[{selector:"tb-transformation-node-to-email-config",templateUrl:"./to-email-config.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class Xt{}e("RulenodeCoreConfigTransformModule",Xt),Xt.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Xt,deps:[],target:t.ɵɵFactoryTarget.NgModule}),Xt.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Xt,declarations:[Yt,Jt,Zt],imports:[O,F,Mt],exports:[Yt,Jt,Zt]}),Xt.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Xt,imports:[[O,F,Mt]]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:Xt,decorators:[{type:i,args:[{declarations:[Yt,Jt,Zt],imports:[O,F,Mt],exports:[Yt,Jt,Zt]}]}]});class eo extends s{constructor(e,t){super(e),this.store=e,this.fb=t,this.entityType=x}configForm(){return this.ruleChainInputConfigForm}onConfigurationSet(e){this.ruleChainInputConfigForm=this.fb.group({ruleChainId:[e?e.ruleChainId:null,[k.required]]})}}e("RuleChainInputComponent",eo),eo.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:eo,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),eo.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:eo,selector:"tb-flow-node-rule-chain-input-config",usesInheritance:!0,ngImport:t,template:'
\n \n \n
\n',components:[{type:ve.EntityAutocompleteComponent,selector:"tb-entity-autocomplete",inputs:["entityType","entitySubtype","excludeEntityIds","labelText","requiredText","required","disabled"],outputs:["entityChanged"]}],directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]},{type:q.RequiredValidator,selector:":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",inputs:["required"]},{type:q.NgControlStatus,selector:"[formControlName],[ngModel],[formControl]"},{type:q.FormControlName,selector:"[formControlName]",inputs:["disabled","formControlName","ngModel"],outputs:["ngModelChange"]}]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:eo,decorators:[{type:o,args:[{selector:"tb-flow-node-rule-chain-input-config",templateUrl:"./rule-chain-input.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class to extends s{constructor(e,t){super(e),this.store=e,this.fb=t}configForm(){return this.ruleChainOutputConfigForm}onConfigurationSet(e){this.ruleChainOutputConfigForm=this.fb.group({})}}e("RuleChainOutputComponent",to),to.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:to,deps:[{token:T.Store},{token:q.FormBuilder}],target:t.ɵɵFactoryTarget.Component}),to.ɵcmp=t.ɵɵngDeclareComponent({minVersion:"12.0.0",version:"12.2.15",type:to,selector:"tb-flow-node-rule-chain-output-config",usesInheritance:!0,ngImport:t,template:'
\n
\n
\n',directives:[{type:E.DefaultLayoutDirective,selector:" [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]",inputs:["fxLayout","fxLayout.xs","fxLayout.sm","fxLayout.md","fxLayout.lg","fxLayout.xl","fxLayout.lt-sm","fxLayout.lt-md","fxLayout.lt-lg","fxLayout.lt-xl","fxLayout.gt-xs","fxLayout.gt-sm","fxLayout.gt-md","fxLayout.gt-lg"]},{type:q.NgControlStatusGroup,selector:"[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]"},{type:q.FormGroupDirective,selector:"[formGroup]",inputs:["formGroup"],outputs:["ngSubmit"],exportAs:["ngForm"]}],pipes:{translate:P.TranslatePipe}}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:to,decorators:[{type:o,args:[{selector:"tb-flow-node-rule-chain-output-config",templateUrl:"./rule-chain-output.component.html",styleUrls:[]}]}],ctorParameters:function(){return[{type:T.Store},{type:q.FormBuilder}]}});class oo{}e("RuleNodeCoreConfigFlowModule",oo),oo.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:oo,deps:[],target:t.ɵɵFactoryTarget.NgModule}),oo.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:oo,declarations:[eo,to],imports:[O,F,Mt],exports:[eo,to]}),oo.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:oo,imports:[[O,F,Mt]]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:oo,decorators:[{type:i,args:[{declarations:[eo,to],imports:[O,F,Mt],exports:[eo,to]}]}]});class ro{constructor(e){!function(e){e.setTranslation("en_US",{tb:{rulenode:{"create-entity-if-not-exists":"Create new entity if not exists","create-entity-if-not-exists-hint":"Create a new entity set above if it does not exist.","entity-name-pattern":"Name pattern","entity-name-pattern-required":"Name pattern is required","entity-type-pattern":"Type pattern","entity-type-pattern-required":"Type pattern is required","entity-cache-expiration":"Entities cache expiration time (sec)","entity-cache-expiration-hint":"Specifies maximum time interval allowed to store found entity records. 0 value means that records will never expire.","entity-cache-expiration-required":"Entities cache expiration time is required.","entity-cache-expiration-range":"Entities cache expiration time should be greater than or equal to 0.","customer-name-pattern":"Customer name pattern","customer-name-pattern-required":"Customer name pattern is required","create-customer-if-not-exists":"Create new customer if not exists","customer-cache-expiration":"Customers cache expiration time (sec)","customer-cache-expiration-hint":"Specifies maximum time interval allowed to store found customer records. 0 value means that records will never expire.","customer-cache-expiration-required":"Customers cache expiration time is required.","customer-cache-expiration-range":"Customers cache expiration time should be greater than or equal to 0.","start-interval":"Start Interval","end-interval":"End Interval","start-interval-time-unit":"Start Interval Time Unit","end-interval-time-unit":"End Interval Time Unit","fetch-mode":"Fetch mode","fetch-mode-hint":"If selected fetch mode 'ALL' you able to choose telemetry sampling order.","order-by":"Order by","order-by-hint":"Select to choose telemetry sampling order.",limit:"Limit","limit-hint":"Min limit value is 2, max - 1000. In case you want to fetch a single entry, select fetch mode 'FIRST' or 'LAST'.","time-unit-milliseconds":"Milliseconds","time-unit-seconds":"Seconds","time-unit-minutes":"Minutes","time-unit-hours":"Hours","time-unit-days":"Days","time-value-range":"Time value should be in a range from 1 to 2147483647.","start-interval-value-required":"Start interval value is required.","end-interval-value-required":"End interval value is required.",filter:"Filter",switch:"Switch","message-type":"Message type","message-type-required":"Message type is required.","message-types-filter":"Message types filter","no-message-types-found":"No message types found","no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one!","message-types-required":"Message types are required.","client-attributes":"Client attributes","shared-attributes":"Shared attributes","server-attributes":"Server attributes","notify-device":"Notify Device","notify-device-hint":"If the message arrives from the device, we will push it back to the device by default.","latest-timeseries":"Latest timeseries","timeseries-key":"Timeseries key","data-keys":"Message data","metadata-keys":"Message metadata","relations-query":"Relations query","device-relations-query":"Device relations query","max-relation-level":"Max relation level","relation-type-pattern":"Relation type pattern","relation-type-pattern-required":"Relation type pattern is required","relation-types-list":"Relation types to propagate","relation-types-list-hint":"If Propagate relation types are not selected, alarms will be propagated without filtering by relation type.","unlimited-level":"Unlimited level","latest-telemetry":"Latest telemetry","attr-mapping":"Attributes mapping","source-attribute":"Source attribute","source-attribute-required":"Source attribute is required.","source-telemetry":"Source telemetry","source-telemetry-required":"Source telemetry is required.","target-attribute":"Target attribute","target-attribute-required":"Target attribute is required.","attr-mapping-required":"At least one attribute mapping should be specified.","fields-mapping":"Fields mapping","fields-mapping-required":"At least one field mapping should be specified.","source-field":"Source field","source-field-required":"Source field is required.","originator-source":"Originator source","originator-customer":"Customer","originator-tenant":"Tenant","originator-related":"Related","originator-alarm-originator":"Alarm Originator","clone-message":"Clone message",transform:"Transform","default-ttl":"Default TTL in seconds","default-ttl-required":"Default TTL is required.","min-default-ttl-message":"Only 0 minimum TTL is allowed.","message-count":"Message count (0 - unlimited)","message-count-required":"Message count is required.","min-message-count-message":"Only 0 minimum message count is allowed.","period-seconds":"Period in seconds","period-seconds-required":"Period is required.","use-metadata-period-in-seconds-patterns":"Use period in seconds pattern","use-metadata-period-in-seconds-patterns-hint":"If selected, rule node use period in seconds interval pattern from message metadata or data assuming that intervals are in the seconds.","period-in-seconds-pattern":"Period in seconds pattern","period-in-seconds-pattern-required":"Period in seconds pattern is required","min-period-seconds-message":"Only 1 second minimum period is allowed.",originator:"Originator","message-body":"Message body","message-metadata":"Message metadata",generate:"Generate","test-generator-function":"Test generator function",generator:"Generator","test-filter-function":"Test filter function","test-switch-function":"Test switch function","test-transformer-function":"Test transformer function",transformer:"Transformer","alarm-create-condition":"Alarm create condition","test-condition-function":"Test condition function","alarm-clear-condition":"Alarm clear condition","alarm-details-builder":"Alarm details builder","test-details-function":"Test details function","alarm-type":"Alarm type","alarm-type-required":"Alarm type is required.","alarm-severity":"Alarm severity","alarm-severity-required":"Alarm severity is required","alarm-severity-pattern":"Alarm severity pattern","alarm-status-filter":"Alarm status filter","alarm-status-list-empty":"Alarm status list is empty","no-alarm-status-matching":"No alarm status matching were found.",propagate:"Propagate alarm to related entities","propagate-to-owner":"Propagate alarm to entity owner (Customer or Tenant)","propagate-to-tenant":"Propagate alarm to Tenant",condition:"Condition",details:"Details","to-string":"To string","test-to-string-function":"Test to string function","from-template":"From Template","from-template-required":"From Template is required","to-template":"To Template","to-template-required":"To Template is required","mail-address-list-template-hint":'Comma separated address list, use ${metadataKey} for value from metadata, $[messageKey] for value from message body',"cc-template":"Cc Template","bcc-template":"Bcc Template","subject-template":"Subject Template","subject-template-required":"Subject Template is required","body-template":"Body Template","body-template-required":"Body Template is required","dynamic-mail-body-type":"Dynamic mail body type","mail-body-type":"Mail body type","request-id-metadata-attribute":"Request Id Metadata attribute name","timeout-sec":"Timeout in seconds","timeout-required":"Timeout is required","min-timeout-message":"Only 0 minimum timeout value is allowed.","endpoint-url-pattern":"Endpoint URL pattern","endpoint-url-pattern-required":"Endpoint URL pattern is required","request-method":"Request method","use-simple-client-http-factory":"Use simple client HTTP factory","ignore-request-body":"Without request body","read-timeout":"Read timeout in millis","read-timeout-hint":"The value of 0 means an infinite timeout","max-parallel-requests-count":"Max number of parallel requests","max-parallel-requests-count-hint":"The value of 0 specifies no limit in parallel processing",headers:"Headers","headers-hint":'Use ${metadataKey} for value from metadata, $[messageKey] for value from message body in header/value fields',header:"Header","header-required":"Header is required",value:"Value","value-required":"Value is required","topic-pattern":"Topic pattern","topic-pattern-required":"Topic pattern is required",topic:"Topic","topic-required":"Topic is required","bootstrap-servers":"Bootstrap servers","bootstrap-servers-required":"Bootstrap servers value is required","other-properties":"Other properties",key:"Key","key-required":"Key is required",retries:"Automatically retry times if fails","min-retries-message":"Only 0 minimum retries is allowed.","batch-size-bytes":"Produces batch size in bytes","min-batch-size-bytes-message":"Only 0 minimum batch size is allowed.","linger-ms":"Time to buffer locally (ms)","min-linger-ms-message":"Only 0 ms minimum value is allowed.","buffer-memory-bytes":"Client buffer max size in bytes","min-buffer-memory-message":"Only 0 minimum buffer size is allowed.",acks:"Number of acknowledgments","key-serializer":"Key serializer","key-serializer-required":"Key serializer is required","value-serializer":"Value serializer","value-serializer-required":"Value serializer is required","topic-arn-pattern":"Topic ARN pattern","topic-arn-pattern-required":"Topic ARN pattern is required","aws-access-key-id":"AWS Access Key ID","aws-access-key-id-required":"AWS Access Key ID is required","aws-secret-access-key":"AWS Secret Access Key","aws-secret-access-key-required":"AWS Secret Access Key is required","aws-region":"AWS Region","aws-region-required":"AWS Region is required","exchange-name-pattern":"Exchange name pattern","routing-key-pattern":"Routing key pattern","message-properties":"Message properties",host:"Host","host-required":"Host is required",port:"Port","port-required":"Port is required","port-range":"Port should be in a range from 1 to 65535.","virtual-host":"Virtual host",username:"Username",password:"Password","automatic-recovery":"Automatic recovery","connection-timeout-ms":"Connection timeout (ms)","min-connection-timeout-ms-message":"Only 0 ms minimum value is allowed.","handshake-timeout-ms":"Handshake timeout (ms)","min-handshake-timeout-ms-message":"Only 0 ms minimum value is allowed.","client-properties":"Client properties","queue-url-pattern":"Queue URL pattern","queue-url-pattern-required":"Queue URL pattern is required","delay-seconds":"Delay (seconds)","min-delay-seconds-message":"Only 0 seconds minimum value is allowed.","max-delay-seconds-message":"Only 900 seconds maximum value is allowed.",name:"Name","name-required":"Name is required","queue-type":"Queue type","sqs-queue-standard":"Standard","sqs-queue-fifo":"FIFO","gcp-project-id":"GCP project ID","gcp-project-id-required":"GCP project ID is required","gcp-service-account-key":"GCP service account key file","gcp-service-account-key-required":"GCP service account key file is required","pubsub-topic-name":"Topic name","pubsub-topic-name-required":"Topic name is required","message-attributes":"Message attributes","message-attributes-hint":'Use ${metadataKey} for value from metadata, $[messageKey] for value from message body in name/value fields',"connect-timeout":"Connection timeout (sec)","connect-timeout-required":"Connection timeout is required.","connect-timeout-range":"Connection timeout should be in a range from 1 to 200.","client-id":"Client ID","client-id-hint":'Hint: Optional. Leave empty for auto-generated Client ID. Be careful when specifying the Client ID. Majority of the MQTT brokers will not allow multiple connections with the same Client ID. To connect to such brokers, your mqtt Client ID must be unique. When platform is running in a micro-services mode, the copy of rule node is launched in each micro-service. This will automatically lead to multiple mqtt clients with the same ID and may cause failures of the rule node. To avoid such failures enable "Add Service ID as suffix to Client ID" option below.',"append-client-id-suffix":"Add Service ID as suffix to Client ID","client-id-suffix-hint":'Hint: Optional. Applied when "Client ID" specified explicitly. If selected then Service ID will be added to Client ID as a suffix. Helps to avoid failures when platform is running in a micro-services mode.',"device-id":"Device ID","device-id-required":"Device ID is required.","clean-session":"Clean session","enable-ssl":"Enable SSL",credentials:"Credentials","credentials-type":"Credentials type","credentials-type-required":"Credentials type is required.","credentials-anonymous":"Anonymous","credentials-basic":"Basic","credentials-pem":"PEM","credentials-pem-hint":"At least Server CA certificate file or a pair of Client certificate and Client private key files are required","credentials-sas":"Shared Access Signature","sas-key":"SAS Key","sas-key-required":"SAS Key is required.",hostname:"Hostname","hostname-required":"Hostname is required.","azure-ca-cert":"CA certificate file","username-required":"Username is required.","password-required":"Password is required.","ca-cert":"Server CA certificate file *","private-key":"Client private key file *",cert:"Client certificate file *","no-file":"No file selected.","drop-file":"Drop a file or click to select a file to upload.","private-key-password":"Private key password","use-system-smtp-settings":"Use system SMTP settings","use-metadata-interval-patterns":"Use interval patterns","use-metadata-interval-patterns-hint":"If selected, rule node use start and end interval patterns from message metadata or data assuming that intervals are in the milliseconds.","use-message-alarm-data":"Use message alarm data","overwrite-alarm-details":"Overwrite alarm details","use-alarm-severity-pattern":"Use alarm severity pattern","check-all-keys":"Check that all selected keys are present","check-all-keys-hint":"If selected, checks that all specified keys are present in the message data and metadata.","check-relation-to-specific-entity":"Check relation to specific entity","check-relation-hint":"Checks existence of relation to specific entity or to any entity based on direction and relation type.","delete-relation-to-specific-entity":"Delete relation to specific entity","delete-relation-hint":"Deletes relation from the originator of the incoming message to the specified entity or list of entities based on direction and type.","remove-current-relations":"Remove current relations","remove-current-relations-hint":"Removes current relations from the originator of the incoming message based on direction and type.","change-originator-to-related-entity":"Change originator to related entity","change-originator-to-related-entity-hint":"Used to process submitted message as a message from another entity.","start-interval-pattern":"Start interval pattern","end-interval-pattern":"End interval pattern","start-interval-pattern-required":"Start interval pattern is required","end-interval-pattern-required":"End interval pattern is required","smtp-protocol":"Protocol","smtp-host":"SMTP host","smtp-host-required":"SMTP host is required.","smtp-port":"SMTP port","smtp-port-required":"You must supply a smtp port.","smtp-port-range":"SMTP port should be in a range from 1 to 65535.","timeout-msec":"Timeout ms","min-timeout-msec-message":"Only 0 ms minimum value is allowed.","enter-username":"Enter username","enter-password":"Enter password","enable-tls":"Enable TLS","tls-version":"TLS version","enable-proxy":"Enable proxy","use-system-proxy-properties":"Use system proxy properties","proxy-host":"Proxy host","proxy-host-required":"Proxy host is required.","proxy-port":"Proxy port","proxy-port-required":"Proxy port is required.","proxy-port-range":"Proxy port should be in a range from 1 to 65535.","proxy-user":"Proxy user","proxy-password":"Proxy password","proxy-scheme":"Proxy scheme","numbers-to-template":"Phone Numbers To Template","numbers-to-template-required":"Phone Numbers To Template is required","numbers-to-template-hint":'Comma separated Phone Numbers, use ${metadataKey} for value from metadata, $[messageKey] for value from message body',"sms-message-template":"SMS message Template","sms-message-template-required":"SMS message Template is required","use-system-sms-settings":"Use system SMS provider settings","min-period-0-seconds-message":"Only 0 second minimum period is allowed.","max-pending-messages":"Maximum pending messages","max-pending-messages-required":"Maximum pending messages is required.","max-pending-messages-range":"Maximum pending messages should be in a range from 1 to 100000.","originator-types-filter":"Originator types filter","interval-seconds":"Interval in seconds","interval-seconds-required":"Interval is required.","min-interval-seconds-message":"Only 1 second minimum interval is allowed.","output-timeseries-key-prefix":"Output timeseries key prefix","output-timeseries-key-prefix-required":"Output timeseries key prefix required.","separator-hint":'You should press "enter" to complete field input.',"entity-details":"Select entity details:","entity-details-title":"Title","entity-details-country":"Country","entity-details-state":"State","entity-details-city":"City","entity-details-zip":"Zip","entity-details-address":"Address","entity-details-address2":"Address2","entity-details-additional_info":"Additional Info","entity-details-phone":"Phone","entity-details-email":"Email","add-to-metadata":"Add selected details to message metadata","add-to-metadata-hint":"If selected, adds the selected details keys to the message metadata instead of message data.","entity-details-list-empty":"No entity details selected.","no-entity-details-matching":"No entity details matching were found.","custom-table-name":"Custom table name","custom-table-name-required":"Table Name is required","custom-table-hint":"You should enter the table name without prefix 'cs_tb_'.","message-field":"Message field","message-field-required":"Message field is required.","table-col":"Table column","table-col-required":"Table column is required.","latitude-key-name":"Latitude key name","longitude-key-name":"Longitude key name","latitude-key-name-required":"Latitude key name is required.","longitude-key-name-required":"Longitude key name is required.","fetch-perimeter-info-from-message-metadata":"Fetch perimeter information from message metadata","perimeter-key-name":"Perimeter key name","perimeter-key-name-required":"Perimeter key name is required.","perimeter-circle":"Circle","perimeter-polygon":"Polygon","perimeter-type":"Perimeter type","circle-center-latitude":"Center latitude","circle-center-latitude-required":"Center latitude is required.","circle-center-longitude":"Center longitude","circle-center-longitude-required":"Center longitude is required.","range-unit-meter":"Meter","range-unit-kilometer":"Kilometer","range-unit-foot":"Foot","range-unit-mile":"Mile","range-unit-nautical-mile":"Nautical mile","range-units":"Range units",range:"Range","range-required":"Range is required.","polygon-definition":"Polygon definition","polygon-definition-required":"Polygon definition is required.","polygon-definition-hint":"Please, use the following format for manual definition of polygon: [[lat1,lon1],[lat2,lon2], ... ,[latN,lonN]].","min-inside-duration":"Minimal inside duration","min-inside-duration-value-required":"Minimal inside duration is required","min-inside-duration-time-unit":"Minimal inside duration time unit","min-outside-duration":"Minimal outside duration","min-outside-duration-value-required":"Minimal outside duration is required","min-outside-duration-time-unit":"Minimal outside duration time unit","tell-failure-if-absent":"Tell Failure","tell-failure-if-absent-hint":'If at least one selected key doesn\'t exist the outbound message will report "Failure".',"get-latest-value-with-ts":"Fetch Latest telemetry with Timestamp","get-latest-value-with-ts-hint":'If selected, latest telemetry values will be added to the outbound message metadata with timestamp, e.g: "temp": "{"ts":1574329385897, "value":42}"',"use-redis-queue":"Use redis queue for message persistence","trim-redis-queue":"Trim redis queue","redis-queue-max-size":"Redis queue max size","add-metadata-key-values-as-kafka-headers":"Add Message metadata key-value pairs to Kafka record headers","add-metadata-key-values-as-kafka-headers-hint":"If selected, key-value pairs from message metadata will be added to the outgoing records headers as byte arrays with predefined charset encoding.","charset-encoding":"Charset encoding","charset-encoding-required":"Charset encoding is required.","charset-us-ascii":"US-ASCII","charset-iso-8859-1":"ISO-8859-1","charset-utf-8":"UTF-8","charset-utf-16be":"UTF-16BE","charset-utf-16le":"UTF-16LE","charset-utf-16":"UTF-16","select-queue-hint":"The queue name can be selected from a drop-down list or add a custom name.","persist-alarm-rules":"Persist state of alarm rules","fetch-alarm-rules":"Fetch state of alarm rules","input-value-key":"Input value key","input-value-key-required":"Input value key is required.","output-value-key":"Output value key","output-value-key-required":"Output value key is required.",round:"Decimals","round-range":"Decimals should be in a range from 0 to 15.","use-cache":"Use cache for latest value","tell-failure-if-delta-is-negative":"Tell Failure if delta is negative","add-period-between-msgs":"Add period between messages","period-value-key":"Period value key","period-value-key-required":"Period value key is required.","general-pattern-hint":'Hint: use ${metadataKey} for value from metadata, $[messageKey] for value from message body',"alarm-severity-pattern-hint":'Hint: use ${metadataKey} for value from metadata, $[messageKey] for value from message body. Alarm severity should be system (CRITICAL, MAJOR etc.)',"output-node-name-hint":"The rule node name corresponds to the relation type of the output message, and it is used to forward messages to other rule nodes in the caller rule chain.","skip-latest-persistence":"Skip latest persistence","use-server-ts":"Use server ts","use-server-ts-hint":"Enable this setting to use the timestamp of the message processing instead of the timestamp from the message. Useful for all sorts of sequential processing if you merge messages from multiple sources (devices, assets, etc).","kv-map-pattern-hint":'Hint: use ${metadataKey} for value from metadata, $[messageKey] for value from message body to substitute "Source" and "Target" key names'},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"},"mail-body-type":{"plain-text":"Plain Text",html:"HTML",dynamic:"Dynamic"}}},!0)}(e)}}e("RuleNodeCoreConfigModule",ro),ro.ɵfac=t.ɵɵngDeclareFactory({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ro,deps:[{token:P.TranslateService}],target:t.ɵɵFactoryTarget.NgModule}),ro.ɵmod=t.ɵɵngDeclareNgModule({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ro,declarations:[Ne],imports:[O,F],exports:[St,Wt,Ht,Xt,oo,Ne]}),ro.ɵinj=t.ɵɵngDeclareInjector({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ro,imports:[[O,F],St,Wt,Ht,Xt,oo]}),t.ɵɵngDeclareClassMetadata({minVersion:"12.0.0",version:"12.2.15",ngImport:t,type:ro,decorators:[{type:i,args:[{declarations:[Ne],imports:[O,F],exports:[St,Wt,Ht,Xt,oo,Ne]}]}],ctorParameters:function(){return[{type:P.TranslateService}]}})}}}));//# sourceMappingURL=rulenode-core-config.js.map From 928836c0c5dd1dc4c13f39a882ee0576f09f249e Mon Sep 17 00:00:00 2001 From: ShvaykaD Date: Wed, 11 May 2022 14:21:06 +0300 Subject: [PATCH 129/276] reverted TransportSqlTestSuite typo from previous commit & updated provision tests --- .../transport/TransportSqlTestSuite.java | 18 +-- .../MqttProvisionJsonDeviceTest.java | 149 +++++++----------- .../MqttProvisionProtoDeviceTest.java | 114 +++++--------- 3 files changed, 105 insertions(+), 176 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/TransportSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/transport/TransportSqlTestSuite.java index 5720e19e56..3f25ded621 100644 --- a/application/src/test/java/org/thingsboard/server/transport/TransportSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/transport/TransportSqlTestSuite.java @@ -20,15 +20,15 @@ import org.junit.runner.RunWith; @RunWith(ClasspathSuite.class) @ClasspathSuite.ClassnameFilters({ -// "org.thingsboard.server.transport.*.rpc.*Test", -// "org.thingsboard.server.transport.*.telemetry.timeseries.sql.*Test", -// "org.thingsboard.server.transport.*.telemetry.attributes.*Test", -// "org.thingsboard.server.transport.*.attributes.updates.*Test", -// "org.thingsboard.server.transport.*.attributes.request.*Test", - "org.thingsboard.server.transport.mqtt.claim.*Test", -// "org.thingsboard.server.transport.*.provision.*Test", -// "org.thingsboard.server.transport.*.credentials.*Test", -// "org.thingsboard.server.transport.lwm2m.*.sql.*Test" + "org.thingsboard.server.transport.*.rpc.*Test", + "org.thingsboard.server.transport.*.telemetry.timeseries.sql.*Test", + "org.thingsboard.server.transport.*.telemetry.attributes.*Test", + "org.thingsboard.server.transport.*.attributes.updates.*Test", + "org.thingsboard.server.transport.*.attributes.request.*Test", + "org.thingsboard.server.transport.*.claim.*Test", + "org.thingsboard.server.transport.*.provision.*Test", + "org.thingsboard.server.transport.*.credentials.*Test", + "org.thingsboard.server.transport.lwm2m.*.sql.*Test" }) public class TransportSqlTestSuite { diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/MqttProvisionJsonDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/MqttProvisionJsonDeviceTest.java index c7e0156372..0a06f99209 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/MqttProvisionJsonDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/MqttProvisionJsonDeviceTest.java @@ -15,13 +15,9 @@ */ package org.thingsboard.server.transport.mqtt.provision; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; import io.netty.handler.codec.mqtt.MqttQoS; import lombok.extern.slf4j.Slf4j; -import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; -import org.eclipse.paho.client.mqttv3.MqttAsyncClient; -import org.eclipse.paho.client.mqttv3.MqttCallback; -import org.eclipse.paho.client.mqttv3.MqttMessage; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -30,20 +26,22 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; -import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.msg.EncryptionUtil; -import org.thingsboard.server.common.transport.util.JsonUtils; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; +import org.thingsboard.server.transport.mqtt.MqttTestCallback; +import org.thingsboard.server.transport.mqtt.MqttTestClient; import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_PROVISION_REQUEST_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC; + @Slf4j @DaoSqlTest public class MqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest { @@ -97,10 +95,12 @@ public class MqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest { .provisionType(DeviceProfileProvisionType.DISABLED) .build(); super.processBeforeTest(configProperties); - byte[] result = createMqttClientAndPublish().getPayloadBytes(); - JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); - Assert.assertEquals("Provision data was not found!", response.get("errorMsg").getAsString()); - Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), response.get("status").getAsString()); + byte[] result = createMqttClientAndPublish(); + JsonNode response = JacksonUtil.fromBytes(result); + Assert.assertTrue(response.hasNonNull("errorMsg")); + Assert.assertTrue(response.hasNonNull("status")); + Assert.assertEquals("Provision data was not found!", response.get("errorMsg").asText()); + Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), response.get("status").asText()); } @@ -113,8 +113,10 @@ public class MqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest { .provisionSecret("testProvisionSecret") .build(); super.processBeforeTest(configProperties); - byte[] result = createMqttClientAndPublish().getPayloadBytes(); - JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); + byte[] result = createMqttClientAndPublish(); + JsonNode response = JacksonUtil.fromBytes(result); + Assert.assertTrue(response.hasNonNull("credentialsType")); + Assert.assertTrue(response.hasNonNull("status")); Device createdDevice = deviceService.findDeviceByTenantIdAndName(tenantId, "Test Provision device"); @@ -122,8 +124,8 @@ public class MqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest { DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, createdDevice.getId()); - Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.get("credentialsType").getAsString()); - Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.get("status").getAsString()); + Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.get("credentialsType").asText()); + Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.get("status").asText()); } @@ -137,8 +139,10 @@ public class MqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest { .build(); super.processBeforeTest(configProperties); String requestCredentials = ",\"credentialsType\": \"ACCESS_TOKEN\",\"token\": \"test_token\""; - byte[] result = createMqttClientAndPublish(requestCredentials).getPayloadBytes(); - JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); + byte[] result = createMqttClientAndPublish(requestCredentials); + JsonNode response = JacksonUtil.fromBytes(result); + Assert.assertTrue(response.hasNonNull("credentialsType")); + Assert.assertTrue(response.hasNonNull("status")); Device createdDevice = deviceService.findDeviceByTenantIdAndName(tenantId, "Test Provision device"); @@ -146,10 +150,10 @@ public class MqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest { DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, createdDevice.getId()); - Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.get("credentialsType").getAsString()); + Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.get("credentialsType").asText()); Assert.assertEquals(deviceCredentials.getCredentialsType().name(), "ACCESS_TOKEN"); Assert.assertEquals(deviceCredentials.getCredentialsId(), "test_token"); - Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.get("status").getAsString()); + Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.get("status").asText()); } @@ -163,8 +167,10 @@ public class MqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest { .build(); super.processBeforeTest(configProperties); String requestCredentials = ",\"credentialsType\": \"X509_CERTIFICATE\",\"hash\": \"testHash\""; - byte[] result = createMqttClientAndPublish(requestCredentials).getPayloadBytes(); - JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); + byte[] result = createMqttClientAndPublish(requestCredentials); + JsonNode response = JacksonUtil.fromBytes(result); + Assert.assertTrue(response.hasNonNull("credentialsType")); + Assert.assertTrue(response.hasNonNull("status")); Device createdDevice = deviceService.findDeviceByTenantIdAndName(tenantId, "Test Provision device"); @@ -172,7 +178,7 @@ public class MqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest { DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, createdDevice.getId()); - Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.get("credentialsType").getAsString()); + Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.get("credentialsType").asText()); Assert.assertEquals(deviceCredentials.getCredentialsType().name(), "X509_CERTIFICATE"); String cert = EncryptionUtil.certTrimNewLines(deviceCredentials.getCredentialsValue()); @@ -181,7 +187,7 @@ public class MqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest { Assert.assertEquals(deviceCredentials.getCredentialsId(), sha3Hash); Assert.assertEquals(deviceCredentials.getCredentialsValue(), "testHash"); - Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.get("status").getAsString()); + Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.get("status").asText()); } @@ -195,8 +201,10 @@ public class MqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest { .build(); super.processBeforeTest(configProperties); String requestCredentials = ",\"credentialsType\": \"MQTT_BASIC\",\"clientId\": \"test_clientId\",\"username\": \"test_username\",\"password\": \"test_password\""; - byte[] result = createMqttClientAndPublish(requestCredentials).getPayloadBytes(); - JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); + byte[] result = createMqttClientAndPublish(requestCredentials); + JsonNode response = JacksonUtil.fromBytes(result); + Assert.assertTrue(response.hasNonNull("credentialsType")); + Assert.assertTrue(response.hasNonNull("status")); Device createdDevice = deviceService.findDeviceByTenantIdAndName(tenantId, "Test Provision device"); @@ -204,7 +212,7 @@ public class MqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest { DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, createdDevice.getId()); - Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.get("credentialsType").getAsString()); + Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.get("credentialsType").asText()); Assert.assertEquals(deviceCredentials.getCredentialsType().name(), "MQTT_BASIC"); Assert.assertEquals(deviceCredentials.getCredentialsId(), EncryptionUtil.getSha3Hash("|", "test_clientId", "test_username")); @@ -214,7 +222,7 @@ public class MqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest { mqttCredentials.setPassword("test_password"); Assert.assertEquals(deviceCredentials.getCredentialsValue(), JacksonUtil.toString(mqttCredentials)); - Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.get("status").getAsString()); + Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.get("status").asText()); } protected void processTestProvisioningCheckPreProvisionedDevice() throws Exception { @@ -226,13 +234,15 @@ public class MqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest { .provisionSecret("testProvisionSecret") .build(); super.processBeforeTest(configProperties); - byte[] result = createMqttClientAndPublish().getPayloadBytes(); - JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); + byte[] result = createMqttClientAndPublish(); + JsonNode response = JacksonUtil.fromBytes(result); + Assert.assertTrue(response.hasNonNull("credentialsType")); + Assert.assertTrue(response.hasNonNull("status")); DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, savedDevice.getId()); - Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.get("credentialsType").getAsString()); - Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.get("status").getAsString()); + Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.get("credentialsType").asText()); + Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.get("status").asText()); } protected void processTestProvisioningWithBadKeyDevice() throws Exception { @@ -244,72 +254,29 @@ public class MqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest { .provisionSecret("testProvisionSecret") .build(); super.processBeforeTest(configProperties); - byte[] result = createMqttClientAndPublish().getPayloadBytes(); - JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); - Assert.assertEquals("Provision data was not found!", response.get("errorMsg").getAsString()); - Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), response.get("status").getAsString()); + byte[] result = createMqttClientAndPublish(); + JsonNode response = JacksonUtil.fromBytes(result); + Assert.assertTrue(response.hasNonNull("errorMsg")); + Assert.assertTrue(response.hasNonNull("status")); + Assert.assertEquals("Provision data was not found!", response.get("errorMsg").asText()); + Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), response.get("status").asText()); } - protected TestMqttCallback createMqttClientAndPublish() throws Exception { + protected byte[] createMqttClientAndPublish() throws Exception { return createMqttClientAndPublish(""); } - protected TestMqttCallback createMqttClientAndPublish(String deviceCredentials) throws Exception { + protected byte[] createMqttClientAndPublish(String deviceCredentials) throws Exception { String provisionRequestMsg = createTestProvisionMessage(deviceCredentials); - MqttAsyncClient client = getMqttAsyncClient("provision"); - TestMqttCallback onProvisionCallback = getTestMqttCallback(); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait("provision"); + MqttTestCallback onProvisionCallback = new MqttTestCallback(DEVICE_PROVISION_RESPONSE_TOPIC); client.setCallback(onProvisionCallback); - client.subscribe(MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC, MqttQoS.AT_MOST_ONCE.value()); - Thread.sleep(2000); - client.publish(MqttTopics.DEVICE_PROVISION_REQUEST_TOPIC, new MqttMessage(provisionRequestMsg.getBytes())); - onProvisionCallback.getLatch().await(3, TimeUnit.SECONDS); - return onProvisionCallback; - } - - - protected TestMqttCallback getTestMqttCallback() { - CountDownLatch latch = new CountDownLatch(1); - return new TestMqttCallback(latch); - } - - - protected static class TestMqttCallback implements MqttCallback { - - private final CountDownLatch latch; - private Integer qoS; - private byte[] payloadBytes; - - TestMqttCallback(CountDownLatch latch) { - this.latch = latch; - } - - public int getQoS() { - return qoS; - } - - public byte[] getPayloadBytes() { - return payloadBytes; - } - - public CountDownLatch getLatch() { - return latch; - } - - @Override - public void connectionLost(Throwable throwable) { - } - - @Override - public void messageArrived(String requestTopic, MqttMessage mqttMessage) throws Exception { - qoS = mqttMessage.getQos(); - payloadBytes = mqttMessage.getPayload(); - latch.countDown(); - } - - @Override - public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { - - } + client.subscribe(DEVICE_PROVISION_RESPONSE_TOPIC, MqttQoS.AT_MOST_ONCE); + client.publishAndWait(DEVICE_PROVISION_REQUEST_TOPIC, provisionRequestMsg.getBytes()); + onProvisionCallback.getSubscribeLatch().await(3, TimeUnit.SECONDS); + client.disconnect(); + return onProvisionCallback.getPayloadBytes(); } protected String createTestProvisionMessage(String deviceCredentials) { diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/MqttProvisionProtoDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/MqttProvisionProtoDeviceTest.java index 335f7d7d6c..9e61b69ebf 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/MqttProvisionProtoDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/MqttProvisionProtoDeviceTest.java @@ -17,10 +17,6 @@ package org.thingsboard.server.transport.mqtt.provision; import io.netty.handler.codec.mqtt.MqttQoS; import lombok.extern.slf4j.Slf4j; -import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; -import org.eclipse.paho.client.mqttv3.MqttAsyncClient; -import org.eclipse.paho.client.mqttv3.MqttCallback; -import org.eclipse.paho.client.mqttv3.MqttMessage; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -29,7 +25,6 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; -import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.msg.EncryptionUtil; @@ -46,11 +41,15 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateBasicMqttCre import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; +import org.thingsboard.server.transport.mqtt.MqttTestCallback; +import org.thingsboard.server.transport.mqtt.MqttTestClient; import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_PROVISION_REQUEST_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC; + @Slf4j @DaoSqlTest public class MqttProvisionProtoDeviceTest extends AbstractMqttIntegrationTest { @@ -104,9 +103,9 @@ public class MqttProvisionProtoDeviceTest extends AbstractMqttIntegrationTest { .provisionType(DeviceProfileProvisionType.DISABLED) .build(); processBeforeTest(configProperties); - ProvisionDeviceResponseMsg result = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes()); + ProvisionDeviceResponseMsg result = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish()); Assert.assertNotNull(result); - Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), result.getStatus().toString()); + Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), result.getStatus().name()); } protected void processTestProvisioningCreateNewDeviceWithoutCredentials() throws Exception { @@ -118,7 +117,7 @@ public class MqttProvisionProtoDeviceTest extends AbstractMqttIntegrationTest { .provisionSecret("testProvisionSecret") .build(); processBeforeTest(configProperties); - ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes()); + ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish()); Device createdDevice = deviceService.findDeviceByTenantIdAndName(tenantId, "Test Provision device"); @@ -126,8 +125,8 @@ public class MqttProvisionProtoDeviceTest extends AbstractMqttIntegrationTest { DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, createdDevice.getId()); - Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.getCredentialsType().toString()); - Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.getStatus().toString()); + Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.getCredentialsType().name()); + Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.getStatus().name()); } protected void processTestProvisioningCreateNewDeviceWithAccessToken() throws Exception { @@ -139,9 +138,12 @@ public class MqttProvisionProtoDeviceTest extends AbstractMqttIntegrationTest { .provisionSecret("testProvisionSecret") .build(); processBeforeTest(configProperties); - CredentialsDataProto requestCredentials = CredentialsDataProto.newBuilder().setValidateDeviceTokenRequestMsg(ValidateDeviceTokenRequestMsg.newBuilder().setToken("test_token").build()).build(); + CredentialsDataProto requestCredentials = CredentialsDataProto.newBuilder() + .setValidateDeviceTokenRequestMsg(ValidateDeviceTokenRequestMsg.newBuilder().setToken("test_token").build()) + .build(); - ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish(createTestsProvisionMessage(CredentialsType.ACCESS_TOKEN, requestCredentials)).getPayloadBytes()); + ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom( + createMqttClientAndPublish(createTestsProvisionMessage(CredentialsType.ACCESS_TOKEN, requestCredentials))); Device createdDevice = deviceService.findDeviceByTenantIdAndName(tenantId, "Test Provision device"); @@ -164,9 +166,13 @@ public class MqttProvisionProtoDeviceTest extends AbstractMqttIntegrationTest { .provisionSecret("testProvisionSecret") .build(); processBeforeTest(configProperties); - CredentialsDataProto requestCredentials = CredentialsDataProto.newBuilder().setValidateDeviceX509CertRequestMsg(ValidateDeviceX509CertRequestMsg.newBuilder().setHash("testHash").build()).build(); + CredentialsDataProto requestCredentials = CredentialsDataProto.newBuilder() + .setValidateDeviceX509CertRequestMsg( + ValidateDeviceX509CertRequestMsg.newBuilder().setHash("testHash").build()) + .build(); - ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish(createTestsProvisionMessage(CredentialsType.X509_CERTIFICATE, requestCredentials)).getPayloadBytes()); + ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom( + createMqttClientAndPublish(createTestsProvisionMessage(CredentialsType.X509_CERTIFICATE, requestCredentials))); Device createdDevice = deviceService.findDeviceByTenantIdAndName(tenantId, "Test Provision device"); @@ -203,7 +209,8 @@ public class MqttProvisionProtoDeviceTest extends AbstractMqttIntegrationTest { .build() ).build(); - ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish(createTestsProvisionMessage(CredentialsType.MQTT_BASIC, requestCredentials)).getPayloadBytes()); + ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom( + createMqttClientAndPublish(createTestsProvisionMessage(CredentialsType.MQTT_BASIC, requestCredentials))); Device createdDevice = deviceService.findDeviceByTenantIdAndName(tenantId, "Test Provision device"); @@ -233,12 +240,12 @@ public class MqttProvisionProtoDeviceTest extends AbstractMqttIntegrationTest { .provisionSecret("testProvisionSecret") .build(); processBeforeTest(configProperties); - ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes()); + ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish()); DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, savedDevice.getId()); - Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.getCredentialsType().toString()); - Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.getStatus().toString()); + Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.getCredentialsType().name()); + Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.getStatus().name()); } protected void processTestProvisioningWithBadKeyDevice() throws Exception { @@ -250,70 +257,25 @@ public class MqttProvisionProtoDeviceTest extends AbstractMqttIntegrationTest { .provisionSecret("testProvisionSecret") .build(); processBeforeTest(configProperties); - ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes()); - Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), response.getStatus().toString()); + ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish()); + Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), response.getStatus().name()); } - protected TestMqttCallback createMqttClientAndPublish() throws Exception { + protected byte[] createMqttClientAndPublish() throws Exception { byte[] provisionRequestMsg = createTestProvisionMessage(); return createMqttClientAndPublish(provisionRequestMsg); } - protected TestMqttCallback createMqttClientAndPublish(byte[] provisionRequestMsg) throws Exception { - MqttAsyncClient client = getMqttAsyncClient("provision"); - TestMqttCallback onProvisionCallback = getTestMqttCallback(); + protected byte[] createMqttClientAndPublish(byte[] provisionRequestMsg) throws Exception { + MqttTestClient client = new MqttTestClient(); + client.connectAndWait("provision"); + MqttTestCallback onProvisionCallback = new MqttTestCallback(DEVICE_PROVISION_RESPONSE_TOPIC); client.setCallback(onProvisionCallback); - client.subscribe(MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC, MqttQoS.AT_MOST_ONCE.value()); - Thread.sleep(2000); - client.publish(MqttTopics.DEVICE_PROVISION_REQUEST_TOPIC, new MqttMessage(provisionRequestMsg)); - onProvisionCallback.getLatch().await(3, TimeUnit.SECONDS); - return onProvisionCallback; - } - - - protected TestMqttCallback getTestMqttCallback() { - CountDownLatch latch = new CountDownLatch(1); - return new TestMqttCallback(latch); - } - - - protected static class TestMqttCallback implements MqttCallback { - - private final CountDownLatch latch; - private Integer qoS; - private byte[] payloadBytes; - - TestMqttCallback(CountDownLatch latch) { - this.latch = latch; - } - - public int getQoS() { - return qoS; - } - - public byte[] getPayloadBytes() { - return payloadBytes; - } - - public CountDownLatch getLatch() { - return latch; - } - - @Override - public void connectionLost(Throwable throwable) { - } - - @Override - public void messageArrived(String requestTopic, MqttMessage mqttMessage) throws Exception { - qoS = mqttMessage.getQos(); - payloadBytes = mqttMessage.getPayload(); - latch.countDown(); - } - - @Override - public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { - - } + client.subscribe(DEVICE_PROVISION_RESPONSE_TOPIC, MqttQoS.AT_MOST_ONCE); + client.publishAndWait(DEVICE_PROVISION_REQUEST_TOPIC, provisionRequestMsg); + onProvisionCallback.getSubscribeLatch().await(3, TimeUnit.SECONDS); + client.disconnect(); + return onProvisionCallback.getPayloadBytes(); } protected byte[] createTestsProvisionMessage(CredentialsType credentialsType, CredentialsDataProto credentialsData) throws Exception { From 6d899607547a7bb113046a86e6ec251310b193eb Mon Sep 17 00:00:00 2001 From: ShvaykaD Date: Wed, 11 May 2022 14:38:37 +0300 Subject: [PATCH 130/276] refactored mqtt claim gateway tests --- .../transport/mqtt/claim/MqttClaimDeviceTest.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/MqttClaimDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/MqttClaimDeviceTest.java index 2779fb84f5..79b50941c0 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/MqttClaimDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/MqttClaimDeviceTest.java @@ -16,8 +16,6 @@ package org.thingsboard.server.transport.mqtt.claim; import lombok.extern.slf4j.Slf4j; -import org.eclipse.paho.client.mqttv3.MqttAsyncClient; -import org.eclipse.paho.client.mqttv3.MqttMessage; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.ClaimRequest; @@ -37,6 +35,7 @@ import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.GATEWAY_CLAIM_TOPIC; @Slf4j @DaoSqlTest @@ -148,8 +147,8 @@ public class MqttClaimDeviceTest extends AbstractMqttIntegrationTest { assertEquals(claimResponse, ClaimResponse.CLAIMED); } - protected void validateGatewayClaimResponse(String deviceName, boolean emptyPayload, MqttAsyncClient client, byte[] failurePayloadBytes, byte[] payloadBytes) throws Exception { - client.publish(MqttTopics.GATEWAY_CLAIM_TOPIC, new MqttMessage(failurePayloadBytes)); + protected void validateGatewayClaimResponse(String deviceName, boolean emptyPayload, MqttTestClient client, byte[] failurePayloadBytes, byte[] payloadBytes) throws Exception { + client.publishAndWait(GATEWAY_CLAIM_TOPIC, failurePayloadBytes); Device savedDevice = doExecuteWithRetriesAndInterval( () -> doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class), @@ -170,7 +169,7 @@ public class MqttClaimDeviceTest extends AbstractMqttIntegrationTest { ClaimResponse claimResponse = doPostClaimAsync("/api/customer/device/" + deviceName + "/claim", claimRequest, ClaimResponse.class, status().isBadRequest()); assertEquals(claimResponse, ClaimResponse.FAILURE); - client.publish(MqttTopics.GATEWAY_CLAIM_TOPIC, new MqttMessage(payloadBytes)); + client.publishAndWait(GATEWAY_CLAIM_TOPIC, payloadBytes); ClaimResult claimResult = doExecuteWithRetriesAndInterval( () -> doPostClaimAsync("/api/customer/device/" + deviceName + "/claim", claimRequest, ClaimResult.class, status().isOk()), @@ -189,7 +188,8 @@ public class MqttClaimDeviceTest extends AbstractMqttIntegrationTest { } protected void processTestGatewayClaimingDevice(String deviceName, boolean emptyPayload) throws Exception { - MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(gatewayAccessToken); byte[] failurePayloadBytes; byte[] payloadBytes; String failurePayload; @@ -207,7 +207,8 @@ public class MqttClaimDeviceTest extends AbstractMqttIntegrationTest { } protected void processProtoTestGatewayClaimDevice(String deviceName, boolean emptyPayload) throws Exception { - MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(gatewayAccessToken); byte[] failurePayloadBytes; byte[] payloadBytes; if (emptyPayload) { From 086dae045ae11293214e707d08e45b3c8aeaad92 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Wed, 11 May 2022 14:45:07 +0300 Subject: [PATCH 131/276] refactoring: - Customer controller - preparation --- .../DefaultTbNotificationEntityService.java | 43 ++++++------- .../entitiy/TbNotificationEntityService.java | 20 +++--- .../entitiy/alarm/DefaultTbAlarmService.java | 3 +- .../entitiy/asset/DefaultTbAssetService.java | 4 +- .../customer/DefaultTbCustomerService.java | 63 +++++++++++++++++++ .../entitiy/customer/TbCustomerService.java | 26 ++++++++ 6 files changed, 123 insertions(+), 36 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java index b6ab974988..b5595ec552 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java @@ -22,17 +22,16 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.rule.engine.api.msg.DeviceCredentialsUpdateNotificationMsg; import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; -import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; @@ -70,10 +69,11 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS @Override public void notifyDeleteEntity(TenantId tenantId, I entityId, E entity, - CustomerId customerId, List relatedEdgeIds, - SecurityUser user, Object... additionalInfo) { - logEntityAction(tenantId, entityId, entity, customerId, ActionType.DELETED, user, additionalInfo); - sendDeleteNotificationMsg(tenantId, entityId, relatedEdgeIds); + CustomerId customerId, ActionType actionType, + List relatedEdgeIds, + SecurityUser user, boolean isBody, Object... additionalInfo) { + logEntityAction(tenantId, entityId, entity, customerId, actionType, user, additionalInfo); + sendDeleteNotificationMsg(tenantId, entityId, entity, relatedEdgeIds, isBody); } @Override @@ -126,7 +126,7 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS gatewayNotificationsService.onDeviceDeleted(device); tbClusterService.onDeviceDeleted(device, null); - notifyDeleteEntity(tenantId, deviceId, device, customerId, relatedEdgeIds, user, additionalInfo); + notifyDeleteEntity(tenantId, deviceId, device, customerId, ActionType.DELETED, relatedEdgeIds, user, false, additionalInfo); } @Override @@ -145,12 +145,10 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS } @Override - public void notifyCreateOrUpdateAsset(TenantId tenantId, AssetId assetId, CustomerId customerId, Asset asset, - ActionType actionType, SecurityUser user, Object... additionalInfo) { - logEntityAction(tenantId, assetId, asset, customerId, actionType, user, additionalInfo); - - if (actionType == ActionType.UPDATED) { - sendEntityNotificationMsg(asset.getTenantId(), asset.getId(), EdgeEventActionType.UPDATED); + public void notifyCreateOrUpdateEntity(TenantId tenantId, I entityId, E entity, CustomerId customerId, ActionType actionType, SecurityUser user, Object... additionalInfo) { + logEntityAction(tenantId, entityId, entity, customerId, actionType, user, additionalInfo); + if (actionType == ActionType.UPDATED) { + sendEntityNotificationMsg(tenantId, entityId, EdgeEventActionType.UPDATED); } } @@ -197,9 +195,10 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS } @Override - public void notifyDeleteAlarm(Alarm alarm, SecurityUser user, List relatedEdgeIds) { - logEntityAction(alarm.getTenantId(), alarm.getOriginator(), alarm, alarm.getCustomerId(), ActionType.ALARM_DELETE, user, null); - sendAlarmDeleteNotificationMsg(alarm, relatedEdgeIds); + public void notifyDeleteCustomer(Customer customer, SecurityUser user, List edgeIds) { + logEntityAction(customer.getTenantId(), customer.getId(), customer, customer.getId(), ActionType.DELETED, user, null); + sendDeleteNotificationMsg(customer.getTenantId(), customer.getId(), customer, edgeIds, false); + tbClusterService.broadcastEntityStateChangeEvent(customer.getTenantId(), customer.getId(), ComponentLifecycleEvent.DELETED); } private void logEntityAction(TenantId tenantId, I entityId, E entity, CustomerId customerId, @@ -228,17 +227,15 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS } } - private void sendDeleteNotificationMsg(TenantId tenantId, EntityId entityId, List edgeIds) { - sendDeleteNotificationMsg(tenantId, entityId, edgeIds, null); - } - - protected void sendAlarmDeleteNotificationMsg(Alarm alarm, List relatedEdgeIds) { + protected void sendDeleteNotificationMsg(TenantId tenantId, I entityId, E entity, List edgeIds, boolean isBody) { try { - sendDeleteNotificationMsg(alarm.getTenantId(), alarm.getId(), relatedEdgeIds, json.writeValueAsString(alarm)); + String body = isBody ? json.writeValueAsString(entity) : null; + sendDeleteNotificationMsg(tenantId, entityId, edgeIds, body); } catch (Exception e) { - log.warn("Failed to push delete alarm msg to core: {}", alarm, e); + log.warn("Failed to push delete " + entity.getClass().getName() + " msg to core: {}", entity, e); } } + private void sendDeleteNotificationMsg(TenantId tenantId, EntityId entityId, List edgeIds, String body) { if (edgeIds != null && !edgeIds.isEmpty()) { for (EdgeId edgeId : edgeIds) { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java index 181a1f41eb..ed416ca051 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java @@ -15,15 +15,14 @@ */ package org.thingsboard.server.service.entitiy; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEventActionType; -import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; @@ -41,9 +40,13 @@ public interface TbNotificationEntityService { ActionType actionType, SecurityUser user, Exception e, Object... additionalInfo); + void notifyCreateOrUpdateEntity(TenantId tenantId, I entityId, E entity, + CustomerId customerId, ActionType actionType, + SecurityUser user, Object... additionalInfo); + void notifyDeleteEntity(TenantId tenantId, I entityId, E entity, CustomerId customerId, - List relatedEdgeIds, SecurityUser user, - Object... additionalInfo); + ActionType actionType, List relatedEdgeIds, SecurityUser user, + boolean isBody, Object... additionalInfo); void notifyAssignOrUnassignEntityToCustomer(TenantId tenantId, I entityId, CustomerId customerId, E entity, @@ -74,13 +77,10 @@ public interface TbNotificationEntityService { void notifyAssignDeviceToTenant(TenantId tenantId, TenantId newTenantId, DeviceId deviceId, CustomerId customerId, Device device, Tenant tenant, SecurityUser user, Object... additionalInfo); - void notifyCreateOrUpdateAsset(TenantId tenantId, AssetId assetId, CustomerId customerId, Asset asset, - ActionType actionType, SecurityUser user, Object... additionalInfo); - void notifyEdge(TenantId tenantId, EdgeId edgeId, CustomerId customerId, Edge edge, ActionType actionType, SecurityUser user, Object... additionalInfo); - void notifyCreateOrUpdateAlarm(Alarm alarm, - ActionType actionType, SecurityUser user, Object... additionalInfo); + void notifyCreateOrUpdateAlarm(Alarm alarm, ActionType actionType, SecurityUser user, Object... additionalInfo); + - void notifyDeleteAlarm(Alarm alarm, SecurityUser user, List relatedEdgeIds); + void notifyDeleteCustomer(Customer customer, SecurityUser user, List relatedEdgeIds); } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java index f0ac516393..22196c4984 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java @@ -79,7 +79,8 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb @Override public Boolean delete(Alarm alarm, SecurityUser user) throws ThingsboardException { List relatedEdgeIds = findRelatedEdgeIds(alarm.getTenantId(), alarm.getOriginator()); - notificationEntityService.notifyDeleteAlarm(alarm, user, relatedEdgeIds); + notificationEntityService.notifyDeleteEntity(alarm.getTenantId(), alarm.getOriginator(), alarm, alarm.getCustomerId(), + ActionType.ALARM_DELETE, relatedEdgeIds, user, true); return alarmService.deleteAlarm(alarm.getTenantId(), alarm.getId()).isSuccessful(); } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java index 84b9c707d0..3f4ce83318 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java @@ -45,7 +45,7 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb TenantId tenantId = asset.getTenantId(); try { Asset savedAsset = checkNotNull(assetService.saveAsset(asset)); - notificationEntityService.notifyCreateOrUpdateAsset(tenantId, savedAsset.getId(), savedAsset.getCustomerId(), asset, actionType, user); + notificationEntityService.notifyCreateOrUpdateEntity(tenantId, savedAsset.getId(), asset, savedAsset.getCustomerId(), actionType, user); return savedAsset; } catch (Exception e) { notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.ASSET), asset, null, actionType, user, e); @@ -60,7 +60,7 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb try { List relatedEdgeIds = findRelatedEdgeIds(tenantId, assetId); assetService.deleteAsset(tenantId, assetId); - notificationEntityService.notifyDeleteEntity(tenantId, assetId, asset, asset.getCustomerId(), relatedEdgeIds, user, asset.toString()); + notificationEntityService.notifyDeleteEntity(tenantId, assetId, asset, asset.getCustomerId(), ActionType.DELETED, relatedEdgeIds, user, false, asset.toString()); return removeAlarmsByEntityId(tenantId, assetId); } catch (Exception e) { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java new file mode 100644 index 0000000000..452c71b51a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.entitiy.customer; + +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.AbstractTbEntityService; +import org.thingsboard.server.service.security.model.SecurityUser; + +import java.util.List; + +@Service +@TbCoreComponent +@AllArgsConstructor +public class DefaultTbCustomerService extends AbstractTbEntityService implements TbCustomerService { + + @Override + public Customer save(Customer customer, SecurityUser user) throws ThingsboardException { + ActionType actionType = customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED; + TenantId tenantId = customer.getTenantId(); + try { + Customer savedCustomer = checkNotNull(customerService.saveCustomer(customer)); + notificationEntityService.notifyCreateOrUpdateEntity(tenantId, savedCustomer.getId(), savedCustomer, savedCustomer.getId(), actionType, user); + return savedCustomer; + } catch (Exception e) { + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.CUSTOMER), customer, null, actionType, user, e); + throw handleException(e); + } + } + + @Override + public void delete(Customer customer, SecurityUser user) throws ThingsboardException { + TenantId tenantId = customer.getTenantId(); + try { + List relatedEdgeIds = findRelatedEdgeIds(tenantId, customer.getId()); + customerService.deleteCustomer(tenantId, customer.getId()); + notificationEntityService.notifyDeleteCustomer(customer, user, relatedEdgeIds); + } catch (Exception e) { + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.CUSTOMER), null, null, ActionType.DELETED, user, e); + throw handleException(e); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java new file mode 100644 index 0000000000..36c5a0c0ed --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.entitiy.customer; + +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.service.security.model.SecurityUser; + +public interface TbCustomerService { + Customer save (Customer customer, SecurityUser user) throws ThingsboardException; + + void delete (Customer customer, SecurityUser user) throws ThingsboardException; +} From c44727851d1c9bea0676cef0ec663d61fc97b9e2 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 11 May 2022 15:58:15 +0300 Subject: [PATCH 132/276] UI: Add map widget settings forms --- .../system/widget_bundles/input_widgets.json | 21 +- .../data/json/system/widget_bundles/maps.json | 77 +- ui-ngx/extra-webpack.config.js | 23 +- ui-ngx/package.json | 2 + .../app/core/api/entity-data-subscription.ts | 6 +- ui-ngx/src/app/core/utils.ts | 4 + .../import-dialog-csv.component.html | 2 +- .../import-dialog.component.html | 2 +- .../widget/data-key-config.component.html | 1 - .../widget/data-key-config.component.ts | 3 +- .../home/components/widget/lib/maps/circle.ts | 25 +- .../widget/lib/maps/common-maps-utils.ts | 7 +- .../components/widget/lib/maps/leaflet-map.ts | 110 ++- .../components/widget/lib/maps/map-models.ts | 878 ++++++++++++------ .../components/widget/lib/maps/map-widget2.ts | 70 +- .../components/widget/lib/maps/maps-utils.ts | 12 +- .../components/widget/lib/maps/markers.ts | 37 +- .../components/widget/lib/maps/polygon.ts | 47 +- .../components/widget/lib/maps/polyline.ts | 24 +- .../widget/lib/maps/providers/google-map.ts | 7 +- .../widget/lib/maps/providers/here-map.ts | 7 +- .../widget/lib/maps/providers/image-map.ts | 11 +- .../lib/maps/providers/openstreet-map.ts | 7 +- .../widget/lib/maps/providers/tencent-map.ts | 7 +- .../map/circle-settings.component.html | 199 ++++ .../settings/map/circle-settings.component.ts | 243 +++++ .../map/common-map-settings.component.html | 93 ++ .../map/common-map-settings.component.ts | 178 ++++ ...atasources-key-autocomplete.component.html | 39 + .../datasources-key-autocomplete.component.ts | 152 +++ ...oogle-map-provider-settings.component.html | 31 + .../google-map-provider-settings.component.ts | 122 +++ .../here-map-provider-settings.component.html | 40 + .../here-map-provider-settings.component.ts | 125 +++ ...image-map-provider-settings.component.html | 71 ++ .../image-map-provider-settings.component.ts | 253 +++++ .../map/map-editor-settings.component.html | 52 ++ .../map/map-editor-settings.component.ts | 141 +++ .../map/map-provider-settings.component.html | 51 + .../map/map-provider-settings.component.ts | 204 ++++ .../settings/map/map-settings.component.html | 53 ++ .../settings/map/map-settings.component.ts | 217 +++++ .../map/map-widget-settings.component.html | 24 + .../map/map-widget-settings.component.ts | 63 ++ .../marker-clustering-settings.component.html | 70 ++ .../marker-clustering-settings.component.ts | 141 +++ .../map/markers-settings.component.html | 197 ++++ .../map/markers-settings.component.ts | 264 ++++++ ...treet-map-provider-settings.component.html | 46 + ...nstreet-map-provider-settings.component.ts | 139 +++ .../map/polygon-settings.component.html | 199 ++++ .../map/polygon-settings.component.ts | 243 +++++ .../map/route-map-settings.component.html | 32 + .../map/route-map-settings.component.ts | 115 +++ .../route-map-widget-settings.component.html | 24 + .../route-map-widget-settings.component.ts | 63 ++ ...ncent-map-provider-settings.component.html | 31 + ...tencent-map-provider-settings.component.ts | 122 +++ ...p-animation-common-settings.component.html | 94 ++ ...rip-animation-common-settings.component.ts | 173 ++++ ...p-animation-marker-settings.component.html | 97 ++ ...rip-animation-marker-settings.component.ts | 175 ++++ ...rip-animation-path-settings.component.html | 116 +++ .../trip-animation-path-settings.component.ts | 187 ++++ ...ip-animation-point-settings.component.html | 94 ++ ...trip-animation-point-settings.component.ts | 163 ++++ ...p-animation-widget-settings.component.html | 45 + ...rip-animation-widget-settings.component.ts | 101 ++ .../lib/settings/widget-settings.module.ts | 101 +- .../widget/lib/settings/widget-settings.scss | 2 +- .../trip-animation.component.ts | 33 +- .../widget/widget-config.component.html | 1 - .../widget/widget-config.component.ts | 3 +- .../widget/widget-settings.component.ts | 54 +- .../resource/resources-library.component.html | 2 +- .../ota-update/ota-update.component.html | 2 +- .../components/color-input.component.html | 2 +- .../app/shared/components/css.component.html | 2 +- .../components/file-input.component.html | 12 +- .../components/file-input.component.scss | 33 +- .../app/shared/components/html.component.html | 4 +- .../app/shared/components/html.component.scss | 8 +- .../app/shared/components/html.component.ts | 2 + .../components/image-input.component.html | 43 +- .../components/image-input.component.scss | 88 +- .../components/image-input.component.ts | 6 +- .../shared/components/js-func.component.html | 6 +- .../shared/components/js-func.component.scss | 4 - .../shared/components/js-func.component.ts | 2 + .../components/json-content.component.html | 2 +- .../json-form/json-form-component.models.ts | 1 + .../components/markdown-editor.component.html | 2 +- .../mat-chip-draggable.directive.ts | 16 + .../multiple-image-input.component.html | 69 ++ .../multiple-image-input.component.scss | 169 ++++ .../multiple-image-input.component.ts | 210 +++++ ui-ngx/src/app/shared/models/widget.models.ts | 18 +- ui-ngx/src/app/shared/shared.module.ts | 6 + .../assets/locale/locale.constant-en_US.json | 193 +++- ui-ngx/src/styles.scss | 5 + ui-ngx/yarn.lock | 12 + 101 files changed, 7168 insertions(+), 617 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/circle-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/circle-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/common-map-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/common-map-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/datasources-key-autocomplete.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/datasources-key-autocomplete.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/google-map-provider-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/google-map-provider-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/here-map-provider-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/here-map-provider-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/image-map-provider-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/image-map-provider-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-editor-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-editor-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-provider-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-provider-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/markers-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/markers-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/openstreet-map-provider-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/openstreet-map-provider-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/polygon-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/polygon-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-widget-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/tencent-map-provider-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/tencent-map-provider-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-common-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-common-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-marker-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-marker-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-path-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-path-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-point-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-point-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-widget-settings.component.ts create mode 100644 ui-ngx/src/app/shared/components/multiple-image-input.component.html create mode 100644 ui-ngx/src/app/shared/components/multiple-image-input.component.scss create mode 100644 ui-ngx/src/app/shared/components/multiple-image-input.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/input_widgets.json b/application/src/main/data/json/system/widget_bundles/input_widgets.json index c5c3773e56..3c9a9fe718 100644 --- a/application/src/main/data/json/system/widget_bundles/input_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/input_widgets.json @@ -18,9 +18,10 @@ "resources": [], "templateHtml": "", "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('openstreet-map', false, self.ctx, null, true);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('openstreet-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('openstreet-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('openstreet-map', false, self.ctx, null, true);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-map-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.7867521952070078,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.7040053227577256,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}

Delete\",\"markerImageSize\":34,\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"color\":\"#fe7569\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"showTooltip\":true,\"autocloseTooltip\":true,\"defaultCenterPosition\":\"0,0\",\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"showTooltipAction\":\"click\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"zoomOnClick\":true,\"showCoverageOnHover\":true,\"animate\":true,\"maxClusterRadius\":80,\"removeOutsideVisibleBounds\":true,\"defaultZoomLevel\":5,\"provider\":\"openstreet-map\",\"draggableMarker\":true,\"editablePolygon\":true,\"mapPageSize\":16384,\"showPolygon\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${coordinates|ts:7}

Delete\"},\"title\":\"Markers Placement - OpenStreetMap\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id;\\n });\\n\\nwidgetContext.map.setMarkerLocation(entityDatasource[0], null, null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"54c293c4-9ca6-e34f-dc6a-0271944c1c66\"},{\"name\":\"delete_polygon\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.savePolygonLocation(entityDatasource[0], null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"6beb7bed-dfd8-388d-b60c-82988ab52f06\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}" } }, @@ -75,9 +76,10 @@ "resources": [], "templateHtml": "", "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('image-map', false, self.ctx, null, true);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('image-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('image-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('image-map', false, self.ctx, null, true);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-map-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}

Delete\",\"markerImageSize\":34,\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"color\":\"#fe7569\",\"mapImageUrl\":\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiCiAgIHhtbG5zOmlua3NjYXBlPSJodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy9uYW1lc3BhY2VzL2lua3NjYXBlIgogICB3aWR0aD0iMTEzNC41MTgzIgogICBoZWlnaHQ9Ijc2Mi43ODI0MSIKICAgaWQ9InN2ZzIiCiAgIHZlcnNpb249IjEuMSIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMC40OC41IHIxMDA0MCIKICAgc29kaXBvZGk6ZG9jbmFtZT0id2ljaGl0YW1hcC1ub2xpYi5zdmciPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0IiAvPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBpZD0iYmFzZSIKICAgICBwYWdlY29sb3I9IiNmZmZmZmYiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgYm9yZGVyb3BhY2l0eT0iMS4wIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTp6b29tPSIwLjM1IgogICAgIGlua3NjYXBlOmN4PSI4OS45MDc4NTciCiAgICAgaW5rc2NhcGU6Y3k9IjQ1My43ODI0MSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ibGF5ZXIxIgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzNjYiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iNzIxIgogICAgIGlua3NjYXBlOndpbmRvdy14PSItNCIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iLTQiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBpbmtzY2FwZTpvYmplY3QtcGF0aHM9InRydWUiCiAgICAgaW5rc2NhcGU6c25hcC1nbG9iYWw9ImZhbHNlIgogICAgIHNob3dndWlkZXM9InRydWUiCiAgICAgaW5rc2NhcGU6Z3VpZGUtYmJveD0idHJ1ZSIKICAgICBmaXQtbWFyZ2luLXRvcD0iMCIKICAgICBmaXQtbWFyZ2luLWxlZnQ9IjAiCiAgICAgZml0LW1hcmdpbi1yaWdodD0iMCIKICAgICBmaXQtbWFyZ2luLWJvdHRvbT0iMCIgLz4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE3Ij4KICAgIDxyZGY6UkRGPgogICAgICA8Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+CiAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+CiAgICAgICAgPGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPgogICAgICAgIDxkYzp0aXRsZT48L2RjOnRpdGxlPgogICAgICA8L2NjOldvcms+CiAgICA8L3JkZjpSREY+CiAgPC9tZXRhZGF0YT4KICA8ZwogICAgIGlua3NjYXBlOmxhYmVsPSJMYXllciAxIgogICAgIGlua3NjYXBlOmdyb3VwbW9kZT0ibGF5ZXIiCiAgICAgaWQ9ImxheWVyMSIKICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjcuMDcxNDI4LC0zMDcuOTAyOTkpIj4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM3ODciCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzY0ZTU5O3N0cm9rZS13aWR0aDoyLjk5OTk5OTc2O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmUiCiAgICAgICBkPSJtIDkwNi4wMzMxNSw3MDYuMTMzNjcgMy40MjkyLDE3Ljc5NTUyIE0gMjguNTcxNDI4LDc2NS4wNTA2NyBjIDE1MC40MzUyMDIsNi44MzM0MiAxNDYuMzkyMzIyLC0yNi4zMzQxNSAxNjYuNDM0NTQyLC0yOS4zMjAwOSAzNi4xNDM3NSwtNS4zODQ3NiAxMTQuMjg2NzYsLTYuNTI1NCAxNDguMzI1MDgsLTguNjIzNTQgNDMuMzc4MDgsLTIuNjczODUgMTQxLjc2MjIxLC0xMS4yMzA5OSAxODguODU1NzgsLTE5LjgzNDE4IDM5LjgxMTM4LC03LjI3Mjg0IDIyMS4zNjk5MSwtMC44NjIzNSAzMTkuMDcxNDEsLTAuODYyMzUgNzAuODI3MzUsMCAxNDYuOTE4NjcsLTEuNzI0NyAyMTguMTc1ODYsLTEuNzI0NyAtMzEuNjE5NywwIDExNy44NTUyLC0yLjU4NzA3IDg2LjIzNTUsLTIuNTg3MDcgbSAtMjUuMDkwNywtNjguMTI2MDYgYyAtNTIuNzk5NiwzNC43ODQ4NCAtNjUuODk1MSw1MS43NDg2NSAtOTUuNjM5LDgxLjQ5MjU4IC0yNC45MzEzLDI0LjkzMTI3IC0xNDAuMzk2NTMsLTE5LjEzOTIgLTE3OC45Mzg3MSwzNi42NTAwNyAtMTIuMjgxNCwxNy43NzcxNSAtNDcuMDAyNTcsNDYuNTQ2NTMgLTY1LjEwNzgzLDU5LjA3MTMzIC0yMC4xMDUsMTMuOTA4MTggLTU2LjAzNjcyLDQ0Ljk1NjY0IC02Ny43Njg4NSw3My4wNzgyNyAtNC44MDE0NywxMS41MDkwMiAtMTMuMzgwNDYsMzUuOTkyOTggLTIzLjQ0OTQ5LDQ2LjA2MjAxIC0xMC40OTY5OSwxMC40OTY5OSAtMzguMzc3MzMsNi4zODU2OSAtNDQuMDIzNDUsMTcuNjQ3NjQgLTE5LjAwNTAyLDM3LjkwODEyIC0yNS40NjUzLDEwMC45MjM1MiAtNjcuNjE3ODksMTAyLjA1MTAyIG0gMTkuMjgxNTEsLTYyNC4wMTQ2NCBjIDM0LjY1OTM0LC0xLjg3MzgyIDg0LjAyNzMzLDcuMzkxMzEgMTA5LjkwMDcxLC00LjI4NTQ1IDEzLjI4MTcyLC01Ljk5NDA4IDQxLjQwNzIxLC0yLjQ2MTM1IDY2LjgyODY2LC0yLjMyMDQ2IDM1LjMyMjM4LDAuMTk1NzggNjQuMzgyNDksMC42MzQ3NyAxMDEuOTE2Nyw1LjAyMzIgMjUuMDMwMzYsMi45MjY1IDQ0LjY2MjczLDM0LjI4NzIyIDU4LjUyNjk4LDUwLjY0MzkgMTcuMDk4NzgsMjAuMTcyNjggNjIuNzYzODYsLTEuNzE0NjcgNjYuMzA1NjYsMzIuMTM0MzMgNS4xMDI3LDQ4Ljc2NTg3IC02LjMyODQsNzguNjM3MjUgNi4xNDExLDk3LjM0MTUgMTkuOTY5MiwyOS45NTM3OSA1MC40ODY0LDE3Ljg1NTc5IDQ0LjYxOTMsODMuOTcxMTkgTSA1ODkuMTAyMjcsMzA5LjcyNzE1IGMgNC42NDM0NiwyMy43MjkyMyAxNS4wNjkwNCw3Mi43NzU3NSAxOS4wNjEyOCwxMzAuNjQyODggMC44NzIwNiwxMi42NDA0OCA1LjQ0NzE4LDI0Ljk5MjUzIDQuMjIyMzEsNDUuMjc3NTcgLTIuNTE3MjEsNDEuNjg3NSAtMTUuNzE3MDYsNDMuNjc3MjcgLTE1LjA5MTIyLDYwLjM2NDg2IDEuNDMxOTUsMzguMTgyMjQgMzAuNjEzNjEsOTMuODM3MTkgMzAuNjEzNjEsMTM5LjcwMTU0IDAsMjQuMTgwOCAtMi42Njk2NCwxMTUuMzkwNDUgNy4zMzAwMSwxMzUuMzg5NzYgMC4xNTkxMSwwLjMxODIxIDEwLjA2NDc2LDM1Ljg4MzMyIDEwLjc3OTQ1LDQ5LjE1NDI0IDAuOTQzNzgsMTcuNTI0NjkgLTI0LjQ3OCwzOS40NzAwOCAtMjguMDI2NTUsNDYuNTY3MTYgLTUuNDc3NywxMC45NTUzOSAtMzYuOTczMjQsMTAuODgxOTcgLTQwLjA5OTUsMjQuMTQ1OTUgLTMuODY4ODQsMTYuNDE0NTEgLTMuODY2Myw0My43OTczNSA0LjA0NjQ3LDU5LjQ0MTI5IG0gOTcuMzM3MzQsLTY5MS4wMDk0MSBjIC01LjAxMzMyLDM1LjUxNTk1IC00My42NTkwMSwxMS4zMTY1MiAtNTguNTM4NjEsMjMuNzgxMzEgLTIxLjMzMDE5LDE3Ljg2ODUyIC02Mi40OTk2NCwzMS40MzIxMiAtNzAuMTI0MzcsMzUuMzY3MDggLTM1LjA4NzYzLDE4LjEwNzkzIC0xMTAuNDcyMTUsLTE1LjE0MTk2IC0xMjUuNjE0MSw0LjI2ODQzIC0xNS45NTA2MywyMC40NDcwMyAtMC4wNzM1LDYxLjQ2NjQ4IC05LjE0NjY2LDg0LjE0OTI0IC02LjAzNTcsMTUuMDg5MjYgLTE4Ljg3NjcsMjMuMDE3MzQgLTI3LjQzOTk3LDMyLjkyNzk4IC0xOS43NDgyOSwyMi44NTU1NSAtNjkuOTc0MjgsNjkuODI0MTkgLTg0Ljc1OTA0LDEwMC4wMDM0NiAtNy40OTc0MSwxNS4zMDQwNCAtMy4yODQyNiw0NC40MjA0MSAtMy40NzA1Myw2My4zNDI4NCAtMC4xMjc5MywxMi45OTQxNCAtMC44MTAxNSwyMy4xMDM4NSAyLjQwMzQzLDI4LjI3NjE4IDQuOTYxNTgsNy45ODU4MSAyMy43MjA1LDI4LjExMjA3IDI0LjIzODY1LDUwLjYxMTQ5IDAuMjk0MTEsMTIuNzcxNDYgMC4wMTMzLDc4LjU5MTAxIDMuMDQ4ODgsODcuNjU1NDkgMi4zMTI1Niw2LjkwNTQ2IDQuMjIwMDQsMjYuNTY0OTcgMTAuMjEzNzcsMzYuNTg2NjIgMTEuMzU0MDEsMTguOTg0MTUgNC4zODczNyw0MC4xNTY2MiAyNy44OTczLDUzLjUwNzk1IDE5LjA1MDEyLDEwLjgxODU5IDQ2Ljg3NzgxLDEyLjIxODYyIDgxLjkyNjE4LDE0LjQ2MDU0IDMzLjcwMzQ1LDIuMTU1ODkgNjEuNTEyMTcsLTEuNDMwMzUgNzYuOTIwNzcsNi4xNDExIDExLjU4NTA4LDUuNjkyNjYgOC41ODE1MSwxNy45MzM0NCAxNC4yOTU0MSwyOS4zNjEyMyA1LjY0MDQyLDExLjI4MDg1IDMxLjUwMjYzLDExLjE1NjI3IDQxLjgwNDA5LDQzLjQ1NDg3IDcuNjA1OSwyMy44NDcxIDMuMDg1OTMsNDQuMTU2OSA2LjcwNzU1LDY1Ljg4NjYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2Nzc3NzY2Njc3Nzc3NzY2Nzc3Nzc3NjY3Nzc3Njc3NzY2Nzc3Nzc3Nzc3Nzc3Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Im0gNDMuMjc3ODgxLDUxNy45NDY3OSBjIDAsMCAyMzAuODQ4Mjg5LC0zLjYzODA1IDI1MC4wMDg2MzksLTMuNjU4NjcgNy40ODIyMiwtMC4wMDggOC42MTk1NCw1LjE1MTk0IDE0LjAyMDksMTEuNDU4NjkgMjQuNTk2MDgsMjguNzE4OTMgOTMuOTA5NjYsMTEyLjkzNTg1IDkzLjkwOTY2LDExMi45MzU4NSIKICAgICAgIGlkPSJwYXRoMzc4OSIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJtIDM1Ljk2MDU1NSw1NzcuNzA0OTQgYyAwLDAgMTY1LjUyNDU2NSwtMS42ODQ1NCAyNDguNzc5NTY1LC0xLjY4NDU0IDQuOTQ3NDksMCA3LjcyOTkzLC0yLjg4MzMgMTAuNTM3NzEsLTUuNzI5NzcgOS42NjEwNywtOS43OTQxNiAyNS42MzE5OSwtMjguNTg5OTUgMjUuNjMxOTksLTI4LjU4OTk1IgogICAgICAgaWQ9InBhdGgzNzkxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzguMzk5NjYzLDY0MS43MzE1NSA0MzEuNzA1OTMsNjM3LjQ2MzExIgogICAgICAgaWQ9InBhdGgzNzk1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzkuMDA5NDQyLDcwNC41Mzg1OSA1MjMuMTcyNTMsNjk3LjgzMTA0IgogICAgICAgaWQ9InBhdGgzNzk3IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzAzLjk1NzYyLDY4Mi41ODY2MSAxNDYuNzk1NDIsMS44MjkzMyBjIDEwLjUzNDAzLDAuMTMxMjcgMTQuMzQzNzQsLTIuNjM3MzkgMjUuNDg3MTUsLTYuMzcyOCAxMC40MTIxMiwtMy40OTAyNyAzMS40MjQxNSwtMi42OTg5NiA0MS4zODUzOCwtMi43NzM4NSBsIDQwNS41NjA3OSwtMy4wNDg5IgogICAgICAgaWQ9InBhdGgzNzk5IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgaWQ9InBhdGgzODA0IgogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDQyNi4yMTc5NCwzMTQuODkwOTggYyAyLjA2NzU0LDkuMDUyNzMgMS44NDE3Nyw1MS43Mjc3NyA2LjUwNzk0LDc0LjgzNDY2IDEuNjc0NzUsOC4yOTMzNiA4LjY3NTA4LDE0LjA2NTk4IDEwLjA1NTQxLDE0Ljg1ODYyIDQuOTAxNDcsMi44MTQ2MyAxMC44MTQ3OSw4LjE0OTgyIDEzLjA0NTc5LDE2LjA4ODMxIDYuNzU3NzksMjQuMDQ1OTEgMC44Nzk3Miw2OC40NTIxMiAwLjg3OTcyLDExMC42ODkzIDAsNi4wOTc4MiAxLjY2MDEsMzAuMTQ2NiAtMi4xNTU4OCwzMy45NjI1OSAtMi41NDA4NSwyLjU0MDgzIC0wLjI4MTYzLDEyLjk5MDY5IC0zLjQzNjc1LDE2LjE0Mzc3IGwgLTkuODQ5NDQsOS44NDMxMSBjIC0xMC4zNjcxNSwxMC4zNjA0NyAtMTEuNTkwMTcsNi41MjYxNCAtMTcuNzM4NDgsMTguODIyNzYgLTMuNTY3NzIsNy4xMzU0MyA1LjQwMjM1LDIwLjY3MjEgNy4zNTQzMiwyNC41NzYwMiAxLjkzMjE0LDMuODY0MyAtMS44NDIxNiw0Ljc3NzczIC0xLjc5MjM1LDcuNDQ2MjYgMC4yNTI4NiwxMy41NDQ4MyAyLjI5NzUsMzczLjkyNzEyIDIuMjk3NSwzNzMuOTI3MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2NS4yNDAyMiw1MTkuNzc2MTIgNC4xMTU5OSw1MDIuMTUxNTgiCiAgICAgICBpZD0icGF0aDM4MDYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMTYuNTMxNjUsNTA0LjE4Njk5IDMuODgwNTksMzEwLjk2NDM2IgogICAgICAgaWQ9InBhdGgzODMxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM4ODkiCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzE3LjY3NzYsNTc2LjQ4NTM5IDEzMC4xODc0MiwxLjUyNDQ0IGMgNC41MTA3OSwzLjI0MTY5IDIwLjM0NDcxLDcuOTY4NTMgMjcuNzQ0ODYsNC4yNjg0NCAzLjE1NTQ2LC0xLjU3NzcyIDkuNDE5LC01LjM4ODE3IDE0LjAyNDg5LC0zLjk2MzU1IDQuMjY2OTgsMS4zMTk4MSA2LjAxNjg5LDMuMTE2MzIgMTAuMzY2MjEsMy4wNDg4OSAxMC4zMDQwMywtMC4xNTk3NSAyMC4yMTE3LDAuMzg3NDEgMzAuNDg4ODYsMC4zMDQ4OSAxNzcuODkwOCwtMS40MjgyNyAzNTYuNTkwMzUsLTIuMTMyNDcgNTM0Ljc3NDU2LC0zLjA0ODg4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDc1LjMwNTAxLDU4Mi44ODgwNSBjIC0zLjQ0NDE4LDExLjM1MDY2IC0yLjEwMzQzLDEyLjQzMzczIDMuNjU4NjUsMjEuMDM3MzEgMy43OTQ0NSw1LjY2NTY0IDUwLjg2MjYxLDEzLjAzODQ1IDQxLjQ2NDg1LDI3LjEzNTA5IC0xMC41MzY5NywxNS44MDU0NyAtMjIuODk3NDUsLTUuNDc3NzIgLTMzLjg0MjYzLC0xLjgyOTMzIC01LjQ1MjM2LDEuODE3NDUgLTcuMzQ5MDEsNS40NTYzMSAtMy42NTg2Niw5LjE0NjY1IDIuODA2ODMsMi44MDY4NCA0LjA0OCwxLjgwMzk2IDYuNTIwMzQsNS4xMDA0MSIKICAgICAgIGlkPSJwYXRoMzkxMCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjAxMDgyLDYzNi44NTMzMyBjIDguMzE4OTksMTMuMTEwMTYgMTguODQ2MjEsMTQuNjM0NjUgMzUuNjcxOTYsMTQuNjM0NjUgMi45Mzg2NSwwIDcuODY5OTgsLTAuOTMzNzEgMTAuNjcxMTEsMCAxMS4zNTkxNywzLjc4NjM5IDI3LjE5Mzk4LDEwLjI3NTc3IDM2LjIwMTkzLDIxLjEyOTQ4IDguMjgwMDIsOS45NzY2MSAxMC4yNTI3OCwyMy44ODMwOCA3LjcwMjAyLDM3LjEwNDI0IC02LjE2OTg5LDMxLjk3OTk4IC0xNi43MTQzMSw1Ni45ODg1MyAtMTkuMDQzNTUsODYuNTY5MDUgLTEuMzQ3OTgsMTcuMTE4OCA0LjUwOTU3LDIyLjUzNTIyIDExLjA3MTQzLDMzLjkyODU3IDEwLjY3MDIzLDE4LjUyNjcyIDguNzI0NTMsMTQuMTk5NTUgOC41NzE0MywzNC4yODU3MiAtMC4xMzk2MywxOC4zMTk0NCAwLDYwLjI2Mzg1IDAsODAuNzE0MjkiCiAgICAgICBpZD0icGF0aDM5MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUyOC41MDgwNiw2NTguOTU3NzYgYyAtMTAuNjgxMjMsMC45MDQ1NCAtNy4xMDgwNCwtNS42MDI1NSAtMTAuODIzNTQsLTguMDc5NTYgLTQuNzg0NTQsLTMuMTg5NjkgLTEyLjIyNzA0LC0xLjI1MTA0IC0xNi43Njg4OCwtNS43OTI4OCAtMC42NjYxMiwtMC42NjYxMiAtOC44MDk2OSwtNC4xMDg3NyAtMTAuMTc0NDcsLTIuNzQzOTkgLTguMzY0NTksOC4zNjQ1OSAtMy4wNDg4OCwyMC41NTE4OCAtMy4wNDg4OCwzMy41Mzc3NCBsIDMuMDIyLDMzOS42OTc0MyIKICAgICAgIGlkPSJwYXRoMzkxNCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA1MTcuOTg5NDEsNjUxLjAzMDY1IGMgLTAuMjIxNzEsLTIuNzAxODQgMS45MDM0NiwtNS41NjIxMyAzLjM1Mzc3LC03LjAxMjQ1IDEuNzk5NDMsLTEuNzk5NDIgNi45MjI5NCwxLjAwNDE5IDguODQxNzgsLTAuOTE0NjYgMC4yODc2NSwtMC4yODc2NiAwLjg0MzI5LC0xMS4xNjQxIDAuMjI4NjYsLTEzLjU2NzUzIC0yLjA2NDgzLC04LjA3NDE2IC0yLjA1ODAxLC0yOC42NTY1OCAtMi4wNTgwMSwtMzguNzIwODYgbCAwLC03My4xNzMyNiIKICAgICAgIGlkPSJwYXRoMzkxNiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNTI4LjY2MDUsNjc1LjQyMTczIC0wLjQ1NzMzLC0zMS41NTU5NiIKICAgICAgIGlkPSJwYXRoMzk3NCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc2Ni4zMTYyNSw1NzkuNjQ0MzEgMC40MzExOCwxMy43OTc2OCBjIDMuMTM2NDMsNC42NjkxNSAzLjAxODI0LDkuNjAwNjggMy4wMTgyNCwxNi4zODQ3NSBsIDAsMTU3LjM3OTgxIgogICAgICAgaWQ9InBhdGgzOTgyIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMTEyMi45MDAxLDc2NS45MTMwMyBjIC0yMDIuMzA2NjksNC42OTA1IC00MDMuNzQ0MDUsLTEuMTEzODEgLTYwNS45NTQ1NCwzLjM1MzkgLTEwLjg2MzYyLDAuMjQwMDIgLTMuMzYxNDcsLTguNTg2MyAtMjguNTM2OCwtOC41ODYzIgogICAgICAgaWQ9InBhdGgzOTg0IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA4NjAuMDA4MDUsNzM3LjA2NjUxIGMgMCwwIC05Ny40NDc1LDAuODU4MDYgLTE0Ny41Njg5MiwwLjg1ODA2IC01LjI2ODYxLDAgLTQuNTE1NDYsLTguMzI5ODYgLTcuMzAwODksLTguMzI5ODYgLTMuOTc0MzUsMCAtOC42MjkyNSwwLjAyMDEgLTEwLjUwOTQ4LDAuMDM1OSAtMi4zMzQ3NywwLjAxOTcgLTEuODEwOTQsOC4zNjU5NyAtNC4xNDU4LDguMzY2OTIgLTQ2LjE2ODk5LDAuMDE4OCAtMTY3LjQwNzY3LC0xLjMwNzk5IC0xNzUuMDUyNjMsLTEuMzA3OTkgLTQuNDI5NTUsMCAtOC41NzYyNywtNi40Mzk3MiAtMTMuMTMxOTgsLTYuNDM5NzIgLTEuMzYxMTUsMCAtNi4yMzg3MywwIC0xNC4zOTQ2NywwIgogICAgICAgaWQ9InBhdGgzOTg2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJNIDY3NS4wMDcwMyw4MzEuMTc0MDIgNjc0LjM5NzI1LDMwOS40MDI5OSIKICAgICAgIGlkPSJwYXRoMzk4OCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc5OS40MDE1NywzMTMuMDYxNjUgMS4yMTk1NSw0OTUuODY2NTMiCiAgICAgICBpZD0icGF0aDM5OTAiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA3MzYuNTk0NTIsMzEyLjQ1MTg4IC0xLjIxOTU1LDcxNi40ODgyMiIKICAgICAgIGlkPSJwYXRoMzk5MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUzMC4wMzA5NCw2NDMuNDU4NTkgMzkyLjM3MTU5LC0zLjAxODI1IgogICAgICAgaWQ9InBhdGg0MDQ4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gODU5LjQ1MDYsMzE0LjkwMTI4IDEuMjkzNTQsNTA3Ljk4MDU4IgogICAgICAgaWQ9InBhdGg0MDUwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjAuOTk5OTk5OTRweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gOTIxLjU0MDE3LDMxMC41ODk0OSAxLjcyNDcxLDUzMS43NTIyNyIKICAgICAgIGlkPSJwYXRoNDA1MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDczNi4yODk2Myw0NTMuMzEwNCAxODUuNjc3MTUsLTAuMzA0ODkiCiAgICAgICBpZD0icGF0aDQxODciCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMDYwLjgxMDUsNTE0Ljk2NzY3IGMgMCwwIC0zNjMuMjgxMjYsLTUuNjI2MTggLTU0NC42NTA0MiwyLjUyMTc4IC00LjE3Nzc2LDAuMTg3NjkgLTEyLjUwMDQ0LDEuMDY3MTEgLTEyLjUwMDQ0LDEuMDY3MTEgLTEuNTcwOTUsMC4xMzQxIC0yLjAwMDkzLC0yLjMyNDk1IC0yLjU5MTU1LC0zLjUwNjIzIC0wLjA5NjcsLTAuMTkzNDMgLTcuMDYwODEsLTEuOTMzNCAtNy42MjIyMSwtMS4zNzE5OSAtMi44OTMxNCwyLjg5MzE0IC03LjYzMTY3LDQuMjQ4NjkgLTEyLjE5NTU1LDQuMTE2IEwgMzY5LjIwMTcsNTE0LjUzNjUiCiAgICAgICBpZD0icGF0aDQyNjEiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzOTkuODE1MzEsNDc5LjYxMTEyIDExLjY0MTgsNS42MDUzIGMgMi45ODQxMiwxLjQzNjc5IDYuNTI4NzgsLTAuNDc3MTIgOS45MTcwOCwtMC40MzExOCBsIDEyNy4xOTczOSwxLjcyNDcxIgogICAgICAgaWQ9InBhdGg0MjYzIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gNTE5LjI1MTUxLDUxNy4xMjM1NyA1MTguODIwMzIsMzA4LjQzMzYyIgogICAgICAgaWQ9InBhdGg0MjY1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjkyNTQ5LDM4OS43MTQ5OCBjIDExLjA0NDk2LDAgMzUuNTMzMDcsMC42MTkyNyA0Mi41Nzk3OCwtMS4wMDM5NyA4LjQwNTIyLC0xLjkzNjE4IDcuMDY2LC02Ljk1Mzc4IDE0LjE5NzEyLC02Ljk1Mzc4IDcuODA5NSwwIDYuNTQyOTEsOC4wNjIzNyAyMC4xNDE3LDguMDYyMzcgMTMuOTkwNjgsMCA0NC45NzY4OSwwLjM3ODg2IDYzLjkzOTkyLDAuMzc4ODYgMTIuMDgzOTUsMCA4Mi4wMDI2NiwwLjMwNDg5IDkzLjYwMDgxLDAuMzA0ODkgOC43NjA0NywwIDEzLjE1OTcsLTIuMjg4MjcgMjEuMzQyMTksLTcuMDEyNDMgNy4xOTUxNSwtNC4xNTQxMyAyLjA1NDU5LC05LjQ5MTM3IDIwLjQyNzU0LC04Ljg0MTc3IDIzLjE0NTQsMC44MTgzMyAxMi42NDMzNCwxNC4wMjQ4NyAzMi4zMTgxOSwxNC4wMjQ4NyAyNS4zNTk1NCwwIDEzMC45OTkwMiwwIDE1MC45MTk4NSwwIDE0LjMzMjQ0LDAgLTQuMTE5MTEsLTEzLjExMDIxIDI5LjI2OTMsLTEzLjQxNTEiCiAgICAgICBpZD0icGF0aDQyNjkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU4OC42Nzk1NyIKICAgICAgIHk9IjczNS44MDQ2MyIKICAgICAgIGlkPSJ0ZXh0NDMxMCIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMiIKICAgICAgICAgeD0iNTg4LjY3OTU3IgogICAgICAgICB5PSI3MzUuODA0NjMiPkxpbmNvbG48L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY4Ni4zOTg1IgogICAgICAgeT0iNzY1LjYyODQyIgogICAgICAgaWQ9InRleHQ0MzEwLTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNiIKICAgICAgICAgeD0iNjg2LjM5ODUiCiAgICAgICAgIHk9Ijc2NS42Mjg0MiI+SGFycnk8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgIHk9Ii04MDIuMzc3MzgiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgiCiAgICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgICAgeT0iLTgwMi4zNzczOCI+V29vZGxhd248L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU2Mi4xMTkyNiIKICAgICAgIHk9Ii03NzEuOTY4MTQiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yIgogICAgICAgICB4PSI1NjIuMTE5MjYiCiAgICAgICAgIHk9Ii03NzEuOTY4MTQiPkVkZ2Vtb29yPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTguMzA0ODciCiAgICAgICB5PSItNzM4LjM2NjQ2IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yLTkiCiAgICAgICAgIHg9IjU5OC4zMDQ4NyIKICAgICAgICAgeT0iLTczOC4zNjY0NiI+T2xpdmVyPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICB5PSItNjc3LjIwMzk4IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00IgogICAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICAgIHk9Ii02NzcuMjAzOTgiPkhpbGxzaWRlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTcuMzI3MDkiCiAgICAgICB5PSItODYyLjYxNDA3IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNS0zIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgtMi05LTQtMSIKICAgICAgICAgeD0iNTk3LjMyNzA5IgogICAgICAgICB5PSItODYyLjYxNDA3Ij5Sb2NrPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1ODcuMzcwMTgiCiAgICAgICB5PSItOTI2LjEzNjYiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTktNy01LTMtMiIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00LTEtMyIKICAgICAgICAgeD0iNTg3LjM3MDE4IgogICAgICAgICB5PSItOTI2LjEzNjYiPldlYmI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9Ijg3MS4xNjEwMSIKICAgICAgIHk9IjYzNy41NzUyIgogICAgICAgaWQ9InRleHQ0NDY1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDY3IgogICAgICAgICB4PSI4NzEuMTYxMDEiCiAgICAgICAgIHk9IjYzNy41NzUyIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICB5PSI1NzcuMDMyNDciCiAgICAgICBpZD0idGV4dDQ0NjUtMyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00IgogICAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICAgIHk9IjU3Ny4wMzI0NyI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgaWQ9InRleHQ0NDkwIgogICAgICAgeT0iNTEwLjI2MTgxIgogICAgICAgeD0iODc1Ljk2NjQ5IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSI1MTAuMjYxODEiCiAgICAgICAgIHg9Ijg3NS45NjY0OSIKICAgICAgICAgaWQ9InRzcGFuNDQ5MiIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MjFzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iODgxLjMxNjU5IgogICAgICAgeT0iNDUwLjE5ODc2IgogICAgICAgaWQ9InRleHQ0NDk0IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDk2IgogICAgICAgICB4PSI4ODEuMzE2NTkiCiAgICAgICAgIHk9IjQ1MC4xOTg3NiI+Mjl0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNjE1Ljc5MjQ4IgogICAgICAgeT0iMzg3Ljc0NzE2IgogICAgICAgaWQ9InRleHQ0NDY1LTMtMSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00LTEiCiAgICAgICAgIHg9IjYxNS43OTI0OCIKICAgICAgICAgeT0iMzg3Ljc0NzE2Ij4zN3RoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MTkiCiAgICAgICB5PSI0ODEuNjUyODYiCiAgICAgICB4PSI0ODQuNjkwMzciCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjQ4MS42NTI4NiIKICAgICAgICAgeD0iNDg0LjY5MDM3IgogICAgICAgICBpZD0idHNwYW40NTIxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj4yNXRoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NjMuMDQ2NzUiCiAgICAgICB5PSI1MTMuMzYxMzMiCiAgICAgICBpZD0idGV4dDQ1MjMiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1MjUiCiAgICAgICAgIHg9IjU2My4wNDY3NSIKICAgICAgICAgeT0iNTEzLjM2MTMzIj4yMXN0PC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MjciCiAgICAgICB5PSI1NzcuODk0ODQiCiAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTc3Ljg5NDg0IgogICAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgICAgaWQ9InRzcGFuNDUyOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzMSIKICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICB4PSI0MzMuNTgwNzUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICAgIHg9IjQzMy41ODA3NSIKICAgICAgICAgaWQ9InRzcGFuNDUzMyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+QW1pZG9uPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI0MDUuNTMwOTgiCiAgICAgICB5PSItNTIzLjU0MDE2IgogICAgICAgaWQ9InRleHQ0NTM1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDUzNyIKICAgICAgICAgeD0iNDA1LjUzMDk4IgogICAgICAgICB5PSItNTIzLjU0MDE2Ij5BcmthbnNhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzOSIKICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICB4PSI3NDUuNDg0NjIiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICAgIHg9Ijc0NS40ODQ2MiIKICAgICAgICAgaWQ9InRzcGFuNDU0MSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+V2VzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTk2LjcyODMzIgogICAgICAgeT0iLTUzMS4yNTkyOCIKICAgICAgIGlkPSJ0ZXh0NDU0MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NDUiCiAgICAgICAgIHg9IjU5Ni43MjgzMyIKICAgICAgICAgeT0iLTUzMS4yNTkyOCI+V2FjbzwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU1NSIKICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICB4PSI1OTUuNDM0ODEiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICAgIHg9IjU5NS40MzQ4MSIKICAgICAgICAgaWQ9InRzcGFuNDU1NyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+TWF6aWU8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgIHk9IjE2Mi4wNjg3NyIKICAgICAgIGlkPSJ0ZXh0NDU1OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMC43MDcxMDY3OCwwLjcwNzEwNjc4LC0wLjcwNzEwNjc4LDAuNzA3MTA2NzgsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjEiCiAgICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgICAgeT0iMTYyLjA2ODc3Ij5ab288L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjI0MC41ODk5NyIKICAgICAgIHk9IjU3NC40NDU0MyIKICAgICAgIGlkPSJ0ZXh0NDU2MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU2NSIKICAgICAgICAgeD0iMjQwLjU4OTk3IgogICAgICAgICB5PSI1NzQuNDQ1NDMiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU2NyIKICAgICAgIHk9IjUxMS42MzY2MyIKICAgICAgIHg9IjIwNi4wMzE3NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTExLjYzNjYzIgogICAgICAgICB4PSIyMDYuMDMxNzUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPjIxc3Q8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjYyMC40NDMxMiIKICAgICAgIHk9Ii01MDYuNjgyMTkiCiAgICAgICBpZD0idGV4dDQ1NzEiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NTczIgogICAgICAgICB4PSI2MjAuNDQzMTIiCiAgICAgICAgIHk9Ii01MDYuNjgyMTkiPk5pbXM8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU4MyIKICAgICAgIHk9IjY5OC44NDAwOSIKICAgICAgIHg9IjM3MC4yMTY4NiIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNjk4Ljg0MDA5IgogICAgICAgICB4PSIzNzAuMjE2ODYiCiAgICAgICAgIGlkPSJ0c3BhbjQ1ODUiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1hcGxlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSIzODQuMDg0MiIKICAgICAgIHk9IjY4MC44NTEzOCIKICAgICAgIGlkPSJ0ZXh0NDU5OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDYwMSIKICAgICAgICAgeD0iMzg0LjA4NDIiCiAgICAgICAgIHk9IjY4MC44NTEzOCI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzNjcuOTA4MTcsMTAwOS45NTk2IDI2My4wMTgzMywwIgogICAgICAgaWQ9InBhdGg0NjA1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDciCiAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgeD0iNzM2LjI2NzQ2IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgICB4PSI3MzYuMjY3NDYiCiAgICAgICAgIGlkPSJ0c3BhbjQ2MDkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1lcmlkaWFuPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ5NzkiCiAgICAgICB5PSI2NDAuMjA1MjYiCiAgICAgICB4PSI1NzIuODMyMTUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjY0MC4yMDUyNiIKICAgICAgICAgeD0iNTcyLjgzMjE1IgogICAgICAgICBpZD0idHNwYW40OTgxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NzUuMDg5NjYiCiAgICAgICB5PSI2NzAuOTAzNSIKICAgICAgIGlkPSJ0ZXh0NDk4MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDk4NSIKICAgICAgICAgeD0iNTc1LjA4OTY2IgogICAgICAgICB5PSI2NzAuOTAzNSI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNDk5LjQ4OTYyIgogICAgICAgeT0iMTAwOC42MDY5IgogICAgICAgaWQ9InRleHQ1MDQ3IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5IgogICAgICAgICB4PSI0OTkuNDg5NjIiCiAgICAgICAgIHk9IjEwMDguNjA2OSI+NDd0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iMjE2LjY0NTQzIgogICAgICAgeT0iNzI1Ljk4Mjk3IgogICAgICAgaWQ9InRleHQ1MDUxIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDUzIgogICAgICAgICB4PSIyMTYuNjQ1NDMiCiAgICAgICAgIHk9IjcyNS45ODI5NyI+S2VsbG9nZzwvdHNwYW4+PC90ZXh0PgogICAgPGZsb3dSb290CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgaWQ9ImZsb3dSb290NTA1NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6MThweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIj48Zmxvd1JlZ2lvbgogICAgICAgICBpZD0iZmxvd1JlZ2lvbjUwNTciPjxyZWN0CiAgICAgICAgICAgaWQ9InJlY3Q1MDU5IgogICAgICAgICAgIHdpZHRoPSIzNDMuNTcxNDQiCiAgICAgICAgICAgaGVpZ2h0PSIxMDMuNTcxNDMiCiAgICAgICAgICAgeD0iMTkuMjg1NzE1IgogICAgICAgICAgIHk9IjE3LjE0Mjg1NyIKICAgICAgICAgICBzdHlsZT0iZm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIgLz48L2Zsb3dSZWdpb24+PGZsb3dQYXJhCiAgICAgICAgIGlkPSJmbG93UGFyYTUwNjEiPjwvZmxvd1BhcmE+PC9mbG93Um9vdD4gICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDYwNy03IgogICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgIHg9Ijc3NC44NzU2MSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgICAgeD0iNzc0Ljg3NTYxIgogICAgICAgICBpZD0idHNwYW40NjA5LTciCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1jQ2xlYW48L3RzcGFuPjwvdGV4dD4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzY0LjE1OTk5LDY1OC40Mjg5MSAyOTkuNTEwMjMsLTEuMDEwMTYgYyA2LjQ5ODcyLC0wLjAyMTkgNi45NzcxOSw5LjI1NDEyIDE2LjU5NjMxLDkuMzkyNDcgMTIuMDU0MjcsMC4xNzMzOSAyOS4xMTA4MywtMC41MzU3MiA1NC4xMTQzNywtMC4zMDExIgogICAgICAgaWQ9InBhdGg1NDQwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgIHk9Ijk0NC4zNTc1NCIKICAgICAgIGlkPSJ0ZXh0NTA0Ny05IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5LTMiCiAgICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgICAgeT0iOTQ0LjM1NzU0Ij5NYWNBcnRodXI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDctNy0xIgogICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgIHg9Ijc4MC44NDYwNyIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgICAgeD0iNzgwLjg0NjA3IgogICAgICAgICBpZD0idHNwYW40NjA5LTctOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+U2VuZWNhPC90c3Bhbj48L3RleHQ+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2Ny42OTU1Myw1MzcuMjEwNiAxNDEuMjgzMDMsLTEuMDEwMTUgYyA2LjQ4OTk5LC0wLjA0NjQgMTIuNzgxMTQsNy4yMzU0NSAxOS4xOTI5LDcuMzIzNiA1NS45MjM2MiwwLjc2ODkgMTU4LjY4OTk3LC0wLjE3MzMzIDIzNi41MTQwMiwtMS4wMTAxNSA3LjgzOTU2LC0wLjA4NDMgMjIuNjMxNDcsLTE5Ljg1MzU1IDMwLjMwNDU3LC0yMC40NTU1OSAyMi4yNjU4OSwtMS4zNTE4MSA0NS4xNzk0NSwtMC41MDUwNyA2Ny42ODAyMiwtMC41MDUwNyAxNi4xNDczMSwtMC42MzI0MSAzLjYxMDE2LDIwLjcwODEzIDI2Ljc2OTA0LDIwLjcwODEzIGwgMjQzLjQ0Njc5LC0xLjAxMDE2IgogICAgICAgaWQ9InBhdGg1NDk2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzY2NjY2MiIC8+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI2ODUuMjA4MTMiCiAgICAgICB5PSI4MjcuNTMwODIiCiAgICAgICBpZD0idGV4dDQzMTAtNy04IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtNiIKICAgICAgICAgeD0iNjg1LjIwODEzIgogICAgICAgICB5PSI4MjcuNTMwODIiPlBhd25lZTwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0iTSA1NTQuMjg1NzIsNzIxLjQyODU3IDU1MCw1NDMuMjE0MjkgNTQ3LjE0Mjg2LDEwMi41IDU0Ni43ODU3MiwyMy4yMTQyODUiCiAgICAgICBpZD0icGF0aDU1MTkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIiAvPgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTI5LjYyNTMxIgogICAgICAgeT0iLTU1MC44NDc3OCIKICAgICAgIGlkPSJ0ZXh0NDU0My01IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU0NS0wIgogICAgICAgICB4PSI1MjkuNjI1MzEiCiAgICAgICAgIHk9Ii01NTAuODQ3NzgiPkJyb2Fkd2F5PC90c3Bhbj48L3RleHQ+CiAgPC9nPgo8L3N2Zz4K\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"showTooltip\":true,\"autocloseTooltip\":true,\"showTooltipAction\":\"click\",\"defaultCenterPosition\":\"0,0\",\"provider\":\"image-map\",\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"mapProvider\":\"HERE.normalDay\",\"draggableMarker\":true,\"editablePolygon\":true,\"mapPageSize\":16384,\"showPolygon\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${coordinates|ts:7}

Delete\"},\"title\":\"Markers Placement - Image Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id;\\n });\\n\\nwidgetContext.map.setMarkerLocation(entityDatasource[0], null, null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"c39f512a-21c6-6b06-3aa1-715262c6553d\"},{\"name\":\"delete_polygon\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.savePolygonLocation(entityDatasource[0], null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"94bf5ffd-b526-c6c3-ae3b-ab42191217d9\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}" } }, @@ -435,9 +437,10 @@ "resources": [], "templateHtml": "", "templateCss": ".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 200px;\n white-space: nowrap;\n}", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('google-map', false, self.ctx, null, true);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('google-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('google-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('google-map', false, self.ctx, null, true);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-map-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}

Delete\",\"markerImageSize\":34,\"gmDefaultMapType\":\"roadmap\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"colorFunction\":\"\\n\",\"color\":\"#fe7569\",\"showTooltip\":true,\"autocloseTooltip\":true,\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"zoomOnClick\":true,\"defaultZoomLevel\":5,\"provider\":\"google-map\",\"showCoverageOnHover\":true,\"animate\":true,\"maxClusterRadius\":80,\"removeOutsideVisibleBounds\":true,\"mapProvider\":\"HERE.normalDay\",\"draggableMarker\":true,\"editablePolygon\":true,\"mapPageSize\":16384,\"showPolygon\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${coordinates|ts:7}

Delete\",\"showPolygonTooltip\":false},\"title\":\"Markers Placement - Google Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id;\\n });\\n\\nwidgetContext.map.setMarkerLocation(entityDatasource[0], null, null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"8d3c0156-0a14-7a6f-0ddd-0ec16b9ffc91\"},{\"name\":\"delete_polygon\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.savePolygonLocation(entityDatasource[0], null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"46bf69cd-8906-234c-a879-e2e4c92f5b67\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}" } }, diff --git a/application/src/main/data/json/system/widget_bundles/maps.json b/application/src/main/data/json/system/widget_bundles/maps.json index d2151ecee0..32c93adc3e 100644 --- a/application/src/main/data/json/system/widget_bundles/maps.json +++ b/application/src/main/data/json/system/widget_bundles/maps.json @@ -18,10 +18,11 @@ "resources": [], "templateHtml": "", "templateCss": ".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 200px;\n white-space: nowrap;\n}", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('tencent-map', true, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.map.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('tencent-map', true);\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('tencent-map', true);\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true,\n ignoreDataUpdateOnIntervalTick: true,\n hasAdditionalLatestDataKeys: true\n };\n}", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First route\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.5851719234007373,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.9015113051937396,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.7253460349565717,\"funcBody\":\"var value = prevValue;\\nif (time % 500 < 100) {\\n value = value + Math.random() * 40 - 20;\\n if (value < 45) {\\n \\tvalue = 45;\\n } else if (value > 130) {\\n \\tvalue = 130;\\n }\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"
${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details
\",\"markerImageSize\":34,\"useColorFunction\":true,\"markerImages\":[\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic7b13uB3VdTb+rrX3zJx6i7qQUAEJIQlRBAZc6BgLDDYmIIExLjgJcQk/YkKc4gIGHH+fDSHg2CGOHRuCQ4ltbBODJIroIIoQIJCQdNXLvVe3nT4ze6/1/XHOlYWQAJuWP37refYz58yd3d6zyt5rr1mX8B7S5Xo5/0nPYaNFM1PY0gGqOhfAgQCNBGlWFFUAYEIeihigbhFdZQwt85BV5Gj9r/718R2XX365vFdzoHe7w6d77xnPkn4YpAtU0YiizNJcmPNkMQFkDiSlowHt2HNtGlTSJ6B+pTpsKTfKgTj3Pi8SMtFtEZnFs8d8dPu7OZ93BcCHtt0+OiL+FJjOiqy5K5dtLwD4PBHGvy0dKLYo8B+1+lAldv50FfmFzWX+84i2M3a8Le2/Dr1jAKqCHtl2y1wC/pEMP9ZRLBaYzF8CCN+pPluUkOKfB6qlmk/dBwTyt8eOv2AZCPpOdPaOAPjA1h9/SJX+TyGXuz0TZi4EcPBeOk+U+RErZh2YyMAyQJEoZUjFgtkCAEScgDyx1hmInTglqDj2U1X0WILaPbWvwHO1WummeuLONhaXHTf2wsfe7rm+rQDe133j/i5xPyrmCr+OouhSKPbdQ5fLiezTIYUBQGMJBgYWxMYSISZhbxgQT8wGAgDiwWxUvCiBxKhSKOqdh4OyV5+6XiEfK/kjVOXQ13apG+I0+adKpXaG0/Si0yZdvPbtmvPbAuCNT98YTBhT/8fAmEpHoXgKgPe/6gFGP0nwG8s2YykcaRCAYYQ5tKTkDVuArDEwMRF5AICS4VZ1AQBSr6oEgL36CBAvlKqIsyLOKQl5TZH4uN+TawDuY6o64lWTJX20v1S633uJNvfmvnbRERelb3XubxnAX26+5gDy6Y9HtrU/wERff1XjSt0WwULDmZEMawPOgilgQ4FaGCEygaXQMQyRMaxiUijUkAEAImIGAFURAOrVA1AmI1ZExGuqoqkVFefhyGtKDql4X4eHc6LxJof0VIVM3nVc4uXaHUPlo0Tpc2fv/zer38r83xKAd6y74iImO31EMf9REA7cpdVBY8NbA5+dFNqsCTQipkitBjAUsLUZNd4qm8AyjDMmJAIRhDzDEBEbJkBVAyJWQJ14AEaciIeSGicOgBeBWNHEeXLkXIM8UvFI4bVBCVJNfdk7STd5xOcp0LZzjIqV/eXq/4i61edM/eaN7yqAqpfzf62Nf5LP5lbko/DbCuxU4saEN1mN2kKTzQbIkuEIEWfVagRDEVkOyXCkVq0aDg2p9YYNAySVerU0WN1R27Jjo6ulMQ1V+ggAOgsjNRNEus/IiUFnYUy2kM23AcrivXh2RiTxjhx5iSmVWEWdpmhQ4qvwSBBrXVPfqDmuVsT7C3aZvKslyZcr9dpxdr81F8ynO/w7DuD1q/8y6kDw2872ticN0deG7wvQHXHmdxGK+1ibQag5ikweliIElNUAEayNYBCSRQRiYzf2rNtx11O/rC5d9dj+1aQyM2Pyz3WGozaNisYNWY7SYtgWA0A5KUVO4qAn3t4+lOzYt+Grh+bDwstHzvjA2tPfd1Z+39FTRhGpi7VBKrE4nyBFDKcNJL5OCerqUEXdVeEQb0mk8lECjR0euxe9cqBUOnoQ6RkXT78hfscAvH71X0Z5kf8Z0dH2CgNf2NkI0d0ZbmtElMtFVEAQ5BFIlkKb00AzFJqCGooQcJjv7t868P3/ubayZvua48ZlJt57xLjjB/cpTssXokK7IQNrbeoZ3pIRJm1aYSUW9cwixglZ7xNU40ppY7mr+sy2ezt7G1s+vP+EGfd/+fS/Ko5pH9/pJK04X6MUDSRapcTXkXJN46QKp1UkqNVqvpxVyLzhOajihh1DpVkmrJ7+uak/bbztAF6/+i8j62p3j20vbgXR+cP3LYU/Djg/KcsdEnIWERcRIk+hzWtEOYSch2U76tk1T6+84Tf/NCdni2tOmbRgy6T26WOiKDBhGFEQhrBhiNAyjDGiQp4DFgI8AChg1BGBXOC9p8QJ0kas3jvEcUxxnLgNpTW9izfdOqGWlve7+OOXrThk6qEHKtKehq9xIlWkvoaYytrwFYqlglgrcZxW+oXSz+ycpOLmnsHypDTIfuTNcuKbAvD2288x22dn7hrVnt/ATBftBE/CH2aCtqkZU6CI2hHZomS4YCPK+5AKHFB2ZNe2Nev/739/e9qY3KRnPzHtQp/LtnfkMhnKZDMa2oDCTIjQhghDC2MCCQITAyYxpmkhAIAZDDA7l4bOSeR9YpLEwfkUjXqMOE0QN2LU4waq9aGBX6/+d7O9sXnu3579jbVTx02dlEilL0FDG1pJG64cJX5IGr6MupY5duU1npIv7sTQ4196ytUDx8+sf+TN6MQ3AyBd8+L8W0a15zYw0d8O3ww4vC7ijlkZU5QctVPE7QhNEVlTRNYUjHcy7tu3fuuVSqXBF8z66962fMeIfDaHfD4nmUyWsrk8BdaYIAh9EFoxzExEysYoAQ5A0ioAEIpIBGZmAM459iKaJo6cT209TnyjWkOSNLRWi1GtV9A3sGPg56uvG1vIZ9N/OO9rM8jS9oavSOwqaEhZYh3khq9K3fdpXWsbvdR3MoYCV/UOVadcOvv2C/AG9IYAfue5j1/U0R5mIhNctxM8yvxLyMVpOduJyLRRnto1MkXK23axlB27sXtT1z//8vqDTt3vk/fMGnX4xGyhiEI2Qi6X1Ww2S7lCIQ3DkCxzQEQKYADANgCbW6UHvwcRaO6fAwCjAewLYAKAcao6UkRIBEniEtRqNVOrVKjeSFCP61oaqurKvqe237P2lnkXn/X/PT9l3OT9Eql2V90QN1wZdRqSuhukhi9T3Q2s9ki+NDzHWppeUqnG/qsH/+b7fzSA33ruI7ODIDh/RCH6KkEZAEINfhia4n4ZO0KzphN5005Z06aRaeOAcjP++4Ff3P/86hWTLjr08i3FfEeurS3LUTanhVwe+XxOwjAw1loLoB/ASgBrAdSAV232Gc0NyJGt70+27mlrzNT6nAEwDcBMACO892kcx1KvN6hUqWu9Xka9XsfgUP/Qjcu+Nf3g6bO7zj7urBNT1F+quxLXfUkaMmDrviQ13+8THdqYqvuLZpfq+qrJNXFDbrp87t0v/cEAXr5iduiTMQvHd2QnKDC9+bC9NUfF9kwwgvNmBGW5Q3O2SFkzAkaCg/71Nz9+2MTZ6rlzLs4Vi0WbyWS5o63N5fM5G0VRaoxpA7ChBVw3ANMq1AKoHUAewCwARwHYvzWctQCeaNUrt4pvgeha17Gtevt47+M4jrVSqZlSqepqjQpVyyX/8xU3VBHF2T//+OeOFbgXaq5fa75ENR3SarzDxDToYz846FTORbPRV7oHG9sm+qEPX3TEM3vc9pm9AfiBP53+T6Pbwo0Cd4aog4p/yXK+lDX5IDIFZDinGS7CckEM+JB//u9/e3Z8NGPTgjl/Maq9s8N2FNtcPpc1bW1tFIZhaIxJATwFYA2AtAVWh4hERBQByIgIE1Gsql8gou8AeAjAfQAeVdUvEtE9reFFIpIloiyATgARgCqALQAGmHmUtTYTRWHDhhaGYE0YYmbHEXZj//rBRc/fXTly5qGHEus2FUceCbxP4DShRJ2mvuIFboyqG5kNcNuWVM965MbNd71pAC99+vADA+MnR6F+TeAg6h1TeE/I2bbAFjVLBbJcpIDzZNke8qNf//yxKblZWz42+9Pj2opFbutop7ZCQdva2hAEQZGZXwGwDEBDRCJV7VTVfVV1BDNPUtXZqnomER2tqi8S0REAzgJwUqvMI6JBAM+p6pdU9f1ElGu1E6lqUVVZVYWI6gA2EFFijJmSiUIPsDbXmGT3b59V6Kv0dd334uLGYTPmHK7Q7lRi65DCawqviXWSrEm1PlvgWMh9KPbut+/77Ohtj/97d98bA6igo7aM+O/Ogp0l8BNFPQhyY2RyE0MqcC7Ia2jyGpksBYj2//WDCx9uk/EDZ8783JhiW5HbigXpaG9HNpvNMXMGwAoR6SWiUKS5KhERS0QqIgmAHcz8sqrOA7AdwCcB9AK4CcBvAdwP4EVV3V9VPwGgC8B4Zv4PIqqoqgPQYObEOadExC1A60RUJaLxURQaZqoRW0NEsm/xgI6u7rV9L295vmvGlKmHQ32vk0QdxfA+oYTq+Vgbi70mR4p6BEaKlTid98S/9f4MV7wBgF/66AEnFbPUz+z/VNTBiywLgxxCFDgwGQqR5wznOeR8+6p1657r6uopfu7wv4mKbW0oFvIoFovIZDIBEXkReUlVG6o6Fs2N/EjvfSczj2Hm/YnoY6r6Ae/9w0T0cVXdSkTfE5FsC8iTAZwI4DAAjxDRj0TkUABTACxS1csAzG39MHlmzqvqGCLKt1xZA0Q0QERtQRBkDZMngrcmNAeMmB08uHpxNsrz2pFtbft4TWInDZtSLE5T8i7uSKRS8XDjBX4fYbnusI2jMkt/tGP9rnjxrl+gICP4Riagrzb1ssKa4CkrYRhwwBFHYGSUOZJKo8oPP/vCoV846opSoZCnQj7HxUJRMplMgGblR5h5wHtfbE1oZAvIHBFtVtX7RKTQ4pSrnHOXAThQRK4BcIaqNkTkRRF5UVUTVf1462/TVPVSEfm2974qIm3MvBhAl6pGAEYAaBcR45zLiUiPiDxKRC6bzZpsNhtGUaj5fIG/dNTltYeeWja3ltbVcGgMZX1IWbUUqDUBbBA+OYxDPuDLSORq6KsN76s48MvzZnwwlzNDgaFzAIBAi0LKtGVtEQHlOaQCQpOHoWDWL+9+ZODCuV99cnTbmM5cIY+2JudZIpronHukxUWemavOuZIxpuG9H8fM8wDMJaJHVfV0ANcDOIyIPg5ghTHm+0S0UETWq2oCoA/AI6r6C2PMgyKyD4BPM/MggJ8COIGIFqnqV1T1YADbVXUjEfUaYxrOOcPMBVXdCmCutbZirQGIlIBwavucl2577NaJM6ftO1nJ9aY+YfEpvDryknamSNdAMQ1AGwxdc/DqDjz9k/7Nw5i96ixBSK/MhTRxJ7oUbracmWAoVGNCtRSCYOxLazfcN7VjdjK+beK4KAqpkMtpJpNRABNVdT2AowHUvffjAYgxZpNz7hUiuk9VT1LVWFX/iojuBfA1IrpfVRcS0Xne+6tUX33+M/zdew8AzxljLvPefxTA3xPRIufcpQA8EYUAFhPRSCKaKSL7EFGgqjtU1RDRZmaeGIbh1sh78s7LxM59R09um7585fqNdtqUMZOMMc4igE0DthSppcYWL80VTNbyX1QCPgNN1fJqDvzi0tnjQviObGia3Ee0JEAml+E8DOUo4pxaE4GUJz3yxJr9/vSIv+8uFAu2kM8jl8vBGNNJRE+q6grn3AZV3QRgi6q2AZjHzHNE5FEAp3vvv8HM8wFQSywvADAPwDgAi0TkPwDcBWDhcFHVh9FcXH9ARE4BMI6ZvyEiHwYwSVW/CeB0IlpERJeo6hwiepmIlnrvVzLzemZex8yDzDwZqlUikGGm6R0H66+evuPYafuNynvFkCCF4xjiBd67otN4C4GmEDAqTuVnR3++beWT/z5YfRUHio8/0dEe7DynJTUvswmmEiwxWcCDwGyee37j4ydNO6ucy+YmZMJQM5kMWWvHqmqPc24eADCzENEGAMvTNH2AiM5Q1W1E9GkR2cLM3yOiS0TkO0R0lao+zMy/8N7PBHAmEZ2C3YiIoKrdqnqjqq5i5j/x3n8bTQt8iapeKyKbjDGfFpEhAGOccw8EQdBhjPmQqk723rP3PrTWvhxF0Xgi6vHeayaTyx075fS7nlvxcPGgg8ZNIjHeSKRMdbEUIEHwEuCOA4DOvB25vSRnAfghMGxEFNRb7ZoM0HFNadFeIjvRgMFkhEDKbEl8Oqq7u3bs+/c9cXQUWo2iCGEYsqrG3vvHAPwEwL2qulZETnXO/Zm1FqoKVf2Bqh6qqr8SkW3e++tU9T4i+ntVnem9vw7ARQA6ReQ5AL9yzl3vnLsewK8APIfmovkiIrpWVWeo6t977x/w3l8nIluI6Dcicqiq/quqgpnJOfdnIvJR59wmEVlCRD9S1QeJKLHWmmw2hyAM9bhpp47q7q4d733aSVBlkBoNQGxgYPdVRZ82N5In9lS7dp42GgA483hMyUY0RXgwXzAjQgUtshp1WhOR5YgDzoiB0U2baqsPLB7z0oxxBxWz2Rxls1lh5gNVdbn3/rwWR68moi5VPZWZt4nIvgBGquoRAH5BRH+OprH4oYh8XlVPQXMvfIOI/BJAFxF1qupxRPRBIjpKVSe3dOtdInKbqj5PRIe3RHayiHydiMYDOIuZfyIin0HTfI4kIgAYa4y5UUQaAI4QkY8ZY5YR0aGq0kcE8k5NNS4t665u6G9r47xDCi8pqabsNbFe9WkoRvU0upYl8GunnqebX7kZQ00O9DipLbKjRfQTPWnXYyBTBxMBBiIML2IVkt20sf6B46d9rJjJ5chaQ0EQRAC2pWm6VlVXq+rZIvIXSZKELcX/Y1U9RlW/AWC8iJyqql9V1aOcc99W1SXMfAmAh1X1qy3O+rKIHCMiGRGptUqude9iIrqWiC4brisiDxHRt1X1KFX9qnPuowDGe++vUNUPishNLQkIiOjPVPVs7/02EVkLYHsYhtYYg0wm1FNmnZPftKF2lFPJisCIkhE1DFiFaNLr1i5R+PntGR5lFMcBLWfCxxbhrgkjgqMAjCKgkrWFX48KZ7RHJm8CziJLOXJpUNu4omAuOfbKOMxkKBOGHIbhHBG576qrrtLHH3/8QmaOdtdd/5tIROLTTjvtyc9//vN3BUGQs9aOA3CyiDxXr9dRrzfo2gf/Ljt1TpyYIMnWtQ4nVW2kNd+bri41fOlMADkQerb1p4/f+WGcaS9X8HOLUQIwCgCUdFGi6ehBt7k+3k4DqQ8cOd2+mQdPnP6xijHB+MAYhGEoqppL03T/J5544iRmpvnz5z+4Zs2a1dOnT5/+8ssvr5o5c+aMWq1WSdM0VdXORYsWHW+tXXbmmWcONV2jQG9v744dO3b0jR07dvSIESNG3HbbbbNFpHPBggWPtMTvVUREWL58ee2VV145bcSIEU+ddNJJ1RY4unLlytXTpk2bEoZh2N/f37dw4cKTrLUdxWLxvnnz5pnf/e53unDhwhPa2tpWnnfeecekabopCIIMEYGIyBjGCfufvmbpltuKY6a4LKkzCh8PpZu913g0oIsAOhOKMQTElyvYPrsY43IRP6uK8wCAYHrUo+gpiXoaG+LR0X5VaNgxNEAHz5pz6PIgMGBmBTCKiJZVKpUjjDEmTdPG/PnzPwSgLCJHoLlY/omqXgLgWSJauHjx4uNPP/30obPPPnsAwGNoLl+O32Xdt/a3v/3txnK5HM6fP/+3aJ2JAAi89zkAUwGcdOqpp+YvvPBCnH322fEJJ5yQA3CH9/5YY8yft0C+SkTmP/roo72NRqPjhhtuODCTyRTPOuusRy+88MJVd9xxx8cWLFiwiog+oqp3ARgVBMEO7xVzJ70/v2jdHbNGqu/16uq98WakmuQgANhsU98MRQwMP7N0iYxhUuybD/n3WzqlAMROROElzfY3NrXHrtTNFHTkMvkiGQNiZhGZ7ZzbPDx5IoKIXK2qZzDzd9F0T/0pEV2qqoeKyN8BwLZt27ap6hmq+l0RmQXgZhH5iohcpaqrwzA0RATn3DXOueta5buqeoWqnqWqT9dqte8DwPbt2zeKyBGq+l1m/giA7wL4map+jYj2S5LEA0AYhp0AvsvMp5577rn3Axi/YcOGxaoKEdkCYBYzqzGEMMgUWILRjXSopzfekFUf5wUKYXYQCoZhykcM08C+DMUMw7Rva8sHqHZCJFD1VtTDaYLuoe3xrLGH/Yu1NiZVtcYAQEVVy7vpmPNU9VHv/RUArgZQ9d5f473/qYj8OwBMmDBhPIBnnXNfAfAj59w5AK4F8DURmcfM1JrY/4jIrSJyq/f+XlV9vmVMPlEoFC4GgM7OznEicmPrB3hJRC4Tkc+IyI+897cFQWBay5lrVfVKVX30lFNOOUZV/aJFiz7YMi79RFQiIgbg2NrazHEHf7+70q1eGiwkROoteQkhOmIYp8DQBGUcYIVwOJMepCCAkBCooCAnUPVwXoU1rrXVoyi7nwgoDO1QyymwzTn34d7e3p8B+NsWFx4AYLP3/l4iuoKIHhaR/yaiLw1z6rp169Z57+cR0bUiAiIaVNU7ReR5Y0xcrVbPbf0ek1U1DwCq2qOqG4jofhHZUi6XAeC7IkIAvqCqIKItaG4LZ4jInxERvPevtK5fY+b7W+0eBGD78uXLx6nqd51z85i5G0Bore1rNJJsxuan1EumFo3w3mtKSupAMASNRJEACBk6ixWphWCaKs1tqegVUIWyiBcPIYhRQlLKhQccNDtW9YEIh0TkiciJyGFtbW29LfCCxx577PtHHHHEhdbabd77bzLzFap6jPf+X5o46Jf333//qWh6kP+P934HMx8F4HQA53rvkc/nl9frdYjIQbsw99SWy6opPvl8BQC6u7u3ENFfq+poVb1IRK4iIvHeX7dy5UpKkuR8Zka9Xv9WNps9n4j2B/DNkSNHnrV9+/ZRIvIhIjpMVZeoqlfVEcyQ6WNmpQ8+nyva9m4IO/XeQ1XFE6UKfYkUhyrTEVDEFkAWO4NuZAuAsPnDKlgFzih8ku0cU5y4NQiCxFrLAPYDUCOizxpjrgAAY4y54YYbvtwS5f1E5B9UdSgIgloURR8BIESEO++8c8qmTZtetNYeHYahdnR0wHv/pIhsrVarvX19fQsA5H71q1/dYq01pVKpkCRJXCqVaGBgwDcaDdfX1zcRwDELFy788JIlS96XJEnBOQcADSIKmfkSIsKwpXfO/bmItBljLlHVa6dNm/bIE088sR+AMUT0WRG5kIgmWWtfIWPcuPZJDJ9r90hIRVTEq5KAlBIIdYH0UCg6FMhZUvDvjSDVnZBhUhUSUijICxHCbDFXZGOMqKoH0KmqQ/l8/ptdXV0/rlar38rn8zs5hJmJmUM0jyPb4/j3h/ze+ylLly6dgr2QaepX3Hnnnefv7ZmdoyUamyTJWABoHvTtmbq6un4xa9asSQCuA7DSWvtSo9E4zHt/dbFYvKLRaKwF0E5EwoBENlKVMOPFkcJDCRBVUlEloLQTLgWz1987FAhImCECJVEh8Z6cdzBk20ITkIg4Y4xX1ZFoHuJM3XfffT/S29uLLVu2oFKp7HQ9/W8ia+2RzHyGqv6TiPzjsccei97e3kxbW9uZACYTURVNb7mIiIYmJIOwLUWqTqQVIqFEDFHV6nC7orDMBB22LOzhWbRC0LJRLalqGYqyQWAJVDPGVJIkqQPYrKq9AGCMmQoAaZpix44d2Lx5M/r7+5Gmbzn4822jVatWvei9/9M0Ted77/9j5syZawAk27ZtswCgqt0AtohIzRhTssZWDdvQkA4RtETaxAOqZSWWnXgR1Kr8/kTbG2ThtaAE9QQSZWIQ2EilFteyhoJCa4lxYMvf9xry3qNUKqFUKiEMQxQKBeRyudcVsXeC0jRFrVZDtVrFzTffnOnp6Tl2/Pjx944ePXrt9OnTzyGirY888sjLCxYsOERExhPRDGvtswACrz4m60pOqIMIBIX4ZqCYAWsZLXumAtid6z8A5DSvlgkKFkcMiBERqHUDiUu8994SkQCoEFF+jyPfhZIkQX9/P/r7+xEEAbLZLKIoQhRFbzugzjnEcYxGo4FGo/EqCejp6Tnv5ptvfk2dH/zgB8sWLFgAVS0CqHjvyTlnq2mFYF3VORnJICKwI2IFI0Qi7TCtLaYCVgnbAdoA6GRhaoPXhipIVJkEUCXP7CrleBAd2RHsvYcxpopmfMreaICZN6LpQWYRmZSmaeeuk7LWIggCWGsRhiGstWBmWGuxqwUFABEZ9ilCROCcQ5qmcM7BOYckSYbd/XuiTczcT80YHHjvZ6MZZ4O+vr5hx+14Va1Qa/M9WB0Asa+SUCcIRuAtg5QEBKDYrEJrwdhiIXhBRQyIJkMxQxQvkELh4RUq4kCJ2VHdOLiOx+YmmTC0trWwnQOgsvtoiegFInKdnZ3rRo0aJT09PTw0NAQAm0VkzvBzw5N/B0mMMU+pqhk7dmxXsVjkzZs35xuNhojICDSPRpPt27c/WSgU5hLRC95722g0aOPgWnbcW5VUBYCSJYBBChgQzWnt2J4BsJyheFkVr7Q6Hc2kZYU6ARSejCjZFN259UOrc6reOucMEfWpqnXOPQIAhULhN8PgMXNl3rx5Y4IgOIuZz46i6KyTTz55JBFVmXnFO4nYrmSMeTKKooEPfvCDs40x8621Z3d2dp566qmnxsxcArC1s7PzkVWrVi1X1QBAv/eeiYg2DK0upOgpiCBQIlIBBOrBOgTCCAAQ0jUQrGS1WF1vUPewLlTlKoQCOARewOqVUgzmtlXWTWuKiqiIVAAgjuOtuy1bgtNOO21ET0/PhO9973sQEXznO99BT0/PxJNPPrkDQAO/97C8k7RBVaO5c+ce19nZmb3yyisxZcoU/NVf/RVWrFjx/kMOOWQ9M3dXKpVRjUYjbKmGinOOnPPYWt04PZGhjHoQCZigAQsFpFwbxqlRpx6k6LI6gK5Kpz8zm20d0JHWQFAYTSUlALDexSNdEB+Y+nQxpZRlppSZ4ZybdPvttz9QqVSOt9Y+SkR+xYoVxx522GF4/PHHceCBB2LZsmWYPn06nnrqqQOZ+REiekZERr+T6BFR37hx47rWr18/NwxDvPLKKygWi3jhhRdw5JFHolarzXvuuee60jSdYFordxFJnHNI0rghiGc4jb3xUDEQEngyYEBrwx7KcuJHZzux1t79KZQ++iv5AHTnCadVBZGQhULh1SsIMfoe7KlsGRqTm5Q1xmkQBJtV9dijjz766f06bwAAEgVJREFUnpUrVy4EgIMPPjh300034bjjjsOaNWtQqVQgIjjqqKOwZMkSzJs3b/Xy5cstgFUA3rZF954cr6eccsrYxx57DJ/85CexcOFCDA0N4cQTT0S1WsWjjz4azp49+4l6vc5Tp049TVU3eu/hVXVbZUN/TH33k8c4DVRIiMFEohCjCIdXLC6VY+44DV+zACCEXiiWgnCkEp1EpKsEqqTEIsTq1Axg+eCy/kczp+QmqDZfuXpRVedNmjRpx9VXX32hiEBEsHTpUtx5551YsGABnHM47LDDcNNNN+GAAw7Al770pc8NPzdsUXe1rsOA7n4dBmjXK3NzgbHrZ2beWQDg7rvvxq233oqLL74YS5YswY4dO/Dkk09i7ty5uOCCCz4bx/FPRGSUiNydph71ap2W9T9eGGgsr4iqZSVVsLJ6Z5lIlU5srfmWAlgHtE7lDjgP5SjgAWb6MBTtoroMgpwoERTwniiJhwq5aPrxB+YOWwuQIaKEmWd573NBEHSoKosIpk+fjltvvRWqitWrV6O7uxvLli3DV77yFRQKhVeBtzcgd/2+exmm3bl3dy4kIowfPx4LFy5EpVLBpk2b0Nvbi+7ublx22WWw1ro4jgsARgJYVq/XUG/Uk2fK95+ypXxfrESGGUIEMhYGTP1ovQOYOr2+kcjvVt+K9c130cp4slyX4nDnBqYbRCAGkTZXUELIVtPeezeUu3rjOEaSJFDVpwEcmKbpLcMTnDhxIm644QYEQQDTPDvBNddcg3322ec1IL1e8d6/qryZOruDffTRR+PrX/866vU6kiTBAQccgOuvvx5hGKI15hki8lTz76lura/fUUt6F4siJIKCiREAakhB6BnGp1ST9lwbngJ2CfE99Zd4cPzIcDqg4xl4wQl64EE+BlyicCnYanHz4RMumviR9vO7C4UC5fN5JqKzVfXlKIomtzzGr5nwGwGwOxe+ngi/ntjuXowxe/s+0Gg0+ohofxG5o1KpoFqv6+LBn496dssPt6dcmWAtlCOCNRDKgJgxEopDoLRl60Cy5p5P4Hhgl/A2NbgmTuUGBeCBOUTokVZAtyiIFJSk5QmJlJKeyvaeer2u9XpdVPVxVZ1Zr9dv25PI7Q7M3sDbEwe+0Q+wt/b21vdwqdVqv1XVaar6eJwkqNdj9JY3bW9IKU5cZRwUDNPcuagBE2G7Kg5RAKnI9SD832HcdgJIARYOVdyknXtjoTpBoaRsTPOMHQy7fMutQy/qQzOr1arW63VNvd+kTc/NfO/9I3vTXXub0N5E9/U+v57Yvp7+VFWkabpYVc8DMJSm6aZyqcSNRk1fxOMHPb/5v+pQtWwgUBCxErGCiOJhXHYMuRkU4r7XAHj3aYhTAaC4rakI9dNkMMSWPBhMSsRKmjRKIyuuZ3Bzfe32crnGlVJJReQ+Vc3HcdyuqgPD4re3ib1ZHfhmVcDuYO4JxNaYetI0HYvmMen91WqVqo1YNqVdW2uutz9NSp3KTNpcxMEYgjEYVNULmvVxiwLVu09D/BoAAcAZXL6j7F9SBVRgiUwPkRJYCQaqrEoMWrrqp4WN2ZfmxXGtWq7UqFwuJyJyP4A5cRw/qKryelywNw7ck+58I336ZvtR1Uaj0XgewMEicl+5XPblcpXqtXJtk33x1KUr/6MAbnKdgQKsDFUVMTtUYFWBvpLvohRX7orZqyJU192K6tSz9Qv5HPcQaCpBZyvjRSiyEFIVkDioiBbL1W3LglGduWJ9LKDExnAtCIJEVU/w3t/MzIfsbiD2dn0jHbkrF+1qSPZkXHY3MMNX59ydaB5ePdNoNLZUqlVfrpSxOvO4earr5xvqvm8iGfggBFNIyiGYQwwQ4xwABqqLhmo+c885eJVf7NUx0gDE4iv9Q/JYc1+MDABvDJQs2DDYhlBmxD2Da6YNxOulW9dsr1TLWiqVtF6vrwawXFU/7Zz7TwB/FCf+MUuW1ylJmqY/F5GzVXVZvV5fWy6XaahU5q26asuA22L7hlbvR4a8NVAYKFsgMBACJZDm7mNHSZ41HpfujtdrovS7bkV58p/oRwpZ8zIIhwM0C0SLoBipCmqNnaHAhq3L7MT9D9mfhjIrrYRt3nu0fG9VAKd673+Npq8t82a5cW9ADdOb4bZdljfbRWSpNt9BeSJJknVDQ0MYHBqiwXRHd9+IriPvffpa4YBCE0I5grCFMRlSGFoF4DMt3ffDUtXLPfPxyzcEEADGnoNH01gWFLNmChQhgTJEOqiKQIQEAiPNU09+Zf3jfZNnH3yY9mVWasoFL16sMWVm3gzgNO/9KiJaq6qTdlfyewNv9+f+QNCGPz8qIgLgaFVdVK83egcGBk25UtWBel9f/4Q1x931yFUbYLWNIxgOoDYgDSJYE6IB8CEEjFKg1D2QdscVfHn9r/EaB+YeAdx8B9z0+Sgz8HxgeR6AMVB6hgzaVMk3Q/2JSQHvJOra+GTXlMPmfEi6o+d87NpTLyTeN5j5ZWae6b3fV0RuIaKZqmr3ZJ33BNzuAO4G0B7vMfOQiNyqzcBN8t7fN1QuN0pDJVQqJe2v9u2oTt9w0l0P/uNz3iQjghA2CMmEGXgOCSYDIqJuAk4AgHrDf7We6u/uPx97zO6x13fl1tyOtfucqRcXM+ZFAHNAmA2iu4gwRkBKos0jAVXy4vKvrHvslWlHHHZk2m1eQKJ5VfXOOauqG4Mg6FXVj4nIalVdpKoHqSrtsrzYed1VXAHsDaQ9caAQ0S0iMoqIPkBEDzWSZHWlXI6HBkvBUKWsQ2nf5uSA7SfeueTqFxPUxtpQAxMSmxBqAhKTBZhoBYALAUCBW3ZU/D6Lz8E1e8NprwACwKQv4nf1fvlUMWsJwEgC5oDpIVJ0EhGrJ6sAICCXuvYVqx8uzXj/YZPSWFbWelyHeA/nPRLvqwxa3XRN4COqugrNKPwx2ozifxVww1y3K4CvA95WAHdQ8xWHDwJY4b1/tlwupwNDVVTKQ9rfP6j19h3dsv+Ow29bdEWvUmO0CWBshowJCTZL3kQAW1pPTb1noPTK9oG0no7Cp9b/7LWi+6YAXP8zuMnn4rFG4kfnQ3MYgIgIU5jxDCmKCigBpE1xZlEfvPDSErffrFkU7BNQpSutxQ1PLo6zSerFi9RV/CvMXFXVQ1R1H1VdhGaIbxnAzgQ5u4vtLsUx8yMA7mPmbQAOJKI2VV2XJMlLtVqtViqVaLBUlUqpn0vloTofOhBVMptzv1h4dd4Yn7cR1GSJwwhiQhIbIjUBthBwJoC8ElzvUHqzKL5+/+l4zQuGu9Kbyplw4m04Ix/xjI68+W6r2gZifdI1dFSaEEtdOW2AJYG6hnqXEMaOnL7ptGO/+L5kjVks2/JjM5nIZKJAoihLmUyIIAjIGANjTEBEHSIyWUQ6RWSdqm5V1YqIpC3RDImoQETjiGgKM5eIaKOIDKpq4r2Hcw6NRgO1egzvUq3V6l5Hxhuys9OPP7T0lke7tj41nQNiG0FtBmojeBMR2yzIRNhKQh9U6L6kkMGq/7t6Ii8uXoDfvRE2bzprx0n/hc93FLiQi8x1zYq0CdAHvcdkV4V3Dupi9b6OgosR+wRGvU3PPuXSHcXcPiMGnvAvcJIZlwsjG2UzMESUzWa16SExZGxLGFS9sVbFK5SUAGBYWYoIMzN5BbnUgSCaph5xXCfvvSZJouVaw1NWejrfL3NK1a07frHwmpFsXcgRvA3hTRahNeRsHmKaXpZtIDoa0P0AoBb7SwZqEt+/AP/6ZnD5g/LGnHwbvtlZCAYzAYbzJwwo4U5xOl0aUB8jcDHUxUSuoQ4pJE0gmbCt9vFTLm4UM2NHDCxNlidDweiQOAyCUDkwFLBBEFhSZrVEqkDzHLEVAiA6PFBFE0pFkjhS9YjjVJ1Lkfg0sZ3SO+rI8NBSo7vvznuuz8S+lDMhwBbWhmRtVr3JgmwAmAhqAlolij+h5svfqMW4ZKiaFu49F1e/WUz+4MxFJ92GS3MR246M+bYSGEAizD8mJ4d6p+oa8L4OcQnUJzA+hhWnqU+gUdA2cPKxnylNHj/rmOrW9N7+F5JGOiQjyXIYcgC2zRejiVXFw5Np5Y3xMGxgxBMJPMSlFHtPUI1NG/eNmhNm8uODUzZse+nB+x78WVs9KXXaDMgYspyBNyG8iQATwIRZwIawYPOCQj4LICSFDNX9V6qJ5O5bgH/8Q/D4o3JnnfhzfC6yvM/IdvPXADpaLd0KoaJPNS+xmjSF1QYkTeEkVfYpGR8j9Q5WRKvjRkztPf5DC3j0iCkn+AQvlDdUu6rbXaPWn5KrCEEErTwXTTKALbDmRgSaGxNk26bmppoQc7p7ux546PE7ZHvfutHGUJ4DOGMRmEi9sSQcwgYR2GTgOCRvDFXVaJUU81sA9PcM+X92Trru+yT+8w/F4o/O3nbyrTiaGF8cUwgOIMZRreZegerDgB6YJiQSw0uqgYsh3sFrjMB5eE1gfAovHka9pjaM+ke2TxiaNnWujBkzOcxnO/KFXKHNBpnRAODSRm+lVh6q1odqPT0bkjXrnuW+oS3tLo1HsKGADIQDsAnhjEFAFgmHsDYCmYBSG4BMRgMQvQTQcYBOBwBVPN5TStd6hxvuPx9L/xgc3lL6u5N+hpGwuHl0u33a2N/nDiTSXxBIRHWCNMilMdQ7DSVF6h1YUxXvyKhD6h0CCKCCVLxa9YASKYlyK/AOIJAyCUFBDGImB4KlEEoMbywCCtQbQ8QhxFiEJqDYWLDJakBEm4g1UKFPDI/Rq16xY9AdZQzOXzgf/X8sBm85AeM5t8P0eXwtItYRbfZToOavCyDxKj81RCPgaKJ3iL1TAw9xCVgdvHcw6uBVm/pNvQIKpwJV2pkKBQCEFKoMYoKFITVGQQxPBsZYeLIwNoQQw3BAjiNEzNioQKzAebQzkJRW9lXcbXEqctx5uOryYUv1R9LblkP1+JsxjS1+MDJn7wkDuhKEHACQQqD4OUgExJPFq/EpqTglcXDqEXoPJYETDwbgROBVAQY7ABCIJQKYYQBYZogaWGMAMkhhEJiQPLMaG5BTlvWUsgXjvJahAxS1RqpfH6i5eYjxhfs/i7clj+rbm8VXQSf/HB8T4LOj2uwzgaF/0GZ2oeHuVqjq48zIQzHee4QiSLUZgwN4kDYdt0Kkqq38BM1XhYnAMMwKGDQ979y0rERIRbENQJWIPgDorF0m2Ei9Xt0/5N4njH+//zzc9XamRH5H0iAffiOC9gLOVeD8kXl7bxjyxYC+OqMv0VaoPsCEukAigNqg1EEEFlWBQKHUFC9SBoOYiEUhRDoIaInBiSgyBDpJoeN2m9qG2Mv1/SV3iir+s1zFbc9chLc97vgdzWR+uYIfugUnC/C3keUlHQXTaQiX7LUCox9en1XwIBENCqTcvM1FVe0gSAcMzYVgxN6a8IrrBit+IHFyrCF850Orcf/ll781Pfd69K7l0j/mJxhtLb4+ot2uDy3t1T30Vihxeml/2U1WxpVLPol3PA088O7/MwI6/ib819j2YDOb154vvBVSxfXdA+nEBz6Ns4G3T8e9Eb3mUOkdJsW++NT2UjpHVO/V5vrvrRfVh7f3pTNLdZyLdxE84N0HEEtOgMsRzukdcBUV2vRWwYOnbTuG3HZXw4J3wki8Eb2uQ/WdojW/RLz/n+CluKaZTMhzm4eJwB9aFHADFf1X7+X6h/4MG9+LubzrHDhM934KLyhoaSPB3/yx3Nco42+811UPfBbvWvD67vSu/0eb3enEn/K17RkeNExXvPHTvyfxeuVQQ0be9zn50hs//c7Re8aBw3T/Z+TScl3niuBm9cCbLLeXGjr3mA3yl+/1+N9zAEHQ6oA/rxLLBPF49o1Fl54vxVJ08Ge/kwvkN0vvPYAAHv8K6ur8BbVEnlNF6XUArNQS/ziJv2jJ5/Cm07W/k/SeWOE9UddvUJ5+pimpYhODTtyT1Y29fsOrv2fxhXj+vR3t7+l/BQcO0z2fc0ucEyeil+7OfV7xFYXI4gvx4Hs9zl3pPbfCeyA67cfmFiaziVX/BgCUcL1XGf27z/vz8S7vNN6I3t23oN8caW0//+lcF/0PC+4VIBJgZm2aPw3/y8AD/peJ8DAtOQEuZLfAQ0sK7Q0rbv6SE/Yen/L/017ojH8LZ5/xb+Hs93ocr0f/D6s769KBP+5xAAAAAElFTkSuQmCC\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"useMarkerImageFunction\":true,\"colorFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n if (percent < 0.5) {\\n percent *=2*100; \\n return tinycolor.mix('green', 'yellow', amount = percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', amount = percent).toHexString();\\n }\\n}\",\"markerImageFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nvar res = {\\n url: images[0],\\n size: 55\\n};\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n var index = Math.min(2, Math.floor(3 * percent));\\n res.url = images[index];\\n}\\nreturn res;\",\"strokeWeight\":4,\"strokeOpacity\":0.65,\"color\":\"#1976d3\",\"tmDefaultMapType\":\"roadmap\",\"showTooltip\":true,\"autocloseTooltip\":true,\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"labelFunction\":\"var vehicleType = dsData[dsIndex]['vehicleType'];\\r\\nif (typeof vehicleType !== undefined) {\\r\\n if (vehicleType == \\\"bus\\\") {\\r\\n return 'Bus: ${entityName}';\\r\\n } else if (vehicleType == \\\"car\\\") {\\r\\n return 'Car: ${entityName}';\\r\\n }\\r\\n}\",\"tooltipFunction\":\"var vehicleType = dsData[dsIndex]['vehicleType'];\\r\\nif (typeof vehicleType !== undefined) {\\r\\n if (vehicleType == \\\"bus\\\") {\\r\\n return 'Bus: ${entityName}
Bus route: ${busRoute}
';\\r\\n } else if (vehicleType == \\\"car\\\") {\\r\\n return 'Car: ${entityName}
Current destination: ${destination}
';\\r\\n }\\r\\n}\",\"provider\":\"tencent-map\",\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\",\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"draggableMarker\":false,\"disableScrollZooming\":false,\"disableZoomControl\":false,\"useLabelFunction\":false,\"useTooltipFunction\":false,\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"showPolygon\":false,\"showCircle\":false},\"title\":\"Route Map - Tencent Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('tencent-map', true, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.map.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true,\n ignoreDataUpdateOnIntervalTick: true,\n hasAdditionalLatestDataKeys: true\n };\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-route-map-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First route\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.5851719234007373,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.9015113051937396,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.7253460349565717,\"funcBody\":\"var value = prevValue;\\nif (time % 500 < 100) {\\n value = value + Math.random() * 40 - 20;\\n if (value < 45) {\\n \\tvalue = 45;\\n } else if (value > 130) {\\n \\tvalue = 130;\\n }\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"tencent-map\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"gmDefaultMapType\":\"roadmap\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"useCustomProvider\":false,\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"mapProviderHere\":\"HERE.normalDay\",\"credentials\":{\"app_id\":\"AhM6TzD9ThyK78CT3ptx\",\"app_code\":\"p6NPiITB3Vv0GMUFnkLOOg\"},\"mapImageUrl\":\"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==\",\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"tmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"
${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details
\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#1976d3\",\"useColorFunction\":true,\"colorFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n if (percent < 0.5) {\\n percent *=2*100; \\n return tinycolor.mix('green', 'yellow', percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', percent).toHexString();\\n }\\n}\",\"useMarkerImageFunction\":true,\"markerImageFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nvar res = {\\n url: images[0],\\n size: 55\\n};\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n var index = Math.min(2, Math.floor(3 * percent));\\n res.url = images[index];\\n}\\nreturn res;\",\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"strokeWeight\":4,\"strokeOpacity\":0.65},\"title\":\"Route Map - Tencent Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" } }, { @@ -36,9 +37,10 @@ "resources": [], "templateHtml": "", "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", - "controllerScript": "self.onInit = function() {\n var $scope = self.ctx.$scope;\n $scope.self = self;\n}\n\nself.actionSources = function() {\n return {\n 'tooltipAction': {\n name: 'widget-action.tooltip-tag-action',\n multiple: false\n }\n }\n};\n\nself.getSettingsSchema = function() {\n return TbTripAnimationWidget.getSettingsSchema();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true,\n ignoreDataUpdateOnIntervalTick: true,\n hasAdditionalLatestDataKeys: true\n };\n}", + "controllerScript": "self.onInit = function() {\n var $scope = self.ctx.$scope;\n $scope.self = self;\n}\n\nself.actionSources = function() {\n return {\n 'tooltipAction': {\n name: 'widget-action.tooltip-tag-action',\n multiple: false\n }\n }\n};\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true,\n ignoreDataUpdateOnIntervalTick: true,\n hasAdditionalLatestDataKeys: true\n };\n}", "settingsSchema": "", - "dataKeySettingsSchema": "{}", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-trip-animation-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var gpsData = [\\n37.771210000, -122.510960000,\\n 37.771990000, -122.497070000,\\n 37.772730000, -122.480740000,\\n 37.773360000, -122.466870000,\\n 37.774270000, -122.458520000,\\n 37.771980000, -122.454110000,\\n 37.768250000, -122.453380000,\\n 37.765920000, -122.456810000,\\n 37.765930000, -122.467680000,\\n 37.765500000, -122.477180000,\\n 37.765300000, -122.481660000,\\n 37.764780000, -122.493350000,\\n 37.764120000, -122.508360000,\\n 37.766410000, -122.510260000,\\n 37.770010000, -122.510830000,\\n 37.770980000, -122.510930000\\n];\\n let value = gpsData.indexOf(prevValue); \\nreturn gpsData[(value == -1 ? 0 : (value + 2) % gpsData.length)];\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var gpsData = [\\n37.771210000, -122.510960000,\\n 37.771990000, -122.497070000,\\n 37.772730000, -122.480740000,\\n 37.773360000, -122.466870000,\\n 37.774270000, -122.458520000,\\n 37.771980000, -122.454110000,\\n 37.768250000, -122.453380000,\\n 37.765920000, -122.456810000,\\n 37.765930000, -122.467680000,\\n 37.765500000, -122.477180000,\\n 37.765300000, -122.481660000,\\n 37.764780000, -122.493350000,\\n 37.764120000, -122.508360000,\\n 37.766410000, -122.510260000,\\n 37.770010000, -122.510830000,\\n 37.770980000, -122.510930000\\n];\\n let value = gpsData.indexOf(prevValue); \\nreturn gpsData[(value == -1 ? 1 : (value + 2) % gpsData.length)];\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"history\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":500}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"mapProvider\":\"OpenStreetMap.Mapnik\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"showTooltip\":true,\"tooltipColor\":\"#fff\",\"tooltipFontColor\":\"#000\",\"tooltipOpacity\":1,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
End Time: ${maxTime}
Start Time: ${minTime}\",\"strokeWeight\":2,\"strokeOpacity\":1,\"pointSize\":10,\"markerImageSize\":34,\"rotationAngle\":180,\"provider\":\"openstreet-map\",\"normalizationStep\":1000,\"decoratorSymbol\":\"arrowHead\",\"decoratorSymbolSize\":10,\"decoratorCustomColor\":\"#000\",\"decoratorOffset\":\"20px\",\"endDecoratorOffset\":\"20px\",\"decoratorRepeat\":\"20px\",\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"pointTooltipOnRightPanel\":true,\"autocloseTooltip\":true,\"useCustomProvider\":false,\"useLabelFunction\":false,\"useTooltipFunction\":false,\"useMarkerImageFunction\":false,\"useColorFunction\":false,\"usePolylineDecorator\":false,\"useDecoratorCustomColor\":false,\"showPoints\":false,\"showPolygon\":false},\"title\":\"Trip Animation\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":false,\"showLegend\":false,\"actions\":{},\"legendConfig\":{\"position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false},\"displayTimewindow\":true}" } }, @@ -54,10 +56,11 @@ "resources": [], "templateHtml": "", "templateCss": ".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 200px;\n white-space: nowrap;\n}", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('google-map', true, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.map.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('google-map', true);\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('google-map', true);\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true,\n ignoreDataUpdateOnIntervalTick: true,\n hasAdditionalLatestDataKeys: true\n };\n}", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First route\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.5851719234007373,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.9015113051937396,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.7253460349565717,\"funcBody\":\"var value = prevValue;\\nif (time % 500 < 100) {\\n value = value + Math.random() * 40 - 20;\\n if (value < 45) {\\n \\tvalue = 45;\\n } else if (value > 130) {\\n \\tvalue = 130;\\n }\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details\",\"markerImageSize\":34,\"gmDefaultMapType\":\"roadmap\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"useColorFunction\":true,\"markerImages\":[\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic7b13uB3VdTb+rrX3zJx6i7qQUAEJIQlRBAZc6BgLDDYmIIExLjgJcQk/YkKc4gIGHH+fDSHg2CGOHRuCQ4ltbBODJIroIIoQIJCQdNXLvVe3nT4ze6/1/XHOlYWQAJuWP37refYz58yd3d6zyt5rr1mX8B7S5Xo5/0nPYaNFM1PY0gGqOhfAgQCNBGlWFFUAYEIeihigbhFdZQwt85BV5Gj9r/718R2XX365vFdzoHe7w6d77xnPkn4YpAtU0YiizNJcmPNkMQFkDiSlowHt2HNtGlTSJ6B+pTpsKTfKgTj3Pi8SMtFtEZnFs8d8dPu7OZ93BcCHtt0+OiL+FJjOiqy5K5dtLwD4PBHGvy0dKLYo8B+1+lAldv50FfmFzWX+84i2M3a8Le2/Dr1jAKqCHtl2y1wC/pEMP9ZRLBaYzF8CCN+pPluUkOKfB6qlmk/dBwTyt8eOv2AZCPpOdPaOAPjA1h9/SJX+TyGXuz0TZi4EcPBeOk+U+RErZh2YyMAyQJEoZUjFgtkCAEScgDyx1hmInTglqDj2U1X0WILaPbWvwHO1WummeuLONhaXHTf2wsfe7rm+rQDe133j/i5xPyrmCr+OouhSKPbdQ5fLiezTIYUBQGMJBgYWxMYSISZhbxgQT8wGAgDiwWxUvCiBxKhSKOqdh4OyV5+6XiEfK/kjVOXQ13apG+I0+adKpXaG0/Si0yZdvPbtmvPbAuCNT98YTBhT/8fAmEpHoXgKgPe/6gFGP0nwG8s2YykcaRCAYYQ5tKTkDVuArDEwMRF5AICS4VZ1AQBSr6oEgL36CBAvlKqIsyLOKQl5TZH4uN+TawDuY6o64lWTJX20v1S633uJNvfmvnbRERelb3XubxnAX26+5gDy6Y9HtrU/wERff1XjSt0WwULDmZEMawPOgilgQ4FaGCEygaXQMQyRMaxiUijUkAEAImIGAFURAOrVA1AmI1ZExGuqoqkVFefhyGtKDql4X4eHc6LxJof0VIVM3nVc4uXaHUPlo0Tpc2fv/zer38r83xKAd6y74iImO31EMf9REA7cpdVBY8NbA5+dFNqsCTQipkitBjAUsLUZNd4qm8AyjDMmJAIRhDzDEBEbJkBVAyJWQJ14AEaciIeSGicOgBeBWNHEeXLkXIM8UvFI4bVBCVJNfdk7STd5xOcp0LZzjIqV/eXq/4i61edM/eaN7yqAqpfzf62Nf5LP5lbko/DbCuxU4saEN1mN2kKTzQbIkuEIEWfVagRDEVkOyXCkVq0aDg2p9YYNAySVerU0WN1R27Jjo6ulMQ1V+ggAOgsjNRNEus/IiUFnYUy2kM23AcrivXh2RiTxjhx5iSmVWEWdpmhQ4qvwSBBrXVPfqDmuVsT7C3aZvKslyZcr9dpxdr81F8ynO/w7DuD1q/8y6kDw2872ticN0deG7wvQHXHmdxGK+1ibQag5ikweliIElNUAEayNYBCSRQRiYzf2rNtx11O/rC5d9dj+1aQyM2Pyz3WGozaNisYNWY7SYtgWA0A5KUVO4qAn3t4+lOzYt+Grh+bDwstHzvjA2tPfd1Z+39FTRhGpi7VBKrE4nyBFDKcNJL5OCerqUEXdVeEQb0mk8lECjR0euxe9cqBUOnoQ6RkXT78hfscAvH71X0Z5kf8Z0dH2CgNf2NkI0d0ZbmtElMtFVEAQ5BFIlkKb00AzFJqCGooQcJjv7t868P3/ubayZvua48ZlJt57xLjjB/cpTssXokK7IQNrbeoZ3pIRJm1aYSUW9cwixglZ7xNU40ppY7mr+sy2ezt7G1s+vP+EGfd/+fS/Ko5pH9/pJK04X6MUDSRapcTXkXJN46QKp1UkqNVqvpxVyLzhOajihh1DpVkmrJ7+uak/bbztAF6/+i8j62p3j20vbgXR+cP3LYU/Djg/KcsdEnIWERcRIk+hzWtEOYSch2U76tk1T6+84Tf/NCdni2tOmbRgy6T26WOiKDBhGFEQhrBhiNAyjDGiQp4DFgI8AChg1BGBXOC9p8QJ0kas3jvEcUxxnLgNpTW9izfdOqGWlve7+OOXrThk6qEHKtKehq9xIlWkvoaYytrwFYqlglgrcZxW+oXSz+ycpOLmnsHypDTIfuTNcuKbAvD2288x22dn7hrVnt/ATBftBE/CH2aCtqkZU6CI2hHZomS4YCPK+5AKHFB2ZNe2Nev/739/e9qY3KRnPzHtQp/LtnfkMhnKZDMa2oDCTIjQhghDC2MCCQITAyYxpmkhAIAZDDA7l4bOSeR9YpLEwfkUjXqMOE0QN2LU4waq9aGBX6/+d7O9sXnu3579jbVTx02dlEilL0FDG1pJG64cJX5IGr6MupY5duU1npIv7sTQ4196ytUDx8+sf+TN6MQ3AyBd8+L8W0a15zYw0d8O3ww4vC7ijlkZU5QctVPE7QhNEVlTRNYUjHcy7tu3fuuVSqXBF8z66962fMeIfDaHfD4nmUyWsrk8BdaYIAh9EFoxzExEysYoAQ5A0ioAEIpIBGZmAM459iKaJo6cT209TnyjWkOSNLRWi1GtV9A3sGPg56uvG1vIZ9N/OO9rM8jS9oavSOwqaEhZYh3khq9K3fdpXWsbvdR3MoYCV/UOVadcOvv2C/AG9IYAfue5j1/U0R5mIhNctxM8yvxLyMVpOduJyLRRnto1MkXK23axlB27sXtT1z//8vqDTt3vk/fMGnX4xGyhiEI2Qi6X1Ww2S7lCIQ3DkCxzQEQKYADANgCbW6UHvwcRaO6fAwCjAewLYAKAcao6UkRIBEniEtRqNVOrVKjeSFCP61oaqurKvqe237P2lnkXn/X/PT9l3OT9Eql2V90QN1wZdRqSuhukhi9T3Q2s9ki+NDzHWppeUqnG/qsH/+b7fzSA33ruI7ODIDh/RCH6KkEZAEINfhia4n4ZO0KzphN5005Z06aRaeOAcjP++4Ff3P/86hWTLjr08i3FfEeurS3LUTanhVwe+XxOwjAw1loLoB/ASgBrAdSAV232Gc0NyJGt70+27mlrzNT6nAEwDcBMACO892kcx1KvN6hUqWu9Xka9XsfgUP/Qjcu+Nf3g6bO7zj7urBNT1F+quxLXfUkaMmDrviQ13+8THdqYqvuLZpfq+qrJNXFDbrp87t0v/cEAXr5iduiTMQvHd2QnKDC9+bC9NUfF9kwwgvNmBGW5Q3O2SFkzAkaCg/71Nz9+2MTZ6rlzLs4Vi0WbyWS5o63N5fM5G0VRaoxpA7ChBVw3ANMq1AKoHUAewCwARwHYvzWctQCeaNUrt4pvgeha17Gtevt47+M4jrVSqZlSqepqjQpVyyX/8xU3VBHF2T//+OeOFbgXaq5fa75ENR3SarzDxDToYz846FTORbPRV7oHG9sm+qEPX3TEM3vc9pm9AfiBP53+T6Pbwo0Cd4aog4p/yXK+lDX5IDIFZDinGS7CckEM+JB//u9/e3Z8NGPTgjl/Maq9s8N2FNtcPpc1bW1tFIZhaIxJATwFYA2AtAVWh4hERBQByIgIE1Gsql8gou8AeAjAfQAeVdUvEtE9reFFIpIloiyATgARgCqALQAGmHmUtTYTRWHDhhaGYE0YYmbHEXZj//rBRc/fXTly5qGHEus2FUceCbxP4DShRJ2mvuIFboyqG5kNcNuWVM965MbNd71pAC99+vADA+MnR6F+TeAg6h1TeE/I2bbAFjVLBbJcpIDzZNke8qNf//yxKblZWz42+9Pj2opFbutop7ZCQdva2hAEQZGZXwGwDEBDRCJV7VTVfVV1BDNPUtXZqnomER2tqi8S0REAzgJwUqvMI6JBAM+p6pdU9f1ElGu1E6lqUVVZVYWI6gA2EFFijJmSiUIPsDbXmGT3b59V6Kv0dd334uLGYTPmHK7Q7lRi65DCawqviXWSrEm1PlvgWMh9KPbut+/77Ohtj/97d98bA6igo7aM+O/Ogp0l8BNFPQhyY2RyE0MqcC7Ia2jyGpksBYj2//WDCx9uk/EDZ8783JhiW5HbigXpaG9HNpvNMXMGwAoR6SWiUKS5KhERS0QqIgmAHcz8sqrOA7AdwCcB9AK4CcBvAdwP4EVV3V9VPwGgC8B4Zv4PIqqoqgPQYObEOadExC1A60RUJaLxURQaZqoRW0NEsm/xgI6u7rV9L295vmvGlKmHQ32vk0QdxfA+oYTq+Vgbi70mR4p6BEaKlTid98S/9f4MV7wBgF/66AEnFbPUz+z/VNTBiywLgxxCFDgwGQqR5wznOeR8+6p1657r6uopfu7wv4mKbW0oFvIoFovIZDIBEXkReUlVG6o6Fs2N/EjvfSczj2Hm/YnoY6r6Ae/9w0T0cVXdSkTfE5FsC8iTAZwI4DAAjxDRj0TkUABTACxS1csAzG39MHlmzqvqGCLKt1xZA0Q0QERtQRBkDZMngrcmNAeMmB08uHpxNsrz2pFtbft4TWInDZtSLE5T8i7uSKRS8XDjBX4fYbnusI2jMkt/tGP9rnjxrl+gICP4Riagrzb1ssKa4CkrYRhwwBFHYGSUOZJKo8oPP/vCoV846opSoZCnQj7HxUJRMplMgGblR5h5wHtfbE1oZAvIHBFtVtX7RKTQ4pSrnHOXAThQRK4BcIaqNkTkRRF5UVUTVf1462/TVPVSEfm2974qIm3MvBhAl6pGAEYAaBcR45zLiUiPiDxKRC6bzZpsNhtGUaj5fIG/dNTltYeeWja3ltbVcGgMZX1IWbUUqDUBbBA+OYxDPuDLSORq6KsN76s48MvzZnwwlzNDgaFzAIBAi0LKtGVtEQHlOaQCQpOHoWDWL+9+ZODCuV99cnTbmM5cIY+2JudZIpronHukxUWemavOuZIxpuG9H8fM8wDMJaJHVfV0ANcDOIyIPg5ghTHm+0S0UETWq2oCoA/AI6r6C2PMgyKyD4BPM/MggJ8COIGIFqnqV1T1YADbVXUjEfUaYxrOOcPMBVXdCmCutbZirQGIlIBwavucl2577NaJM6ftO1nJ9aY+YfEpvDryknamSNdAMQ1AGwxdc/DqDjz9k/7Nw5i96ixBSK/MhTRxJ7oUbracmWAoVGNCtRSCYOxLazfcN7VjdjK+beK4KAqpkMtpJpNRABNVdT2AowHUvffjAYgxZpNz7hUiuk9VT1LVWFX/iojuBfA1IrpfVRcS0Xne+6tUX33+M/zdew8AzxljLvPefxTA3xPRIufcpQA8EYUAFhPRSCKaKSL7EFGgqjtU1RDRZmaeGIbh1sh78s7LxM59R09um7585fqNdtqUMZOMMc4igE0DthSppcYWL80VTNbyX1QCPgNN1fJqDvzi0tnjQviObGia3Ee0JEAml+E8DOUo4pxaE4GUJz3yxJr9/vSIv+8uFAu2kM8jl8vBGNNJRE+q6grn3AZV3QRgi6q2AZjHzHNE5FEAp3vvv8HM8wFQSywvADAPwDgAi0TkPwDcBWDhcFHVh9FcXH9ARE4BMI6ZvyEiHwYwSVW/CeB0IlpERJeo6hwiepmIlnrvVzLzemZex8yDzDwZqlUikGGm6R0H66+evuPYafuNynvFkCCF4xjiBd67otN4C4GmEDAqTuVnR3++beWT/z5YfRUHio8/0dEe7DynJTUvswmmEiwxWcCDwGyee37j4ydNO6ucy+YmZMJQM5kMWWvHqmqPc24eADCzENEGAMvTNH2AiM5Q1W1E9GkR2cLM3yOiS0TkO0R0lao+zMy/8N7PBHAmEZ2C3YiIoKrdqnqjqq5i5j/x3n8bTQt8iapeKyKbjDGfFpEhAGOccw8EQdBhjPmQqk723rP3PrTWvhxF0Xgi6vHeayaTyx075fS7nlvxcPGgg8ZNIjHeSKRMdbEUIEHwEuCOA4DOvB25vSRnAfghMGxEFNRb7ZoM0HFNadFeIjvRgMFkhEDKbEl8Oqq7u3bs+/c9cXQUWo2iCGEYsqrG3vvHAPwEwL2qulZETnXO/Zm1FqoKVf2Bqh6qqr8SkW3e++tU9T4i+ntVnem9vw7ARQA6ReQ5AL9yzl3vnLsewK8APIfmovkiIrpWVWeo6t977x/w3l8nIluI6Dcicqiq/quqgpnJOfdnIvJR59wmEVlCRD9S1QeJKLHWmmw2hyAM9bhpp47q7q4d733aSVBlkBoNQGxgYPdVRZ82N5In9lS7dp42GgA483hMyUY0RXgwXzAjQgUtshp1WhOR5YgDzoiB0U2baqsPLB7z0oxxBxWz2Rxls1lh5gNVdbn3/rwWR68moi5VPZWZt4nIvgBGquoRAH5BRH+OprH4oYh8XlVPQXMvfIOI/BJAFxF1qupxRPRBIjpKVSe3dOtdInKbqj5PRIe3RHayiHydiMYDOIuZfyIin0HTfI4kIgAYa4y5UUQaAI4QkY8ZY5YR0aGq0kcE8k5NNS4t665u6G9r47xDCi8pqabsNbFe9WkoRvU0upYl8GunnqebX7kZQ00O9DipLbKjRfQTPWnXYyBTBxMBBiIML2IVkt20sf6B46d9rJjJ5chaQ0EQRAC2pWm6VlVXq+rZIvIXSZKELcX/Y1U9RlW/AWC8iJyqql9V1aOcc99W1SXMfAmAh1X1qy3O+rKIHCMiGRGptUqude9iIrqWiC4brisiDxHRt1X1KFX9qnPuowDGe++vUNUPishNLQkIiOjPVPVs7/02EVkLYHsYhtYYg0wm1FNmnZPftKF2lFPJisCIkhE1DFiFaNLr1i5R+PntGR5lFMcBLWfCxxbhrgkjgqMAjCKgkrWFX48KZ7RHJm8CziJLOXJpUNu4omAuOfbKOMxkKBOGHIbhHBG576qrrtLHH3/8QmaOdtdd/5tIROLTTjvtyc9//vN3BUGQs9aOA3CyiDxXr9dRrzfo2gf/Ljt1TpyYIMnWtQ4nVW2kNd+bri41fOlMADkQerb1p4/f+WGcaS9X8HOLUQIwCgCUdFGi6ehBt7k+3k4DqQ8cOd2+mQdPnP6xijHB+MAYhGEoqppL03T/J5544iRmpvnz5z+4Zs2a1dOnT5/+8ssvr5o5c+aMWq1WSdM0VdXORYsWHW+tXXbmmWcONV2jQG9v744dO3b0jR07dvSIESNG3HbbbbNFpHPBggWPtMTvVUREWL58ee2VV145bcSIEU+ddNJJ1RY4unLlytXTpk2bEoZh2N/f37dw4cKTrLUdxWLxvnnz5pnf/e53unDhwhPa2tpWnnfeecekabopCIIMEYGIyBjGCfufvmbpltuKY6a4LKkzCh8PpZu913g0oIsAOhOKMQTElyvYPrsY43IRP6uK8wCAYHrUo+gpiXoaG+LR0X5VaNgxNEAHz5pz6PIgMGBmBTCKiJZVKpUjjDEmTdPG/PnzPwSgLCJHoLlY/omqXgLgWSJauHjx4uNPP/30obPPPnsAwGNoLl+O32Xdt/a3v/3txnK5HM6fP/+3aJ2JAAi89zkAUwGcdOqpp+YvvPBCnH322fEJJ5yQA3CH9/5YY8yft0C+SkTmP/roo72NRqPjhhtuODCTyRTPOuusRy+88MJVd9xxx8cWLFiwiog+oqp3ARgVBMEO7xVzJ70/v2jdHbNGqu/16uq98WakmuQgANhsU98MRQwMP7N0iYxhUuybD/n3WzqlAMROROElzfY3NrXHrtTNFHTkMvkiGQNiZhGZ7ZzbPDx5IoKIXK2qZzDzd9F0T/0pEV2qqoeKyN8BwLZt27ap6hmq+l0RmQXgZhH5iohcpaqrwzA0RATn3DXOueta5buqeoWqnqWqT9dqte8DwPbt2zeKyBGq+l1m/giA7wL4map+jYj2S5LEA0AYhp0AvsvMp5577rn3Axi/YcOGxaoKEdkCYBYzqzGEMMgUWILRjXSopzfekFUf5wUKYXYQCoZhykcM08C+DMUMw7Rva8sHqHZCJFD1VtTDaYLuoe3xrLGH/Yu1NiZVtcYAQEVVy7vpmPNU9VHv/RUArgZQ9d5f473/qYj8OwBMmDBhPIBnnXNfAfAj59w5AK4F8DURmcfM1JrY/4jIrSJyq/f+XlV9vmVMPlEoFC4GgM7OznEicmPrB3hJRC4Tkc+IyI+897cFQWBay5lrVfVKVX30lFNOOUZV/aJFiz7YMi79RFQiIgbg2NrazHEHf7+70q1eGiwkROoteQkhOmIYp8DQBGUcYIVwOJMepCCAkBCooCAnUPVwXoU1rrXVoyi7nwgoDO1QyymwzTn34d7e3p8B+NsWFx4AYLP3/l4iuoKIHhaR/yaiLw1z6rp169Z57+cR0bUiAiIaVNU7ReR5Y0xcrVbPbf0ek1U1DwCq2qOqG4jofhHZUi6XAeC7IkIAvqCqIKItaG4LZ4jInxERvPevtK5fY+b7W+0eBGD78uXLx6nqd51z85i5G0Bore1rNJJsxuan1EumFo3w3mtKSupAMASNRJEACBk6ixWphWCaKs1tqegVUIWyiBcPIYhRQlLKhQccNDtW9YEIh0TkiciJyGFtbW29LfCCxx577PtHHHHEhdbabd77bzLzFap6jPf+X5o46Jf333//qWh6kP+P934HMx8F4HQA53rvkc/nl9frdYjIQbsw99SWy6opPvl8BQC6u7u3ENFfq+poVb1IRK4iIvHeX7dy5UpKkuR8Zka9Xv9WNps9n4j2B/DNkSNHnrV9+/ZRIvIhIjpMVZeoqlfVEcyQ6WNmpQ8+nyva9m4IO/XeQ1XFE6UKfYkUhyrTEVDEFkAWO4NuZAuAsPnDKlgFzih8ku0cU5y4NQiCxFrLAPYDUCOizxpjrgAAY4y54YYbvtwS5f1E5B9UdSgIgloURR8BIESEO++8c8qmTZtetNYeHYahdnR0wHv/pIhsrVarvX19fQsA5H71q1/dYq01pVKpkCRJXCqVaGBgwDcaDdfX1zcRwDELFy788JIlS96XJEnBOQcADSIKmfkSIsKwpXfO/bmItBljLlHVa6dNm/bIE088sR+AMUT0WRG5kIgmWWtfIWPcuPZJDJ9r90hIRVTEq5KAlBIIdYH0UCg6FMhZUvDvjSDVnZBhUhUSUijICxHCbDFXZGOMqKoH0KmqQ/l8/ptdXV0/rlar38rn8zs5hJmJmUM0jyPb4/j3h/ze+ylLly6dgr2QaepX3Hnnnefv7ZmdoyUamyTJWABoHvTtmbq6un4xa9asSQCuA7DSWvtSo9E4zHt/dbFYvKLRaKwF0E5EwoBENlKVMOPFkcJDCRBVUlEloLQTLgWz1987FAhImCECJVEh8Z6cdzBk20ITkIg4Y4xX1ZFoHuJM3XfffT/S29uLLVu2oFKp7HQ9/W8ia+2RzHyGqv6TiPzjsccei97e3kxbW9uZACYTURVNb7mIiIYmJIOwLUWqTqQVIqFEDFHV6nC7orDMBB22LOzhWbRC0LJRLalqGYqyQWAJVDPGVJIkqQPYrKq9AGCMmQoAaZpix44d2Lx5M/r7+5Gmbzn4822jVatWvei9/9M0Ted77/9j5syZawAk27ZtswCgqt0AtohIzRhTssZWDdvQkA4RtETaxAOqZSWWnXgR1Kr8/kTbG2ThtaAE9QQSZWIQ2EilFteyhoJCa4lxYMvf9xry3qNUKqFUKiEMQxQKBeRyudcVsXeC0jRFrVZDtVrFzTffnOnp6Tl2/Pjx944ePXrt9OnTzyGirY888sjLCxYsOERExhPRDGvtswACrz4m60pOqIMIBIX4ZqCYAWsZLXumAtid6z8A5DSvlgkKFkcMiBERqHUDiUu8994SkQCoEFF+jyPfhZIkQX9/P/r7+xEEAbLZLKIoQhRFbzugzjnEcYxGo4FGo/EqCejp6Tnv5ptvfk2dH/zgB8sWLFgAVS0CqHjvyTlnq2mFYF3VORnJICKwI2IFI0Qi7TCtLaYCVgnbAdoA6GRhaoPXhipIVJkEUCXP7CrleBAd2RHsvYcxpopmfMreaICZN6LpQWYRmZSmaeeuk7LWIggCWGsRhiGstWBmWGuxqwUFABEZ9ilCROCcQ5qmcM7BOYckSYbd/XuiTczcT80YHHjvZ6MZZ4O+vr5hx+14Va1Qa/M9WB0Asa+SUCcIRuAtg5QEBKDYrEJrwdhiIXhBRQyIJkMxQxQvkELh4RUq4kCJ2VHdOLiOx+YmmTC0trWwnQOgsvtoiegFInKdnZ3rRo0aJT09PTw0NAQAm0VkzvBzw5N/B0mMMU+pqhk7dmxXsVjkzZs35xuNhojICDSPRpPt27c/WSgU5hLRC95722g0aOPgWnbcW5VUBYCSJYBBChgQzWnt2J4BsJyheFkVr7Q6Hc2kZYU6ARSejCjZFN259UOrc6reOucMEfWpqnXOPQIAhULhN8PgMXNl3rx5Y4IgOIuZz46i6KyTTz55JBFVmXnFO4nYrmSMeTKKooEPfvCDs40x8621Z3d2dp566qmnxsxcArC1s7PzkVWrVi1X1QBAv/eeiYg2DK0upOgpiCBQIlIBBOrBOgTCCAAQ0jUQrGS1WF1vUPewLlTlKoQCOARewOqVUgzmtlXWTWuKiqiIVAAgjuOtuy1bgtNOO21ET0/PhO9973sQEXznO99BT0/PxJNPPrkDQAO/97C8k7RBVaO5c+ce19nZmb3yyisxZcoU/NVf/RVWrFjx/kMOOWQ9M3dXKpVRjUYjbKmGinOOnPPYWt04PZGhjHoQCZigAQsFpFwbxqlRpx6k6LI6gK5Kpz8zm20d0JHWQFAYTSUlALDexSNdEB+Y+nQxpZRlppSZ4ZybdPvttz9QqVSOt9Y+SkR+xYoVxx522GF4/PHHceCBB2LZsmWYPn06nnrqqQOZ+REiekZERr+T6BFR37hx47rWr18/NwxDvPLKKygWi3jhhRdw5JFHolarzXvuuee60jSdYFordxFJnHNI0rghiGc4jb3xUDEQEngyYEBrwx7KcuJHZzux1t79KZQ++iv5AHTnCadVBZGQhULh1SsIMfoe7KlsGRqTm5Q1xmkQBJtV9dijjz766f06bwAAEgVJREFUnpUrVy4EgIMPPjh300034bjjjsOaNWtQqVQgIjjqqKOwZMkSzJs3b/Xy5cstgFUA3rZF954cr6eccsrYxx57DJ/85CexcOFCDA0N4cQTT0S1WsWjjz4azp49+4l6vc5Tp049TVU3eu/hVXVbZUN/TH33k8c4DVRIiMFEohCjCIdXLC6VY+44DV+zACCEXiiWgnCkEp1EpKsEqqTEIsTq1Axg+eCy/kczp+QmqDZfuXpRVedNmjRpx9VXX32hiEBEsHTpUtx5551YsGABnHM47LDDcNNNN+GAAw7Al770pc8NPzdsUXe1rsOA7n4dBmjXK3NzgbHrZ2beWQDg7rvvxq233oqLL74YS5YswY4dO/Dkk09i7ty5uOCCCz4bx/FPRGSUiNydph71ap2W9T9eGGgsr4iqZSVVsLJ6Z5lIlU5srfmWAlgHtE7lDjgP5SjgAWb6MBTtoroMgpwoERTwniiJhwq5aPrxB+YOWwuQIaKEmWd573NBEHSoKosIpk+fjltvvRWqitWrV6O7uxvLli3DV77yFRQKhVeBtzcgd/2+exmm3bl3dy4kIowfPx4LFy5EpVLBpk2b0Nvbi+7ublx22WWw1ro4jgsARgJYVq/XUG/Uk2fK95+ypXxfrESGGUIEMhYGTP1ovQOYOr2+kcjvVt+K9c130cp4slyX4nDnBqYbRCAGkTZXUELIVtPeezeUu3rjOEaSJFDVpwEcmKbpLcMTnDhxIm644QYEQQDTPDvBNddcg3322ec1IL1e8d6/qryZOruDffTRR+PrX/866vU6kiTBAQccgOuvvx5hGKI15hki8lTz76lura/fUUt6F4siJIKCiREAakhB6BnGp1ST9lwbngJ2CfE99Zd4cPzIcDqg4xl4wQl64EE+BlyicCnYanHz4RMumviR9vO7C4UC5fN5JqKzVfXlKIomtzzGr5nwGwGwOxe+ngi/ntjuXowxe/s+0Gg0+ohofxG5o1KpoFqv6+LBn496dssPt6dcmWAtlCOCNRDKgJgxEopDoLRl60Cy5p5P4Hhgl/A2NbgmTuUGBeCBOUTokVZAtyiIFJSk5QmJlJKeyvaeer2u9XpdVPVxVZ1Zr9dv25PI7Q7M3sDbEwe+0Q+wt/b21vdwqdVqv1XVaar6eJwkqNdj9JY3bW9IKU5cZRwUDNPcuagBE2G7Kg5RAKnI9SD832HcdgJIARYOVdyknXtjoTpBoaRsTPOMHQy7fMutQy/qQzOr1arW63VNvd+kTc/NfO/9I3vTXXub0N5E9/U+v57Yvp7+VFWkabpYVc8DMJSm6aZyqcSNRk1fxOMHPb/5v+pQtWwgUBCxErGCiOJhXHYMuRkU4r7XAHj3aYhTAaC4rakI9dNkMMSWPBhMSsRKmjRKIyuuZ3Bzfe32crnGlVJJReQ+Vc3HcdyuqgPD4re3ib1ZHfhmVcDuYO4JxNaYetI0HYvmMen91WqVqo1YNqVdW2uutz9NSp3KTNpcxMEYgjEYVNULmvVxiwLVu09D/BoAAcAZXL6j7F9SBVRgiUwPkRJYCQaqrEoMWrrqp4WN2ZfmxXGtWq7UqFwuJyJyP4A5cRw/qKryelywNw7ck+58I336ZvtR1Uaj0XgewMEicl+5XPblcpXqtXJtk33x1KUr/6MAbnKdgQKsDFUVMTtUYFWBvpLvohRX7orZqyJU192K6tSz9Qv5HPcQaCpBZyvjRSiyEFIVkDioiBbL1W3LglGduWJ9LKDExnAtCIJEVU/w3t/MzIfsbiD2dn0jHbkrF+1qSPZkXHY3MMNX59ydaB5ePdNoNLZUqlVfrpSxOvO4earr5xvqvm8iGfggBFNIyiGYQwwQ4xwABqqLhmo+c885eJVf7NUx0gDE4iv9Q/JYc1+MDABvDJQs2DDYhlBmxD2Da6YNxOulW9dsr1TLWiqVtF6vrwawXFU/7Zz7TwB/FCf+MUuW1ylJmqY/F5GzVXVZvV5fWy6XaahU5q26asuA22L7hlbvR4a8NVAYKFsgMBACJZDm7mNHSZ41HpfujtdrovS7bkV58p/oRwpZ8zIIhwM0C0SLoBipCmqNnaHAhq3L7MT9D9mfhjIrrYRt3nu0fG9VAKd673+Npq8t82a5cW9ADdOb4bZdljfbRWSpNt9BeSJJknVDQ0MYHBqiwXRHd9+IriPvffpa4YBCE0I5grCFMRlSGFoF4DMt3ffDUtXLPfPxyzcEEADGnoNH01gWFLNmChQhgTJEOqiKQIQEAiPNU09+Zf3jfZNnH3yY9mVWasoFL16sMWVm3gzgNO/9KiJaq6qTdlfyewNv9+f+QNCGPz8qIgLgaFVdVK83egcGBk25UtWBel9f/4Q1x931yFUbYLWNIxgOoDYgDSJYE6IB8CEEjFKg1D2QdscVfHn9r/EaB+YeAdx8B9z0+Sgz8HxgeR6AMVB6hgzaVMk3Q/2JSQHvJOra+GTXlMPmfEi6o+d87NpTLyTeN5j5ZWae6b3fV0RuIaKZqmr3ZJ33BNzuAO4G0B7vMfOQiNyqzcBN8t7fN1QuN0pDJVQqJe2v9u2oTt9w0l0P/uNz3iQjghA2CMmEGXgOCSYDIqJuAk4AgHrDf7We6u/uPx97zO6x13fl1tyOtfucqRcXM+ZFAHNAmA2iu4gwRkBKos0jAVXy4vKvrHvslWlHHHZk2m1eQKJ5VfXOOauqG4Mg6FXVj4nIalVdpKoHqSrtsrzYed1VXAHsDaQ9caAQ0S0iMoqIPkBEDzWSZHWlXI6HBkvBUKWsQ2nf5uSA7SfeueTqFxPUxtpQAxMSmxBqAhKTBZhoBYALAUCBW3ZU/D6Lz8E1e8NprwACwKQv4nf1fvlUMWsJwEgC5oDpIVJ0EhGrJ6sAICCXuvYVqx8uzXj/YZPSWFbWelyHeA/nPRLvqwxa3XRN4COqugrNKPwx2ozifxVww1y3K4CvA95WAHdQ8xWHDwJY4b1/tlwupwNDVVTKQ9rfP6j19h3dsv+Ow29bdEWvUmO0CWBshowJCTZL3kQAW1pPTb1noPTK9oG0no7Cp9b/7LWi+6YAXP8zuMnn4rFG4kfnQ3MYgIgIU5jxDCmKCigBpE1xZlEfvPDSErffrFkU7BNQpSutxQ1PLo6zSerFi9RV/CvMXFXVQ1R1H1VdhGaIbxnAzgQ5u4vtLsUx8yMA7mPmbQAOJKI2VV2XJMlLtVqtViqVaLBUlUqpn0vloTofOhBVMptzv1h4dd4Yn7cR1GSJwwhiQhIbIjUBthBwJoC8ElzvUHqzKL5+/+l4zQuGu9Kbyplw4m04Ix/xjI68+W6r2gZifdI1dFSaEEtdOW2AJYG6hnqXEMaOnL7ptGO/+L5kjVks2/JjM5nIZKJAoihLmUyIIAjIGANjTEBEHSIyWUQ6RWSdqm5V1YqIpC3RDImoQETjiGgKM5eIaKOIDKpq4r2Hcw6NRgO1egzvUq3V6l5Hxhuys9OPP7T0lke7tj41nQNiG0FtBmojeBMR2yzIRNhKQh9U6L6kkMGq/7t6Ii8uXoDfvRE2bzprx0n/hc93FLiQi8x1zYq0CdAHvcdkV4V3Dupi9b6OgosR+wRGvU3PPuXSHcXcPiMGnvAvcJIZlwsjG2UzMESUzWa16SExZGxLGFS9sVbFK5SUAGBYWYoIMzN5BbnUgSCaph5xXCfvvSZJouVaw1NWejrfL3NK1a07frHwmpFsXcgRvA3hTRahNeRsHmKaXpZtIDoa0P0AoBb7SwZqEt+/AP/6ZnD5g/LGnHwbvtlZCAYzAYbzJwwo4U5xOl0aUB8jcDHUxUSuoQ4pJE0gmbCt9vFTLm4UM2NHDCxNlidDweiQOAyCUDkwFLBBEFhSZrVEqkDzHLEVAiA6PFBFE0pFkjhS9YjjVJ1Lkfg0sZ3SO+rI8NBSo7vvznuuz8S+lDMhwBbWhmRtVr3JgmwAmAhqAlolij+h5svfqMW4ZKiaFu49F1e/WUz+4MxFJ92GS3MR246M+bYSGEAizD8mJ4d6p+oa8L4OcQnUJzA+hhWnqU+gUdA2cPKxnylNHj/rmOrW9N7+F5JGOiQjyXIYcgC2zRejiVXFw5Np5Y3xMGxgxBMJPMSlFHtPUI1NG/eNmhNm8uODUzZse+nB+x78WVs9KXXaDMgYspyBNyG8iQATwIRZwIawYPOCQj4LICSFDNX9V6qJ5O5bgH/8Q/D4o3JnnfhzfC6yvM/IdvPXADpaLd0KoaJPNS+xmjSF1QYkTeEkVfYpGR8j9Q5WRKvjRkztPf5DC3j0iCkn+AQvlDdUu6rbXaPWn5KrCEEErTwXTTKALbDmRgSaGxNk26bmppoQc7p7ux546PE7ZHvfutHGUJ4DOGMRmEi9sSQcwgYR2GTgOCRvDFXVaJUU81sA9PcM+X92Trru+yT+8w/F4o/O3nbyrTiaGF8cUwgOIMZRreZegerDgB6YJiQSw0uqgYsh3sFrjMB5eE1gfAovHka9pjaM+ke2TxiaNnWujBkzOcxnO/KFXKHNBpnRAODSRm+lVh6q1odqPT0bkjXrnuW+oS3tLo1HsKGADIQDsAnhjEFAFgmHsDYCmYBSG4BMRgMQvQTQcYBOBwBVPN5TStd6hxvuPx9L/xgc3lL6u5N+hpGwuHl0u33a2N/nDiTSXxBIRHWCNMilMdQ7DSVF6h1YUxXvyKhD6h0CCKCCVLxa9YASKYlyK/AOIJAyCUFBDGImB4KlEEoMbywCCtQbQ8QhxFiEJqDYWLDJakBEm4g1UKFPDI/Rq16xY9AdZQzOXzgf/X8sBm85AeM5t8P0eXwtItYRbfZToOavCyDxKj81RCPgaKJ3iL1TAw9xCVgdvHcw6uBVm/pNvQIKpwJV2pkKBQCEFKoMYoKFITVGQQxPBsZYeLIwNoQQw3BAjiNEzNioQKzAebQzkJRW9lXcbXEqctx5uOryYUv1R9LblkP1+JsxjS1+MDJn7wkDuhKEHACQQqD4OUgExJPFq/EpqTglcXDqEXoPJYETDwbgROBVAQY7ABCIJQKYYQBYZogaWGMAMkhhEJiQPLMaG5BTlvWUsgXjvJahAxS1RqpfH6i5eYjxhfs/i7clj+rbm8VXQSf/HB8T4LOj2uwzgaF/0GZ2oeHuVqjq48zIQzHee4QiSLUZgwN4kDYdt0Kkqq38BM1XhYnAMMwKGDQ979y0rERIRbENQJWIPgDorF0m2Ei9Xt0/5N4njH+//zzc9XamRH5H0iAffiOC9gLOVeD8kXl7bxjyxYC+OqMv0VaoPsCEukAigNqg1EEEFlWBQKHUFC9SBoOYiEUhRDoIaInBiSgyBDpJoeN2m9qG2Mv1/SV3iir+s1zFbc9chLc97vgdzWR+uYIfugUnC/C3keUlHQXTaQiX7LUCox9en1XwIBENCqTcvM1FVe0gSAcMzYVgxN6a8IrrBit+IHFyrCF850Orcf/ll781Pfd69K7l0j/mJxhtLb4+ot2uDy3t1T30Vihxeml/2U1WxpVLPol3PA088O7/MwI6/ib819j2YDOb154vvBVSxfXdA+nEBz6Ns4G3T8e9Eb3mUOkdJsW++NT2UjpHVO/V5vrvrRfVh7f3pTNLdZyLdxE84N0HEEtOgMsRzukdcBUV2vRWwYOnbTuG3HZXw4J3wki8Eb2uQ/WdojW/RLz/n+CluKaZTMhzm4eJwB9aFHADFf1X7+X6h/4MG9+LubzrHDhM934KLyhoaSPB3/yx3Nco42+811UPfBbvWvD67vSu/0eb3enEn/K17RkeNExXvPHTvyfxeuVQQ0be9zn50hs//c7Re8aBw3T/Z+TScl3niuBm9cCbLLeXGjr3mA3yl+/1+N9zAEHQ6oA/rxLLBPF49o1Fl54vxVJ08Ge/kwvkN0vvPYAAHv8K6ur8BbVEnlNF6XUArNQS/ziJv2jJ5/Cm07W/k/SeWOE9UddvUJ5+pimpYhODTtyT1Y29fsOrv2fxhXj+vR3t7+l/BQcO0z2fc0ucEyeil+7OfV7xFYXI4gvx4Hs9zl3pPbfCeyA67cfmFiaziVX/BgCUcL1XGf27z/vz8S7vNN6I3t23oN8caW0//+lcF/0PC+4VIBJgZm2aPw3/y8AD/peJ8DAtOQEuZLfAQ0sK7Q0rbv6SE/Yen/L/017ojH8LZ5/xb+Hs93ocr0f/D6s769KBP+5xAAAAAElFTkSuQmCC\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"useMarkerImageFunction\":true,\"colorFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n if (percent < 0.5) {\\n percent *=2*100; \\n return tinycolor.mix('green', 'yellow', amount = percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', amount = percent).toHexString();\\n }\\n}\",\"markerImageFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nvar res = {\\n url: images[0],\\n size: 55\\n};\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n var index = Math.min(2, Math.floor(3 * percent));\\n res.url = images[index];\\n}\\nreturn res;\",\"strokeWeight\":4,\"strokeOpacity\":0.65,\"color\":\"#1976d2\",\"showTooltip\":true,\"autocloseTooltip\":true,\"labelFunction\":\"var vehicleType = dsData[dsIndex]['vehicleType'];\\r\\nif (typeof vehicleType !== undefined) {\\r\\n if (vehicleType == \\\"bus\\\") {\\r\\n return 'Bus: ${entityName}';\\r\\n } else if (vehicleType == \\\"car\\\") {\\r\\n return 'Car: ${entityName}';\\r\\n }\\r\\n}\",\"tooltipFunction\":\"var vehicleType = dsData[dsIndex]['vehicleType'];\\r\\nif (typeof vehicleType !== undefined) {\\r\\n if (vehicleType == \\\"bus\\\") {\\r\\n return 'Bus: ${entityName}
Bus route: ${busRoute}
';\\r\\n } else if (vehicleType == \\\"car\\\") {\\r\\n return 'Car: ${entityName}
Current destination: ${destination}
';\\r\\n }\\r\\n}\",\"provider\":\"google-map\",\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\",\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"draggableMarker\":false,\"disableScrollZooming\":false,\"disableZoomControl\":false,\"useLabelFunction\":false,\"useTooltipFunction\":false,\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"showPolygon\":false,\"showCircle\":false},\"title\":\"Route Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('google-map', true, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.map.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true,\n ignoreDataUpdateOnIntervalTick: true,\n hasAdditionalLatestDataKeys: true\n };\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-route-map-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First route\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.5851719234007373,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.9015113051937396,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.7253460349565717,\"funcBody\":\"var value = prevValue;\\nif (time % 500 < 100) {\\n value = value + Math.random() * 40 - 20;\\n if (value < 45) {\\n \\tvalue = 45;\\n } else if (value > 130) {\\n \\tvalue = 130;\\n }\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"google-map\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"gmDefaultMapType\":\"roadmap\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"useCustomProvider\":false,\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"mapProviderHere\":\"HERE.normalDay\",\"credentials\":{\"app_id\":\"AhM6TzD9ThyK78CT3ptx\",\"app_code\":\"p6NPiITB3Vv0GMUFnkLOOg\"},\"mapImageUrl\":\"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==\",\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"tmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#1976d2\",\"useColorFunction\":true,\"colorFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n if (percent < 0.5) {\\n percent *=2*100; \\n return tinycolor.mix('green', 'yellow', percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', percent).toHexString();\\n }\\n}\",\"useMarkerImageFunction\":true,\"markerImageFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nvar res = {\\n url: images[0],\\n size: 55\\n};\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n var index = Math.min(2, Math.floor(3 * percent));\\n res.url = images[index];\\n}\\nreturn res;\",\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"strokeWeight\":4,\"strokeOpacity\":0.65},\"title\":\"Route Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" } }, { @@ -72,10 +75,11 @@ "resources": [], "templateHtml": "", "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('openstreet-map', true, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.map.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('openstreet-map', true);\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('openstreet-map', true);\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true,\n ignoreDataUpdateOnIntervalTick: true,\n hasAdditionalLatestDataKeys: true\n };\n}", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First route\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.5851719234007373,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.9015113051937396,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.7253460349565717,\"funcBody\":\"var value = prevValue;\\nif (time % 500 < 100) {\\n value = value + Math.random() * 40 - 20;\\n if (value < 45) {\\n \\tvalue = 45;\\n } else if (value > 130) {\\n \\tvalue = 130;\\n }\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details\",\"markerImageSize\":34,\"useColorFunction\":true,\"markerImages\":[\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic7b13uB3VdTb+rrX3zJx6i7qQUAEJIQlRBAZc6BgLDDYmIIExLjgJcQk/YkKc4gIGHH+fDSHg2CGOHRuCQ4ltbBODJIroIIoQIJCQdNXLvVe3nT4ze6/1/XHOlYWQAJuWP37refYz58yd3d6zyt5rr1mX8B7S5Xo5/0nPYaNFM1PY0gGqOhfAgQCNBGlWFFUAYEIeihigbhFdZQwt85BV5Gj9r/718R2XX365vFdzoHe7w6d77xnPkn4YpAtU0YiizNJcmPNkMQFkDiSlowHt2HNtGlTSJ6B+pTpsKTfKgTj3Pi8SMtFtEZnFs8d8dPu7OZ93BcCHtt0+OiL+FJjOiqy5K5dtLwD4PBHGvy0dKLYo8B+1+lAldv50FfmFzWX+84i2M3a8Le2/Dr1jAKqCHtl2y1wC/pEMP9ZRLBaYzF8CCN+pPluUkOKfB6qlmk/dBwTyt8eOv2AZCPpOdPaOAPjA1h9/SJX+TyGXuz0TZi4EcPBeOk+U+RErZh2YyMAyQJEoZUjFgtkCAEScgDyx1hmInTglqDj2U1X0WILaPbWvwHO1WummeuLONhaXHTf2wsfe7rm+rQDe133j/i5xPyrmCr+OouhSKPbdQ5fLiezTIYUBQGMJBgYWxMYSISZhbxgQT8wGAgDiwWxUvCiBxKhSKOqdh4OyV5+6XiEfK/kjVOXQ13apG+I0+adKpXaG0/Si0yZdvPbtmvPbAuCNT98YTBhT/8fAmEpHoXgKgPe/6gFGP0nwG8s2YykcaRCAYYQ5tKTkDVuArDEwMRF5AICS4VZ1AQBSr6oEgL36CBAvlKqIsyLOKQl5TZH4uN+TawDuY6o64lWTJX20v1S633uJNvfmvnbRERelb3XubxnAX26+5gDy6Y9HtrU/wERff1XjSt0WwULDmZEMawPOgilgQ4FaGCEygaXQMQyRMaxiUijUkAEAImIGAFURAOrVA1AmI1ZExGuqoqkVFefhyGtKDql4X4eHc6LxJof0VIVM3nVc4uXaHUPlo0Tpc2fv/zer38r83xKAd6y74iImO31EMf9REA7cpdVBY8NbA5+dFNqsCTQipkitBjAUsLUZNd4qm8AyjDMmJAIRhDzDEBEbJkBVAyJWQJ14AEaciIeSGicOgBeBWNHEeXLkXIM8UvFI4bVBCVJNfdk7STd5xOcp0LZzjIqV/eXq/4i61edM/eaN7yqAqpfzf62Nf5LP5lbko/DbCuxU4saEN1mN2kKTzQbIkuEIEWfVagRDEVkOyXCkVq0aDg2p9YYNAySVerU0WN1R27Jjo6ulMQ1V+ggAOgsjNRNEus/IiUFnYUy2kM23AcrivXh2RiTxjhx5iSmVWEWdpmhQ4qvwSBBrXVPfqDmuVsT7C3aZvKslyZcr9dpxdr81F8ynO/w7DuD1q/8y6kDw2872ticN0deG7wvQHXHmdxGK+1ibQag5ikweliIElNUAEayNYBCSRQRiYzf2rNtx11O/rC5d9dj+1aQyM2Pyz3WGozaNisYNWY7SYtgWA0A5KUVO4qAn3t4+lOzYt+Grh+bDwstHzvjA2tPfd1Z+39FTRhGpi7VBKrE4nyBFDKcNJL5OCerqUEXdVeEQb0mk8lECjR0euxe9cqBUOnoQ6RkXT78hfscAvH71X0Z5kf8Z0dH2CgNf2NkI0d0ZbmtElMtFVEAQ5BFIlkKb00AzFJqCGooQcJjv7t868P3/ubayZvua48ZlJt57xLjjB/cpTssXokK7IQNrbeoZ3pIRJm1aYSUW9cwixglZ7xNU40ppY7mr+sy2ezt7G1s+vP+EGfd/+fS/Ko5pH9/pJK04X6MUDSRapcTXkXJN46QKp1UkqNVqvpxVyLzhOajihh1DpVkmrJ7+uak/bbztAF6/+i8j62p3j20vbgXR+cP3LYU/Djg/KcsdEnIWERcRIk+hzWtEOYSch2U76tk1T6+84Tf/NCdni2tOmbRgy6T26WOiKDBhGFEQhrBhiNAyjDGiQp4DFgI8AChg1BGBXOC9p8QJ0kas3jvEcUxxnLgNpTW9izfdOqGWlve7+OOXrThk6qEHKtKehq9xIlWkvoaYytrwFYqlglgrcZxW+oXSz+ycpOLmnsHypDTIfuTNcuKbAvD2288x22dn7hrVnt/ATBftBE/CH2aCtqkZU6CI2hHZomS4YCPK+5AKHFB2ZNe2Nev/739/e9qY3KRnPzHtQp/LtnfkMhnKZDMa2oDCTIjQhghDC2MCCQITAyYxpmkhAIAZDDA7l4bOSeR9YpLEwfkUjXqMOE0QN2LU4waq9aGBX6/+d7O9sXnu3579jbVTx02dlEilL0FDG1pJG64cJX5IGr6MupY5duU1npIv7sTQ4196ytUDx8+sf+TN6MQ3AyBd8+L8W0a15zYw0d8O3ww4vC7ijlkZU5QctVPE7QhNEVlTRNYUjHcy7tu3fuuVSqXBF8z66962fMeIfDaHfD4nmUyWsrk8BdaYIAh9EFoxzExEysYoAQ5A0ioAEIpIBGZmAM459iKaJo6cT209TnyjWkOSNLRWi1GtV9A3sGPg56uvG1vIZ9N/OO9rM8jS9oavSOwqaEhZYh3khq9K3fdpXWsbvdR3MoYCV/UOVadcOvv2C/AG9IYAfue5j1/U0R5mIhNctxM8yvxLyMVpOduJyLRRnto1MkXK23axlB27sXtT1z//8vqDTt3vk/fMGnX4xGyhiEI2Qi6X1Ww2S7lCIQ3DkCxzQEQKYADANgCbW6UHvwcRaO6fAwCjAewLYAKAcao6UkRIBEniEtRqNVOrVKjeSFCP61oaqurKvqe237P2lnkXn/X/PT9l3OT9Eql2V90QN1wZdRqSuhukhi9T3Q2s9ki+NDzHWppeUqnG/qsH/+b7fzSA33ruI7ODIDh/RCH6KkEZAEINfhia4n4ZO0KzphN5005Z06aRaeOAcjP++4Ff3P/86hWTLjr08i3FfEeurS3LUTanhVwe+XxOwjAw1loLoB/ASgBrAdSAV232Gc0NyJGt70+27mlrzNT6nAEwDcBMACO892kcx1KvN6hUqWu9Xka9XsfgUP/Qjcu+Nf3g6bO7zj7urBNT1F+quxLXfUkaMmDrviQ13+8THdqYqvuLZpfq+qrJNXFDbrp87t0v/cEAXr5iduiTMQvHd2QnKDC9+bC9NUfF9kwwgvNmBGW5Q3O2SFkzAkaCg/71Nz9+2MTZ6rlzLs4Vi0WbyWS5o63N5fM5G0VRaoxpA7ChBVw3ANMq1AKoHUAewCwARwHYvzWctQCeaNUrt4pvgeha17Gtevt47+M4jrVSqZlSqepqjQpVyyX/8xU3VBHF2T//+OeOFbgXaq5fa75ENR3SarzDxDToYz846FTORbPRV7oHG9sm+qEPX3TEM3vc9pm9AfiBP53+T6Pbwo0Cd4aog4p/yXK+lDX5IDIFZDinGS7CckEM+JB//u9/e3Z8NGPTgjl/Maq9s8N2FNtcPpc1bW1tFIZhaIxJATwFYA2AtAVWh4hERBQByIgIE1Gsql8gou8AeAjAfQAeVdUvEtE9reFFIpIloiyATgARgCqALQAGmHmUtTYTRWHDhhaGYE0YYmbHEXZj//rBRc/fXTly5qGHEus2FUceCbxP4DShRJ2mvuIFboyqG5kNcNuWVM965MbNd71pAC99+vADA+MnR6F+TeAg6h1TeE/I2bbAFjVLBbJcpIDzZNke8qNf//yxKblZWz42+9Pj2opFbutop7ZCQdva2hAEQZGZXwGwDEBDRCJV7VTVfVV1BDNPUtXZqnomER2tqi8S0REAzgJwUqvMI6JBAM+p6pdU9f1ElGu1E6lqUVVZVYWI6gA2EFFijJmSiUIPsDbXmGT3b59V6Kv0dd334uLGYTPmHK7Q7lRi65DCawqviXWSrEm1PlvgWMh9KPbut+/77Ohtj/97d98bA6igo7aM+O/Ogp0l8BNFPQhyY2RyE0MqcC7Ia2jyGpksBYj2//WDCx9uk/EDZ8783JhiW5HbigXpaG9HNpvNMXMGwAoR6SWiUKS5KhERS0QqIgmAHcz8sqrOA7AdwCcB9AK4CcBvAdwP4EVV3V9VPwGgC8B4Zv4PIqqoqgPQYObEOadExC1A60RUJaLxURQaZqoRW0NEsm/xgI6u7rV9L295vmvGlKmHQ32vk0QdxfA+oYTq+Vgbi70mR4p6BEaKlTid98S/9f4MV7wBgF/66AEnFbPUz+z/VNTBiywLgxxCFDgwGQqR5wznOeR8+6p1657r6uopfu7wv4mKbW0oFvIoFovIZDIBEXkReUlVG6o6Fs2N/EjvfSczj2Hm/YnoY6r6Ae/9w0T0cVXdSkTfE5FsC8iTAZwI4DAAjxDRj0TkUABTACxS1csAzG39MHlmzqvqGCLKt1xZA0Q0QERtQRBkDZMngrcmNAeMmB08uHpxNsrz2pFtbft4TWInDZtSLE5T8i7uSKRS8XDjBX4fYbnusI2jMkt/tGP9rnjxrl+gICP4Riagrzb1ssKa4CkrYRhwwBFHYGSUOZJKo8oPP/vCoV846opSoZCnQj7HxUJRMplMgGblR5h5wHtfbE1oZAvIHBFtVtX7RKTQ4pSrnHOXAThQRK4BcIaqNkTkRRF5UVUTVf1462/TVPVSEfm2974qIm3MvBhAl6pGAEYAaBcR45zLiUiPiDxKRC6bzZpsNhtGUaj5fIG/dNTltYeeWja3ltbVcGgMZX1IWbUUqDUBbBA+OYxDPuDLSORq6KsN76s48MvzZnwwlzNDgaFzAIBAi0LKtGVtEQHlOaQCQpOHoWDWL+9+ZODCuV99cnTbmM5cIY+2JudZIpronHukxUWemavOuZIxpuG9H8fM8wDMJaJHVfV0ANcDOIyIPg5ghTHm+0S0UETWq2oCoA/AI6r6C2PMgyKyD4BPM/MggJ8COIGIFqnqV1T1YADbVXUjEfUaYxrOOcPMBVXdCmCutbZirQGIlIBwavucl2577NaJM6ftO1nJ9aY+YfEpvDryknamSNdAMQ1AGwxdc/DqDjz9k/7Nw5i96ixBSK/MhTRxJ7oUbracmWAoVGNCtRSCYOxLazfcN7VjdjK+beK4KAqpkMtpJpNRABNVdT2AowHUvffjAYgxZpNz7hUiuk9VT1LVWFX/iojuBfA1IrpfVRcS0Xne+6tUX33+M/zdew8AzxljLvPefxTA3xPRIufcpQA8EYUAFhPRSCKaKSL7EFGgqjtU1RDRZmaeGIbh1sh78s7LxM59R09um7585fqNdtqUMZOMMc4igE0DthSppcYWL80VTNbyX1QCPgNN1fJqDvzi0tnjQviObGia3Ee0JEAml+E8DOUo4pxaE4GUJz3yxJr9/vSIv+8uFAu2kM8jl8vBGNNJRE+q6grn3AZV3QRgi6q2AZjHzHNE5FEAp3vvv8HM8wFQSywvADAPwDgAi0TkPwDcBWDhcFHVh9FcXH9ARE4BMI6ZvyEiHwYwSVW/CeB0IlpERJeo6hwiepmIlnrvVzLzemZex8yDzDwZqlUikGGm6R0H66+evuPYafuNynvFkCCF4xjiBd67otN4C4GmEDAqTuVnR3++beWT/z5YfRUHio8/0dEe7DynJTUvswmmEiwxWcCDwGyee37j4ydNO6ucy+YmZMJQM5kMWWvHqmqPc24eADCzENEGAMvTNH2AiM5Q1W1E9GkR2cLM3yOiS0TkO0R0lao+zMy/8N7PBHAmEZ2C3YiIoKrdqnqjqq5i5j/x3n8bTQt8iapeKyKbjDGfFpEhAGOccw8EQdBhjPmQqk723rP3PrTWvhxF0Xgi6vHeayaTyx075fS7nlvxcPGgg8ZNIjHeSKRMdbEUIEHwEuCOA4DOvB25vSRnAfghMGxEFNRb7ZoM0HFNadFeIjvRgMFkhEDKbEl8Oqq7u3bs+/c9cXQUWo2iCGEYsqrG3vvHAPwEwL2qulZETnXO/Zm1FqoKVf2Bqh6qqr8SkW3e++tU9T4i+ntVnem9vw7ARQA6ReQ5AL9yzl3vnLsewK8APIfmovkiIrpWVWeo6t977x/w3l8nIluI6Dcicqiq/quqgpnJOfdnIvJR59wmEVlCRD9S1QeJKLHWmmw2hyAM9bhpp47q7q4d733aSVBlkBoNQGxgYPdVRZ82N5In9lS7dp42GgA483hMyUY0RXgwXzAjQgUtshp1WhOR5YgDzoiB0U2baqsPLB7z0oxxBxWz2Rxls1lh5gNVdbn3/rwWR68moi5VPZWZt4nIvgBGquoRAH5BRH+OprH4oYh8XlVPQXMvfIOI/BJAFxF1qupxRPRBIjpKVSe3dOtdInKbqj5PRIe3RHayiHydiMYDOIuZfyIin0HTfI4kIgAYa4y5UUQaAI4QkY8ZY5YR0aGq0kcE8k5NNS4t665u6G9r47xDCi8pqabsNbFe9WkoRvU0upYl8GunnqebX7kZQ00O9DipLbKjRfQTPWnXYyBTBxMBBiIML2IVkt20sf6B46d9rJjJ5chaQ0EQRAC2pWm6VlVXq+rZIvIXSZKELcX/Y1U9RlW/AWC8iJyqql9V1aOcc99W1SXMfAmAh1X1qy3O+rKIHCMiGRGptUqude9iIrqWiC4brisiDxHRt1X1KFX9qnPuowDGe++vUNUPishNLQkIiOjPVPVs7/02EVkLYHsYhtYYg0wm1FNmnZPftKF2lFPJisCIkhE1DFiFaNLr1i5R+PntGR5lFMcBLWfCxxbhrgkjgqMAjCKgkrWFX48KZ7RHJm8CziJLOXJpUNu4omAuOfbKOMxkKBOGHIbhHBG576qrrtLHH3/8QmaOdtdd/5tIROLTTjvtyc9//vN3BUGQs9aOA3CyiDxXr9dRrzfo2gf/Ljt1TpyYIMnWtQ4nVW2kNd+bri41fOlMADkQerb1p4/f+WGcaS9X8HOLUQIwCgCUdFGi6ehBt7k+3k4DqQ8cOd2+mQdPnP6xijHB+MAYhGEoqppL03T/J5544iRmpvnz5z+4Zs2a1dOnT5/+8ssvr5o5c+aMWq1WSdM0VdXORYsWHW+tXXbmmWcONV2jQG9v744dO3b0jR07dvSIESNG3HbbbbNFpHPBggWPtMTvVUREWL58ee2VV145bcSIEU+ddNJJ1RY4unLlytXTpk2bEoZh2N/f37dw4cKTrLUdxWLxvnnz5pnf/e53unDhwhPa2tpWnnfeecekabopCIIMEYGIyBjGCfufvmbpltuKY6a4LKkzCh8PpZu913g0oIsAOhOKMQTElyvYPrsY43IRP6uK8wCAYHrUo+gpiXoaG+LR0X5VaNgxNEAHz5pz6PIgMGBmBTCKiJZVKpUjjDEmTdPG/PnzPwSgLCJHoLlY/omqXgLgWSJauHjx4uNPP/30obPPPnsAwGNoLl+O32Xdt/a3v/3txnK5HM6fP/+3aJ2JAAi89zkAUwGcdOqpp+YvvPBCnH322fEJJ5yQA3CH9/5YY8yft0C+SkTmP/roo72NRqPjhhtuODCTyRTPOuusRy+88MJVd9xxx8cWLFiwiog+oqp3ARgVBMEO7xVzJ70/v2jdHbNGqu/16uq98WakmuQgANhsU98MRQwMP7N0iYxhUuybD/n3WzqlAMROROElzfY3NrXHrtTNFHTkMvkiGQNiZhGZ7ZzbPDx5IoKIXK2qZzDzd9F0T/0pEV2qqoeKyN8BwLZt27ap6hmq+l0RmQXgZhH5iohcpaqrwzA0RATn3DXOueta5buqeoWqnqWqT9dqte8DwPbt2zeKyBGq+l1m/giA7wL4map+jYj2S5LEA0AYhp0AvsvMp5577rn3Axi/YcOGxaoKEdkCYBYzqzGEMMgUWILRjXSopzfekFUf5wUKYXYQCoZhykcM08C+DMUMw7Rva8sHqHZCJFD1VtTDaYLuoe3xrLGH/Yu1NiZVtcYAQEVVy7vpmPNU9VHv/RUArgZQ9d5f473/qYj8OwBMmDBhPIBnnXNfAfAj59w5AK4F8DURmcfM1JrY/4jIrSJyq/f+XlV9vmVMPlEoFC4GgM7OznEicmPrB3hJRC4Tkc+IyI+897cFQWBay5lrVfVKVX30lFNOOUZV/aJFiz7YMi79RFQiIgbg2NrazHEHf7+70q1eGiwkROoteQkhOmIYp8DQBGUcYIVwOJMepCCAkBCooCAnUPVwXoU1rrXVoyi7nwgoDO1QyymwzTn34d7e3p8B+NsWFx4AYLP3/l4iuoKIHhaR/yaiLw1z6rp169Z57+cR0bUiAiIaVNU7ReR5Y0xcrVbPbf0ek1U1DwCq2qOqG4jofhHZUi6XAeC7IkIAvqCqIKItaG4LZ4jInxERvPevtK5fY+b7W+0eBGD78uXLx6nqd51z85i5G0Bore1rNJJsxuan1EumFo3w3mtKSupAMASNRJEACBk6ixWphWCaKs1tqegVUIWyiBcPIYhRQlLKhQccNDtW9YEIh0TkiciJyGFtbW29LfCCxx577PtHHHHEhdbabd77bzLzFap6jPf+X5o46Jf333//qWh6kP+P934HMx8F4HQA53rvkc/nl9frdYjIQbsw99SWy6opPvl8BQC6u7u3ENFfq+poVb1IRK4iIvHeX7dy5UpKkuR8Zka9Xv9WNps9n4j2B/DNkSNHnrV9+/ZRIvIhIjpMVZeoqlfVEcyQ6WNmpQ8+nyva9m4IO/XeQ1XFE6UKfYkUhyrTEVDEFkAWO4NuZAuAsPnDKlgFzih8ku0cU5y4NQiCxFrLAPYDUCOizxpjrgAAY4y54YYbvtwS5f1E5B9UdSgIgloURR8BIESEO++8c8qmTZtetNYeHYahdnR0wHv/pIhsrVarvX19fQsA5H71q1/dYq01pVKpkCRJXCqVaGBgwDcaDdfX1zcRwDELFy788JIlS96XJEnBOQcADSIKmfkSIsKwpXfO/bmItBljLlHVa6dNm/bIE088sR+AMUT0WRG5kIgmWWtfIWPcuPZJDJ9r90hIRVTEq5KAlBIIdYH0UCg6FMhZUvDvjSDVnZBhUhUSUijICxHCbDFXZGOMqKoH0KmqQ/l8/ptdXV0/rlar38rn8zs5hJmJmUM0jyPb4/j3h/ze+ylLly6dgr2QaepX3Hnnnefv7ZmdoyUamyTJWABoHvTtmbq6un4xa9asSQCuA7DSWvtSo9E4zHt/dbFYvKLRaKwF0E5EwoBENlKVMOPFkcJDCRBVUlEloLQTLgWz1987FAhImCECJVEh8Z6cdzBk20ITkIg4Y4xX1ZFoHuJM3XfffT/S29uLLVu2oFKp7HQ9/W8ia+2RzHyGqv6TiPzjsccei97e3kxbW9uZACYTURVNb7mIiIYmJIOwLUWqTqQVIqFEDFHV6nC7orDMBB22LOzhWbRC0LJRLalqGYqyQWAJVDPGVJIkqQPYrKq9AGCMmQoAaZpix44d2Lx5M/r7+5Gmbzn4822jVatWvei9/9M0Ted77/9j5syZawAk27ZtswCgqt0AtohIzRhTssZWDdvQkA4RtETaxAOqZSWWnXgR1Kr8/kTbG2ThtaAE9QQSZWIQ2EilFteyhoJCa4lxYMvf9xry3qNUKqFUKiEMQxQKBeRyudcVsXeC0jRFrVZDtVrFzTffnOnp6Tl2/Pjx944ePXrt9OnTzyGirY888sjLCxYsOERExhPRDGvtswACrz4m60pOqIMIBIX4ZqCYAWsZLXumAtid6z8A5DSvlgkKFkcMiBERqHUDiUu8994SkQCoEFF+jyPfhZIkQX9/P/r7+xEEAbLZLKIoQhRFbzugzjnEcYxGo4FGo/EqCejp6Tnv5ptvfk2dH/zgB8sWLFgAVS0CqHjvyTlnq2mFYF3VORnJICKwI2IFI0Qi7TCtLaYCVgnbAdoA6GRhaoPXhipIVJkEUCXP7CrleBAd2RHsvYcxpopmfMreaICZN6LpQWYRmZSmaeeuk7LWIggCWGsRhiGstWBmWGuxqwUFABEZ9ilCROCcQ5qmcM7BOYckSYbd/XuiTczcT80YHHjvZ6MZZ4O+vr5hx+14Va1Qa/M9WB0Asa+SUCcIRuAtg5QEBKDYrEJrwdhiIXhBRQyIJkMxQxQvkELh4RUq4kCJ2VHdOLiOx+YmmTC0trWwnQOgsvtoiegFInKdnZ3rRo0aJT09PTw0NAQAm0VkzvBzw5N/B0mMMU+pqhk7dmxXsVjkzZs35xuNhojICDSPRpPt27c/WSgU5hLRC95722g0aOPgWnbcW5VUBYCSJYBBChgQzWnt2J4BsJyheFkVr7Q6Hc2kZYU6ARSejCjZFN259UOrc6reOucMEfWpqnXOPQIAhULhN8PgMXNl3rx5Y4IgOIuZz46i6KyTTz55JBFVmXnFO4nYrmSMeTKKooEPfvCDs40x8621Z3d2dp566qmnxsxcArC1s7PzkVWrVi1X1QBAv/eeiYg2DK0upOgpiCBQIlIBBOrBOgTCCAAQ0jUQrGS1WF1vUPewLlTlKoQCOARewOqVUgzmtlXWTWuKiqiIVAAgjuOtuy1bgtNOO21ET0/PhO9973sQEXznO99BT0/PxJNPPrkDQAO/97C8k7RBVaO5c+ce19nZmb3yyisxZcoU/NVf/RVWrFjx/kMOOWQ9M3dXKpVRjUYjbKmGinOOnPPYWt04PZGhjHoQCZigAQsFpFwbxqlRpx6k6LI6gK5Kpz8zm20d0JHWQFAYTSUlALDexSNdEB+Y+nQxpZRlppSZ4ZybdPvttz9QqVSOt9Y+SkR+xYoVxx522GF4/PHHceCBB2LZsmWYPn06nnrqqQOZ+REiekZERr+T6BFR37hx47rWr18/NwxDvPLKKygWi3jhhRdw5JFHolarzXvuuee60jSdYFordxFJnHNI0rghiGc4jb3xUDEQEngyYEBrwx7KcuJHZzux1t79KZQ++iv5AHTnCadVBZGQhULh1SsIMfoe7KlsGRqTm5Q1xmkQBJtV9dijjz766f06bwAAEgVJREFUnpUrVy4EgIMPPjh300034bjjjsOaNWtQqVQgIjjqqKOwZMkSzJs3b/Xy5cstgFUA3rZF954cr6eccsrYxx57DJ/85CexcOFCDA0N4cQTT0S1WsWjjz4azp49+4l6vc5Tp049TVU3eu/hVXVbZUN/TH33k8c4DVRIiMFEohCjCIdXLC6VY+44DV+zACCEXiiWgnCkEp1EpKsEqqTEIsTq1Axg+eCy/kczp+QmqDZfuXpRVedNmjRpx9VXX32hiEBEsHTpUtx5551YsGABnHM47LDDcNNNN+GAAw7Al770pc8NPzdsUXe1rsOA7n4dBmjXK3NzgbHrZ2beWQDg7rvvxq233oqLL74YS5YswY4dO/Dkk09i7ty5uOCCCz4bx/FPRGSUiNydph71ap2W9T9eGGgsr4iqZSVVsLJ6Z5lIlU5srfmWAlgHtE7lDjgP5SjgAWb6MBTtoroMgpwoERTwniiJhwq5aPrxB+YOWwuQIaKEmWd573NBEHSoKosIpk+fjltvvRWqitWrV6O7uxvLli3DV77yFRQKhVeBtzcgd/2+exmm3bl3dy4kIowfPx4LFy5EpVLBpk2b0Nvbi+7ublx22WWw1ro4jgsARgJYVq/XUG/Uk2fK95+ypXxfrESGGUIEMhYGTP1ovQOYOr2+kcjvVt+K9c130cp4slyX4nDnBqYbRCAGkTZXUELIVtPeezeUu3rjOEaSJFDVpwEcmKbpLcMTnDhxIm644QYEQQDTPDvBNddcg3322ec1IL1e8d6/qryZOruDffTRR+PrX/866vU6kiTBAQccgOuvvx5hGKI15hki8lTz76lura/fUUt6F4siJIKCiREAakhB6BnGp1ST9lwbngJ2CfE99Zd4cPzIcDqg4xl4wQl64EE+BlyicCnYanHz4RMumviR9vO7C4UC5fN5JqKzVfXlKIomtzzGr5nwGwGwOxe+ngi/ntjuXowxe/s+0Gg0+ohofxG5o1KpoFqv6+LBn496dssPt6dcmWAtlCOCNRDKgJgxEopDoLRl60Cy5p5P4Hhgl/A2NbgmTuUGBeCBOUTokVZAtyiIFJSk5QmJlJKeyvaeer2u9XpdVPVxVZ1Zr9dv25PI7Q7M3sDbEwe+0Q+wt/b21vdwqdVqv1XVaar6eJwkqNdj9JY3bW9IKU5cZRwUDNPcuagBE2G7Kg5RAKnI9SD832HcdgJIARYOVdyknXtjoTpBoaRsTPOMHQy7fMutQy/qQzOr1arW63VNvd+kTc/NfO/9I3vTXXub0N5E9/U+v57Yvp7+VFWkabpYVc8DMJSm6aZyqcSNRk1fxOMHPb/5v+pQtWwgUBCxErGCiOJhXHYMuRkU4r7XAHj3aYhTAaC4rakI9dNkMMSWPBhMSsRKmjRKIyuuZ3Bzfe32crnGlVJJReQ+Vc3HcdyuqgPD4re3ib1ZHfhmVcDuYO4JxNaYetI0HYvmMen91WqVqo1YNqVdW2uutz9NSp3KTNpcxMEYgjEYVNULmvVxiwLVu09D/BoAAcAZXL6j7F9SBVRgiUwPkRJYCQaqrEoMWrrqp4WN2ZfmxXGtWq7UqFwuJyJyP4A5cRw/qKryelywNw7ck+58I336ZvtR1Uaj0XgewMEicl+5XPblcpXqtXJtk33x1KUr/6MAbnKdgQKsDFUVMTtUYFWBvpLvohRX7orZqyJU192K6tSz9Qv5HPcQaCpBZyvjRSiyEFIVkDioiBbL1W3LglGduWJ9LKDExnAtCIJEVU/w3t/MzIfsbiD2dn0jHbkrF+1qSPZkXHY3MMNX59ydaB5ePdNoNLZUqlVfrpSxOvO4earr5xvqvm8iGfggBFNIyiGYQwwQ4xwABqqLhmo+c885eJVf7NUx0gDE4iv9Q/JYc1+MDABvDJQs2DDYhlBmxD2Da6YNxOulW9dsr1TLWiqVtF6vrwawXFU/7Zz7TwB/FCf+MUuW1ylJmqY/F5GzVXVZvV5fWy6XaahU5q26asuA22L7hlbvR4a8NVAYKFsgMBACJZDm7mNHSZ41HpfujtdrovS7bkV58p/oRwpZ8zIIhwM0C0SLoBipCmqNnaHAhq3L7MT9D9mfhjIrrYRt3nu0fG9VAKd673+Npq8t82a5cW9ADdOb4bZdljfbRWSpNt9BeSJJknVDQ0MYHBqiwXRHd9+IriPvffpa4YBCE0I5grCFMRlSGFoF4DMt3ffDUtXLPfPxyzcEEADGnoNH01gWFLNmChQhgTJEOqiKQIQEAiPNU09+Zf3jfZNnH3yY9mVWasoFL16sMWVm3gzgNO/9KiJaq6qTdlfyewNv9+f+QNCGPz8qIgLgaFVdVK83egcGBk25UtWBel9f/4Q1x931yFUbYLWNIxgOoDYgDSJYE6IB8CEEjFKg1D2QdscVfHn9r/EaB+YeAdx8B9z0+Sgz8HxgeR6AMVB6hgzaVMk3Q/2JSQHvJOra+GTXlMPmfEi6o+d87NpTLyTeN5j5ZWae6b3fV0RuIaKZqmr3ZJ33BNzuAO4G0B7vMfOQiNyqzcBN8t7fN1QuN0pDJVQqJe2v9u2oTt9w0l0P/uNz3iQjghA2CMmEGXgOCSYDIqJuAk4AgHrDf7We6u/uPx97zO6x13fl1tyOtfucqRcXM+ZFAHNAmA2iu4gwRkBKos0jAVXy4vKvrHvslWlHHHZk2m1eQKJ5VfXOOauqG4Mg6FXVj4nIalVdpKoHqSrtsrzYed1VXAHsDaQ9caAQ0S0iMoqIPkBEDzWSZHWlXI6HBkvBUKWsQ2nf5uSA7SfeueTqFxPUxtpQAxMSmxBqAhKTBZhoBYALAUCBW3ZU/D6Lz8E1e8NprwACwKQv4nf1fvlUMWsJwEgC5oDpIVJ0EhGrJ6sAICCXuvYVqx8uzXj/YZPSWFbWelyHeA/nPRLvqwxa3XRN4COqugrNKPwx2ozifxVww1y3K4CvA95WAHdQ8xWHDwJY4b1/tlwupwNDVVTKQ9rfP6j19h3dsv+Ow29bdEWvUmO0CWBshowJCTZL3kQAW1pPTb1noPTK9oG0no7Cp9b/7LWi+6YAXP8zuMnn4rFG4kfnQ3MYgIgIU5jxDCmKCigBpE1xZlEfvPDSErffrFkU7BNQpSutxQ1PLo6zSerFi9RV/CvMXFXVQ1R1H1VdhGaIbxnAzgQ5u4vtLsUx8yMA7mPmbQAOJKI2VV2XJMlLtVqtViqVaLBUlUqpn0vloTofOhBVMptzv1h4dd4Yn7cR1GSJwwhiQhIbIjUBthBwJoC8ElzvUHqzKL5+/+l4zQuGu9Kbyplw4m04Ix/xjI68+W6r2gZifdI1dFSaEEtdOW2AJYG6hnqXEMaOnL7ptGO/+L5kjVks2/JjM5nIZKJAoihLmUyIIAjIGANjTEBEHSIyWUQ6RWSdqm5V1YqIpC3RDImoQETjiGgKM5eIaKOIDKpq4r2Hcw6NRgO1egzvUq3V6l5Hxhuys9OPP7T0lke7tj41nQNiG0FtBmojeBMR2yzIRNhKQh9U6L6kkMGq/7t6Ii8uXoDfvRE2bzprx0n/hc93FLiQi8x1zYq0CdAHvcdkV4V3Dupi9b6OgosR+wRGvU3PPuXSHcXcPiMGnvAvcJIZlwsjG2UzMESUzWa16SExZGxLGFS9sVbFK5SUAGBYWYoIMzN5BbnUgSCaph5xXCfvvSZJouVaw1NWejrfL3NK1a07frHwmpFsXcgRvA3hTRahNeRsHmKaXpZtIDoa0P0AoBb7SwZqEt+/AP/6ZnD5g/LGnHwbvtlZCAYzAYbzJwwo4U5xOl0aUB8jcDHUxUSuoQ4pJE0gmbCt9vFTLm4UM2NHDCxNlidDweiQOAyCUDkwFLBBEFhSZrVEqkDzHLEVAiA6PFBFE0pFkjhS9YjjVJ1Lkfg0sZ3SO+rI8NBSo7vvznuuz8S+lDMhwBbWhmRtVr3JgmwAmAhqAlolij+h5svfqMW4ZKiaFu49F1e/WUz+4MxFJ92GS3MR246M+bYSGEAizD8mJ4d6p+oa8L4OcQnUJzA+hhWnqU+gUdA2cPKxnylNHj/rmOrW9N7+F5JGOiQjyXIYcgC2zRejiVXFw5Np5Y3xMGxgxBMJPMSlFHtPUI1NG/eNmhNm8uODUzZse+nB+x78WVs9KXXaDMgYspyBNyG8iQATwIRZwIawYPOCQj4LICSFDNX9V6qJ5O5bgH/8Q/D4o3JnnfhzfC6yvM/IdvPXADpaLd0KoaJPNS+xmjSF1QYkTeEkVfYpGR8j9Q5WRKvjRkztPf5DC3j0iCkn+AQvlDdUu6rbXaPWn5KrCEEErTwXTTKALbDmRgSaGxNk26bmppoQc7p7ux546PE7ZHvfutHGUJ4DOGMRmEi9sSQcwgYR2GTgOCRvDFXVaJUU81sA9PcM+X92Trru+yT+8w/F4o/O3nbyrTiaGF8cUwgOIMZRreZegerDgB6YJiQSw0uqgYsh3sFrjMB5eE1gfAovHka9pjaM+ke2TxiaNnWujBkzOcxnO/KFXKHNBpnRAODSRm+lVh6q1odqPT0bkjXrnuW+oS3tLo1HsKGADIQDsAnhjEFAFgmHsDYCmYBSG4BMRgMQvQTQcYBOBwBVPN5TStd6hxvuPx9L/xgc3lL6u5N+hpGwuHl0u33a2N/nDiTSXxBIRHWCNMilMdQ7DSVF6h1YUxXvyKhD6h0CCKCCVLxa9YASKYlyK/AOIJAyCUFBDGImB4KlEEoMbywCCtQbQ8QhxFiEJqDYWLDJakBEm4g1UKFPDI/Rq16xY9AdZQzOXzgf/X8sBm85AeM5t8P0eXwtItYRbfZToOavCyDxKj81RCPgaKJ3iL1TAw9xCVgdvHcw6uBVm/pNvQIKpwJV2pkKBQCEFKoMYoKFITVGQQxPBsZYeLIwNoQQw3BAjiNEzNioQKzAebQzkJRW9lXcbXEqctx5uOryYUv1R9LblkP1+JsxjS1+MDJn7wkDuhKEHACQQqD4OUgExJPFq/EpqTglcXDqEXoPJYETDwbgROBVAQY7ABCIJQKYYQBYZogaWGMAMkhhEJiQPLMaG5BTlvWUsgXjvJahAxS1RqpfH6i5eYjxhfs/i7clj+rbm8VXQSf/HB8T4LOj2uwzgaF/0GZ2oeHuVqjq48zIQzHee4QiSLUZgwN4kDYdt0Kkqq38BM1XhYnAMMwKGDQ979y0rERIRbENQJWIPgDorF0m2Ei9Xt0/5N4njH+//zzc9XamRH5H0iAffiOC9gLOVeD8kXl7bxjyxYC+OqMv0VaoPsCEukAigNqg1EEEFlWBQKHUFC9SBoOYiEUhRDoIaInBiSgyBDpJoeN2m9qG2Mv1/SV3iir+s1zFbc9chLc97vgdzWR+uYIfugUnC/C3keUlHQXTaQiX7LUCox9en1XwIBENCqTcvM1FVe0gSAcMzYVgxN6a8IrrBit+IHFyrCF850Orcf/ll781Pfd69K7l0j/mJxhtLb4+ot2uDy3t1T30Vihxeml/2U1WxpVLPol3PA088O7/MwI6/ib819j2YDOb154vvBVSxfXdA+nEBz6Ns4G3T8e9Eb3mUOkdJsW++NT2UjpHVO/V5vrvrRfVh7f3pTNLdZyLdxE84N0HEEtOgMsRzukdcBUV2vRWwYOnbTuG3HZXw4J3wki8Eb2uQ/WdojW/RLz/n+CluKaZTMhzm4eJwB9aFHADFf1X7+X6h/4MG9+LubzrHDhM934KLyhoaSPB3/yx3Nco42+811UPfBbvWvD67vSu/0eb3enEn/K17RkeNExXvPHTvyfxeuVQQ0be9zn50hs//c7Re8aBw3T/Z+TScl3niuBm9cCbLLeXGjr3mA3yl+/1+N9zAEHQ6oA/rxLLBPF49o1Fl54vxVJ08Ge/kwvkN0vvPYAAHv8K6ur8BbVEnlNF6XUArNQS/ziJv2jJ5/Cm07W/k/SeWOE9UddvUJ5+pimpYhODTtyT1Y29fsOrv2fxhXj+vR3t7+l/BQcO0z2fc0ucEyeil+7OfV7xFYXI4gvx4Hs9zl3pPbfCeyA67cfmFiaziVX/BgCUcL1XGf27z/vz8S7vNN6I3t23oN8caW0//+lcF/0PC+4VIBJgZm2aPw3/y8AD/peJ8DAtOQEuZLfAQ0sK7Q0rbv6SE/Yen/L/017ojH8LZ5/xb+Hs93ocr0f/D6s769KBP+5xAAAAAElFTkSuQmCC\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"useMarkerImageFunction\":true,\"colorFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n if (percent < 0.5) {\\n percent *=2*100; \\n return tinycolor.mix('green', 'yellow', amount = percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', amount = percent).toHexString();\\n }\\n}\",\"markerImageFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nvar res = {\\n url: images[0],\\n size: 55\\n};\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n var index = Math.min(2, Math.floor(3 * percent));\\n res.url = images[index];\\n}\\nreturn res;\",\"strokeWeight\":4,\"strokeOpacity\":0.65,\"color\":\"#1976d3\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"showTooltip\":true,\"autocloseTooltip\":true,\"labelFunction\":\"var vehicleType = dsData[dsIndex]['vehicleType'];\\r\\nif (typeof vehicleType !== undefined) {\\r\\n if (vehicleType == \\\"bus\\\") {\\r\\n return 'Bus: ${entityName}';\\r\\n } else if (vehicleType == \\\"car\\\") {\\r\\n return 'Car: ${entityName}';\\r\\n }\\r\\n}\",\"tooltipFunction\":\"var vehicleType = dsData[dsIndex]['vehicleType'];\\r\\nif (typeof vehicleType !== undefined) {\\r\\n if (vehicleType == \\\"bus\\\") {\\r\\n return 'Bus: ${entityName}
Bus route: ${busRoute}
';\\r\\n } else if (vehicleType == \\\"car\\\") {\\r\\n return 'Car: ${entityName}
Current destination: ${destination}
';\\r\\n }\\r\\n}\",\"provider\":\"openstreet-map\",\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\",\"useCustomProvider\":false,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"draggableMarker\":false,\"disableScrollZooming\":false,\"disableZoomControl\":false,\"useLabelFunction\":false,\"useTooltipFunction\":false,\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"showPolygon\":false,\"showCircle\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonColorFunction\":false,\"polygonOpacity\":0.2,\"usePolygonStrokeColorFunction\":false,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"showPolygonTooltip\":false},\"title\":\"Route Map - OpenStreetMap\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('openstreet-map', true, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.map.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true,\n ignoreDataUpdateOnIntervalTick: true,\n hasAdditionalLatestDataKeys: true\n };\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-route-map-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First route\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.5851719234007373,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.9015113051937396,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.7253460349565717,\"funcBody\":\"var value = prevValue;\\nif (time % 500 < 100) {\\n value = value + Math.random() * 40 - 20;\\n if (value < 45) {\\n \\tvalue = 45;\\n } else if (value > 130) {\\n \\tvalue = 130;\\n }\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"openstreet-map\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"gmDefaultMapType\":\"roadmap\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"useCustomProvider\":false,\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"mapProviderHere\":\"HERE.normalDay\",\"credentials\":{\"app_id\":\"AhM6TzD9ThyK78CT3ptx\",\"app_code\":\"p6NPiITB3Vv0GMUFnkLOOg\"},\"mapImageUrl\":\"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==\",\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"tmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#1976d3\",\"useColorFunction\":true,\"colorFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n if (percent < 0.5) {\\n percent *=2*100; \\n return tinycolor.mix('green', 'yellow', percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', percent).toHexString();\\n }\\n}\",\"useMarkerImageFunction\":true,\"markerImageFunction\":\"var speed = dsData[dsIndex]['Speed'];\\nvar res = {\\n url: images[0],\\n size: 55\\n};\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n var index = Math.min(2, Math.floor(3 * percent));\\n res.url = images[index];\\n}\\nreturn res;\",\"markerImages\":[\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic7b13uB3VdTb+rrX3zJx6i7qQUAEJIQlRBAZc6BgLDDYmIIExLjgJcQk/YkKc4gIGHH+fDSHg2CGOHRuCQ4ltbBODJIroIIoQIJCQdNXLvVe3nT4ze6/1/XHOlYWQAJuWP37refYz58yd3d6zyt5rr1mX8B7S5Xo5/0nPYaNFM1PY0gGqOhfAgQCNBGlWFFUAYEIeihigbhFdZQwt85BV5Gj9r/718R2XX365vFdzoHe7w6d77xnPkn4YpAtU0YiizNJcmPNkMQFkDiSlowHt2HNtGlTSJ6B+pTpsKTfKgTj3Pi8SMtFtEZnFs8d8dPu7OZ93BcCHtt0+OiL+FJjOiqy5K5dtLwD4PBHGvy0dKLYo8B+1+lAldv50FfmFzWX+84i2M3a8Le2/Dr1jAKqCHtl2y1wC/pEMP9ZRLBaYzF8CCN+pPluUkOKfB6qlmk/dBwTyt8eOv2AZCPpOdPaOAPjA1h9/SJX+TyGXuz0TZi4EcPBeOk+U+RErZh2YyMAyQJEoZUjFgtkCAEScgDyx1hmInTglqDj2U1X0WILaPbWvwHO1WummeuLONhaXHTf2wsfe7rm+rQDe133j/i5xPyrmCr+OouhSKPbdQ5fLiezTIYUBQGMJBgYWxMYSISZhbxgQT8wGAgDiwWxUvCiBxKhSKOqdh4OyV5+6XiEfK/kjVOXQ13apG+I0+adKpXaG0/Si0yZdvPbtmvPbAuCNT98YTBhT/8fAmEpHoXgKgPe/6gFGP0nwG8s2YykcaRCAYYQ5tKTkDVuArDEwMRF5AICS4VZ1AQBSr6oEgL36CBAvlKqIsyLOKQl5TZH4uN+TawDuY6o64lWTJX20v1S633uJNvfmvnbRERelb3XubxnAX26+5gDy6Y9HtrU/wERff1XjSt0WwULDmZEMawPOgilgQ4FaGCEygaXQMQyRMaxiUijUkAEAImIGAFURAOrVA1AmI1ZExGuqoqkVFefhyGtKDql4X4eHc6LxJof0VIVM3nVc4uXaHUPlo0Tpc2fv/zer38r83xKAd6y74iImO31EMf9REA7cpdVBY8NbA5+dFNqsCTQipkitBjAUsLUZNd4qm8AyjDMmJAIRhDzDEBEbJkBVAyJWQJ14AEaciIeSGicOgBeBWNHEeXLkXIM8UvFI4bVBCVJNfdk7STd5xOcp0LZzjIqV/eXq/4i61edM/eaN7yqAqpfzf62Nf5LP5lbko/DbCuxU4saEN1mN2kKTzQbIkuEIEWfVagRDEVkOyXCkVq0aDg2p9YYNAySVerU0WN1R27Jjo6ulMQ1V+ggAOgsjNRNEus/IiUFnYUy2kM23AcrivXh2RiTxjhx5iSmVWEWdpmhQ4qvwSBBrXVPfqDmuVsT7C3aZvKslyZcr9dpxdr81F8ynO/w7DuD1q/8y6kDw2872ticN0deG7wvQHXHmdxGK+1ibQag5ikweliIElNUAEayNYBCSRQRiYzf2rNtx11O/rC5d9dj+1aQyM2Pyz3WGozaNisYNWY7SYtgWA0A5KUVO4qAn3t4+lOzYt+Grh+bDwstHzvjA2tPfd1Z+39FTRhGpi7VBKrE4nyBFDKcNJL5OCerqUEXdVeEQb0mk8lECjR0euxe9cqBUOnoQ6RkXT78hfscAvH71X0Z5kf8Z0dH2CgNf2NkI0d0ZbmtElMtFVEAQ5BFIlkKb00AzFJqCGooQcJjv7t868P3/ubayZvua48ZlJt57xLjjB/cpTssXokK7IQNrbeoZ3pIRJm1aYSUW9cwixglZ7xNU40ppY7mr+sy2ezt7G1s+vP+EGfd/+fS/Ko5pH9/pJK04X6MUDSRapcTXkXJN46QKp1UkqNVqvpxVyLzhOajihh1DpVkmrJ7+uak/bbztAF6/+i8j62p3j20vbgXR+cP3LYU/Djg/KcsdEnIWERcRIk+hzWtEOYSch2U76tk1T6+84Tf/NCdni2tOmbRgy6T26WOiKDBhGFEQhrBhiNAyjDGiQp4DFgI8AChg1BGBXOC9p8QJ0kas3jvEcUxxnLgNpTW9izfdOqGWlve7+OOXrThk6qEHKtKehq9xIlWkvoaYytrwFYqlglgrcZxW+oXSz+ycpOLmnsHypDTIfuTNcuKbAvD2288x22dn7hrVnt/ATBftBE/CH2aCtqkZU6CI2hHZomS4YCPK+5AKHFB2ZNe2Nev/739/e9qY3KRnPzHtQp/LtnfkMhnKZDMa2oDCTIjQhghDC2MCCQITAyYxpmkhAIAZDDA7l4bOSeR9YpLEwfkUjXqMOE0QN2LU4waq9aGBX6/+d7O9sXnu3579jbVTx02dlEilL0FDG1pJG64cJX5IGr6MupY5duU1npIv7sTQ4196ytUDx8+sf+TN6MQ3AyBd8+L8W0a15zYw0d8O3ww4vC7ijlkZU5QctVPE7QhNEVlTRNYUjHcy7tu3fuuVSqXBF8z66962fMeIfDaHfD4nmUyWsrk8BdaYIAh9EFoxzExEysYoAQ5A0ioAEIpIBGZmAM459iKaJo6cT209TnyjWkOSNLRWi1GtV9A3sGPg56uvG1vIZ9N/OO9rM8jS9oavSOwqaEhZYh3khq9K3fdpXWsbvdR3MoYCV/UOVadcOvv2C/AG9IYAfue5j1/U0R5mIhNctxM8yvxLyMVpOduJyLRRnto1MkXK23axlB27sXtT1z//8vqDTt3vk/fMGnX4xGyhiEI2Qi6X1Ww2S7lCIQ3DkCxzQEQKYADANgCbW6UHvwcRaO6fAwCjAewLYAKAcao6UkRIBEniEtRqNVOrVKjeSFCP61oaqurKvqe237P2lnkXn/X/PT9l3OT9Eql2V90QN1wZdRqSuhukhi9T3Q2s9ki+NDzHWppeUqnG/qsH/+b7fzSA33ruI7ODIDh/RCH6KkEZAEINfhia4n4ZO0KzphN5005Z06aRaeOAcjP++4Ff3P/86hWTLjr08i3FfEeurS3LUTanhVwe+XxOwjAw1loLoB/ASgBrAdSAV232Gc0NyJGt70+27mlrzNT6nAEwDcBMACO892kcx1KvN6hUqWu9Xka9XsfgUP/Qjcu+Nf3g6bO7zj7urBNT1F+quxLXfUkaMmDrviQ13+8THdqYqvuLZpfq+qrJNXFDbrp87t0v/cEAXr5iduiTMQvHd2QnKDC9+bC9NUfF9kwwgvNmBGW5Q3O2SFkzAkaCg/71Nz9+2MTZ6rlzLs4Vi0WbyWS5o63N5fM5G0VRaoxpA7ChBVw3ANMq1AKoHUAewCwARwHYvzWctQCeaNUrt4pvgeha17Gtevt47+M4jrVSqZlSqepqjQpVyyX/8xU3VBHF2T//+OeOFbgXaq5fa75ENR3SarzDxDToYz846FTORbPRV7oHG9sm+qEPX3TEM3vc9pm9AfiBP53+T6Pbwo0Cd4aog4p/yXK+lDX5IDIFZDinGS7CckEM+JB//u9/e3Z8NGPTgjl/Maq9s8N2FNtcPpc1bW1tFIZhaIxJATwFYA2AtAVWh4hERBQByIgIE1Gsql8gou8AeAjAfQAeVdUvEtE9reFFIpIloiyATgARgCqALQAGmHmUtTYTRWHDhhaGYE0YYmbHEXZj//rBRc/fXTly5qGHEus2FUceCbxP4DShRJ2mvuIFboyqG5kNcNuWVM965MbNd71pAC99+vADA+MnR6F+TeAg6h1TeE/I2bbAFjVLBbJcpIDzZNke8qNf//yxKblZWz42+9Pj2opFbutop7ZCQdva2hAEQZGZXwGwDEBDRCJV7VTVfVV1BDNPUtXZqnomER2tqi8S0REAzgJwUqvMI6JBAM+p6pdU9f1ElGu1E6lqUVVZVYWI6gA2EFFijJmSiUIPsDbXmGT3b59V6Kv0dd334uLGYTPmHK7Q7lRi65DCawqviXWSrEm1PlvgWMh9KPbut+/77Ohtj/97d98bA6igo7aM+O/Ogp0l8BNFPQhyY2RyE0MqcC7Ia2jyGpksBYj2//WDCx9uk/EDZ8783JhiW5HbigXpaG9HNpvNMXMGwAoR6SWiUKS5KhERS0QqIgmAHcz8sqrOA7AdwCcB9AK4CcBvAdwP4EVV3V9VPwGgC8B4Zv4PIqqoqgPQYObEOadExC1A60RUJaLxURQaZqoRW0NEsm/xgI6u7rV9L295vmvGlKmHQ32vk0QdxfA+oYTq+Vgbi70mR4p6BEaKlTid98S/9f4MV7wBgF/66AEnFbPUz+z/VNTBiywLgxxCFDgwGQqR5wznOeR8+6p1657r6uopfu7wv4mKbW0oFvIoFovIZDIBEXkReUlVG6o6Fs2N/EjvfSczj2Hm/YnoY6r6Ae/9w0T0cVXdSkTfE5FsC8iTAZwI4DAAjxDRj0TkUABTACxS1csAzG39MHlmzqvqGCLKt1xZA0Q0QERtQRBkDZMngrcmNAeMmB08uHpxNsrz2pFtbft4TWInDZtSLE5T8i7uSKRS8XDjBX4fYbnusI2jMkt/tGP9rnjxrl+gICP4Riagrzb1ssKa4CkrYRhwwBFHYGSUOZJKo8oPP/vCoV846opSoZCnQj7HxUJRMplMgGblR5h5wHtfbE1oZAvIHBFtVtX7RKTQ4pSrnHOXAThQRK4BcIaqNkTkRRF5UVUTVf1462/TVPVSEfm2974qIm3MvBhAl6pGAEYAaBcR45zLiUiPiDxKRC6bzZpsNhtGUaj5fIG/dNTltYeeWja3ltbVcGgMZX1IWbUUqDUBbBA+OYxDPuDLSORq6KsN76s48MvzZnwwlzNDgaFzAIBAi0LKtGVtEQHlOaQCQpOHoWDWL+9+ZODCuV99cnTbmM5cIY+2JudZIpronHukxUWemavOuZIxpuG9H8fM8wDMJaJHVfV0ANcDOIyIPg5ghTHm+0S0UETWq2oCoA/AI6r6C2PMgyKyD4BPM/MggJ8COIGIFqnqV1T1YADbVXUjEfUaYxrOOcPMBVXdCmCutbZirQGIlIBwavucl2577NaJM6ftO1nJ9aY+YfEpvDryknamSNdAMQ1AGwxdc/DqDjz9k/7Nw5i96ixBSK/MhTRxJ7oUbracmWAoVGNCtRSCYOxLazfcN7VjdjK+beK4KAqpkMtpJpNRABNVdT2AowHUvffjAYgxZpNz7hUiuk9VT1LVWFX/iojuBfA1IrpfVRcS0Xne+6tUX33+M/zdew8AzxljLvPefxTA3xPRIufcpQA8EYUAFhPRSCKaKSL7EFGgqjtU1RDRZmaeGIbh1sh78s7LxM59R09um7585fqNdtqUMZOMMc4igE0DthSppcYWL80VTNbyX1QCPgNN1fJqDvzi0tnjQviObGia3Ee0JEAml+E8DOUo4pxaE4GUJz3yxJr9/vSIv+8uFAu2kM8jl8vBGNNJRE+q6grn3AZV3QRgi6q2AZjHzHNE5FEAp3vvv8HM8wFQSywvADAPwDgAi0TkPwDcBWDhcFHVh9FcXH9ARE4BMI6ZvyEiHwYwSVW/CeB0IlpERJeo6hwiepmIlnrvVzLzemZex8yDzDwZqlUikGGm6R0H66+evuPYafuNynvFkCCF4xjiBd67otN4C4GmEDAqTuVnR3++beWT/z5YfRUHio8/0dEe7DynJTUvswmmEiwxWcCDwGyee37j4ydNO6ucy+YmZMJQM5kMWWvHqmqPc24eADCzENEGAMvTNH2AiM5Q1W1E9GkR2cLM3yOiS0TkO0R0lao+zMy/8N7PBHAmEZ2C3YiIoKrdqnqjqq5i5j/x3n8bTQt8iapeKyKbjDGfFpEhAGOccw8EQdBhjPmQqk723rP3PrTWvhxF0Xgi6vHeayaTyx075fS7nlvxcPGgg8ZNIjHeSKRMdbEUIEHwEuCOA4DOvB25vSRnAfghMGxEFNRb7ZoM0HFNadFeIjvRgMFkhEDKbEl8Oqq7u3bs+/c9cXQUWo2iCGEYsqrG3vvHAPwEwL2qulZETnXO/Zm1FqoKVf2Bqh6qqr8SkW3e++tU9T4i+ntVnem9vw7ARQA6ReQ5AL9yzl3vnLsewK8APIfmovkiIrpWVWeo6t977x/w3l8nIluI6Dcicqiq/quqgpnJOfdnIvJR59wmEVlCRD9S1QeJKLHWmmw2hyAM9bhpp47q7q4d733aSVBlkBoNQGxgYPdVRZ82N5In9lS7dp42GgA483hMyUY0RXgwXzAjQgUtshp1WhOR5YgDzoiB0U2baqsPLB7z0oxxBxWz2Rxls1lh5gNVdbn3/rwWR68moi5VPZWZt4nIvgBGquoRAH5BRH+OprH4oYh8XlVPQXMvfIOI/BJAFxF1qupxRPRBIjpKVSe3dOtdInKbqj5PRIe3RHayiHydiMYDOIuZfyIin0HTfI4kIgAYa4y5UUQaAI4QkY8ZY5YR0aGq0kcE8k5NNS4t665u6G9r47xDCi8pqabsNbFe9WkoRvU0upYl8GunnqebX7kZQ00O9DipLbKjRfQTPWnXYyBTBxMBBiIML2IVkt20sf6B46d9rJjJ5chaQ0EQRAC2pWm6VlVXq+rZIvIXSZKELcX/Y1U9RlW/AWC8iJyqql9V1aOcc99W1SXMfAmAh1X1qy3O+rKIHCMiGRGptUqude9iIrqWiC4brisiDxHRt1X1KFX9qnPuowDGe++vUNUPishNLQkIiOjPVPVs7/02EVkLYHsYhtYYg0wm1FNmnZPftKF2lFPJisCIkhE1DFiFaNLr1i5R+PntGR5lFMcBLWfCxxbhrgkjgqMAjCKgkrWFX48KZ7RHJm8CziJLOXJpUNu4omAuOfbKOMxkKBOGHIbhHBG576qrrtLHH3/8QmaOdtdd/5tIROLTTjvtyc9//vN3BUGQs9aOA3CyiDxXr9dRrzfo2gf/Ljt1TpyYIMnWtQ4nVW2kNd+bri41fOlMADkQerb1p4/f+WGcaS9X8HOLUQIwCgCUdFGi6ehBt7k+3k4DqQ8cOd2+mQdPnP6xijHB+MAYhGEoqppL03T/J5544iRmpvnz5z+4Zs2a1dOnT5/+8ssvr5o5c+aMWq1WSdM0VdXORYsWHW+tXXbmmWcONV2jQG9v744dO3b0jR07dvSIESNG3HbbbbNFpHPBggWPtMTvVUREWL58ee2VV145bcSIEU+ddNJJ1RY4unLlytXTpk2bEoZh2N/f37dw4cKTrLUdxWLxvnnz5pnf/e53unDhwhPa2tpWnnfeecekabopCIIMEYGIyBjGCfufvmbpltuKY6a4LKkzCh8PpZu913g0oIsAOhOKMQTElyvYPrsY43IRP6uK8wCAYHrUo+gpiXoaG+LR0X5VaNgxNEAHz5pz6PIgMGBmBTCKiJZVKpUjjDEmTdPG/PnzPwSgLCJHoLlY/omqXgLgWSJauHjx4uNPP/30obPPPnsAwGNoLl+O32Xdt/a3v/3txnK5HM6fP/+3aJ2JAAi89zkAUwGcdOqpp+YvvPBCnH322fEJJ5yQA3CH9/5YY8yft0C+SkTmP/roo72NRqPjhhtuODCTyRTPOuusRy+88MJVd9xxx8cWLFiwiog+oqp3ARgVBMEO7xVzJ70/v2jdHbNGqu/16uq98WakmuQgANhsU98MRQwMP7N0iYxhUuybD/n3WzqlAMROROElzfY3NrXHrtTNFHTkMvkiGQNiZhGZ7ZzbPDx5IoKIXK2qZzDzd9F0T/0pEV2qqoeKyN8BwLZt27ap6hmq+l0RmQXgZhH5iohcpaqrwzA0RATn3DXOueta5buqeoWqnqWqT9dqte8DwPbt2zeKyBGq+l1m/giA7wL4map+jYj2S5LEA0AYhp0AvsvMp5577rn3Axi/YcOGxaoKEdkCYBYzqzGEMMgUWILRjXSopzfekFUf5wUKYXYQCoZhykcM08C+DMUMw7Rva8sHqHZCJFD1VtTDaYLuoe3xrLGH/Yu1NiZVtcYAQEVVy7vpmPNU9VHv/RUArgZQ9d5f473/qYj8OwBMmDBhPIBnnXNfAfAj59w5AK4F8DURmcfM1JrY/4jIrSJyq/f+XlV9vmVMPlEoFC4GgM7OznEicmPrB3hJRC4Tkc+IyI+897cFQWBay5lrVfVKVX30lFNOOUZV/aJFiz7YMi79RFQiIgbg2NrazHEHf7+70q1eGiwkROoteQkhOmIYp8DQBGUcYIVwOJMepCCAkBCooCAnUPVwXoU1rrXVoyi7nwgoDO1QyymwzTn34d7e3p8B+NsWFx4AYLP3/l4iuoKIHhaR/yaiLw1z6rp169Z57+cR0bUiAiIaVNU7ReR5Y0xcrVbPbf0ek1U1DwCq2qOqG4jofhHZUi6XAeC7IkIAvqCqIKItaG4LZ4jInxERvPevtK5fY+b7W+0eBGD78uXLx6nqd51z85i5G0Bore1rNJJsxuan1EumFo3w3mtKSupAMASNRJEACBk6ixWphWCaKs1tqegVUIWyiBcPIYhRQlLKhQccNDtW9YEIh0TkiciJyGFtbW29LfCCxx577PtHHHHEhdbabd77bzLzFap6jPf+X5o46Jf333//qWh6kP+P934HMx8F4HQA53rvkc/nl9frdYjIQbsw99SWy6opPvl8BQC6u7u3ENFfq+poVb1IRK4iIvHeX7dy5UpKkuR8Zka9Xv9WNps9n4j2B/DNkSNHnrV9+/ZRIvIhIjpMVZeoqlfVEcyQ6WNmpQ8+nyva9m4IO/XeQ1XFE6UKfYkUhyrTEVDEFkAWO4NuZAuAsPnDKlgFzih8ku0cU5y4NQiCxFrLAPYDUCOizxpjrgAAY4y54YYbvtwS5f1E5B9UdSgIgloURR8BIESEO++8c8qmTZtetNYeHYahdnR0wHv/pIhsrVarvX19fQsA5H71q1/dYq01pVKpkCRJXCqVaGBgwDcaDdfX1zcRwDELFy788JIlS96XJEnBOQcADSIKmfkSIsKwpXfO/bmItBljLlHVa6dNm/bIE088sR+AMUT0WRG5kIgmWWtfIWPcuPZJDJ9r90hIRVTEq5KAlBIIdYH0UCg6FMhZUvDvjSDVnZBhUhUSUijICxHCbDFXZGOMqKoH0KmqQ/l8/ptdXV0/rlar38rn8zs5hJmJmUM0jyPb4/j3h/ze+ylLly6dgr2QaepX3Hnnnefv7ZmdoyUamyTJWABoHvTtmbq6un4xa9asSQCuA7DSWvtSo9E4zHt/dbFYvKLRaKwF0E5EwoBENlKVMOPFkcJDCRBVUlEloLQTLgWz1987FAhImCECJVEh8Z6cdzBk20ITkIg4Y4xX1ZFoHuJM3XfffT/S29uLLVu2oFKp7HQ9/W8ia+2RzHyGqv6TiPzjsccei97e3kxbW9uZACYTURVNb7mIiIYmJIOwLUWqTqQVIqFEDFHV6nC7orDMBB22LOzhWbRC0LJRLalqGYqyQWAJVDPGVJIkqQPYrKq9AGCMmQoAaZpix44d2Lx5M/r7+5Gmbzn4822jVatWvei9/9M0Ted77/9j5syZawAk27ZtswCgqt0AtohIzRhTssZWDdvQkA4RtETaxAOqZSWWnXgR1Kr8/kTbG2ThtaAE9QQSZWIQ2EilFteyhoJCa4lxYMvf9xry3qNUKqFUKiEMQxQKBeRyudcVsXeC0jRFrVZDtVrFzTffnOnp6Tl2/Pjx944ePXrt9OnTzyGirY888sjLCxYsOERExhPRDGvtswACrz4m60pOqIMIBIX4ZqCYAWsZLXumAtid6z8A5DSvlgkKFkcMiBERqHUDiUu8994SkQCoEFF+jyPfhZIkQX9/P/r7+xEEAbLZLKIoQhRFbzugzjnEcYxGo4FGo/EqCejp6Tnv5ptvfk2dH/zgB8sWLFgAVS0CqHjvyTlnq2mFYF3VORnJICKwI2IFI0Qi7TCtLaYCVgnbAdoA6GRhaoPXhipIVJkEUCXP7CrleBAd2RHsvYcxpopmfMreaICZN6LpQWYRmZSmaeeuk7LWIggCWGsRhiGstWBmWGuxqwUFABEZ9ilCROCcQ5qmcM7BOYckSYbd/XuiTczcT80YHHjvZ6MZZ4O+vr5hx+14Va1Qa/M9WB0Asa+SUCcIRuAtg5QEBKDYrEJrwdhiIXhBRQyIJkMxQxQvkELh4RUq4kCJ2VHdOLiOx+YmmTC0trWwnQOgsvtoiegFInKdnZ3rRo0aJT09PTw0NAQAm0VkzvBzw5N/B0mMMU+pqhk7dmxXsVjkzZs35xuNhojICDSPRpPt27c/WSgU5hLRC95722g0aOPgWnbcW5VUBYCSJYBBChgQzWnt2J4BsJyheFkVr7Q6Hc2kZYU6ARSejCjZFN259UOrc6reOucMEfWpqnXOPQIAhULhN8PgMXNl3rx5Y4IgOIuZz46i6KyTTz55JBFVmXnFO4nYrmSMeTKKooEPfvCDs40x8621Z3d2dp566qmnxsxcArC1s7PzkVWrVi1X1QBAv/eeiYg2DK0upOgpiCBQIlIBBOrBOgTCCAAQ0jUQrGS1WF1vUPewLlTlKoQCOARewOqVUgzmtlXWTWuKiqiIVAAgjuOtuy1bgtNOO21ET0/PhO9973sQEXznO99BT0/PxJNPPrkDQAO/97C8k7RBVaO5c+ce19nZmb3yyisxZcoU/NVf/RVWrFjx/kMOOWQ9M3dXKpVRjUYjbKmGinOOnPPYWt04PZGhjHoQCZigAQsFpFwbxqlRpx6k6LI6gK5Kpz8zm20d0JHWQFAYTSUlALDexSNdEB+Y+nQxpZRlppSZ4ZybdPvttz9QqVSOt9Y+SkR+xYoVxx522GF4/PHHceCBB2LZsmWYPn06nnrqqQOZ+REiekZERr+T6BFR37hx47rWr18/NwxDvPLKKygWi3jhhRdw5JFHolarzXvuuee60jSdYFordxFJnHNI0rghiGc4jb3xUDEQEngyYEBrwx7KcuJHZzux1t79KZQ++iv5AHTnCadVBZGQhULh1SsIMfoe7KlsGRqTm5Q1xmkQBJtV9dijjz766f06bwAAEgVJREFUnpUrVy4EgIMPPjh300034bjjjsOaNWtQqVQgIjjqqKOwZMkSzJs3b/Xy5cstgFUA3rZF954cr6eccsrYxx57DJ/85CexcOFCDA0N4cQTT0S1WsWjjz4azp49+4l6vc5Tp049TVU3eu/hVXVbZUN/TH33k8c4DVRIiMFEohCjCIdXLC6VY+44DV+zACCEXiiWgnCkEp1EpKsEqqTEIsTq1Axg+eCy/kczp+QmqDZfuXpRVedNmjRpx9VXX32hiEBEsHTpUtx5551YsGABnHM47LDDcNNNN+GAAw7Al770pc8NPzdsUXe1rsOA7n4dBmjXK3NzgbHrZ2beWQDg7rvvxq233oqLL74YS5YswY4dO/Dkk09i7ty5uOCCCz4bx/FPRGSUiNydph71ap2W9T9eGGgsr4iqZSVVsLJ6Z5lIlU5srfmWAlgHtE7lDjgP5SjgAWb6MBTtoroMgpwoERTwniiJhwq5aPrxB+YOWwuQIaKEmWd573NBEHSoKosIpk+fjltvvRWqitWrV6O7uxvLli3DV77yFRQKhVeBtzcgd/2+exmm3bl3dy4kIowfPx4LFy5EpVLBpk2b0Nvbi+7ublx22WWw1ro4jgsARgJYVq/XUG/Uk2fK95+ypXxfrESGGUIEMhYGTP1ovQOYOr2+kcjvVt+K9c130cp4slyX4nDnBqYbRCAGkTZXUELIVtPeezeUu3rjOEaSJFDVpwEcmKbpLcMTnDhxIm644QYEQQDTPDvBNddcg3322ec1IL1e8d6/qryZOruDffTRR+PrX/866vU6kiTBAQccgOuvvx5hGKI15hki8lTz76lura/fUUt6F4siJIKCiREAakhB6BnGp1ST9lwbngJ2CfE99Zd4cPzIcDqg4xl4wQl64EE+BlyicCnYanHz4RMumviR9vO7C4UC5fN5JqKzVfXlKIomtzzGr5nwGwGwOxe+ngi/ntjuXowxe/s+0Gg0+ohofxG5o1KpoFqv6+LBn496dssPt6dcmWAtlCOCNRDKgJgxEopDoLRl60Cy5p5P4Hhgl/A2NbgmTuUGBeCBOUTokVZAtyiIFJSk5QmJlJKeyvaeer2u9XpdVPVxVZ1Zr9dv25PI7Q7M3sDbEwe+0Q+wt/b21vdwqdVqv1XVaar6eJwkqNdj9JY3bW9IKU5cZRwUDNPcuagBE2G7Kg5RAKnI9SD832HcdgJIARYOVdyknXtjoTpBoaRsTPOMHQy7fMutQy/qQzOr1arW63VNvd+kTc/NfO/9I3vTXXub0N5E9/U+v57Yvp7+VFWkabpYVc8DMJSm6aZyqcSNRk1fxOMHPb/5v+pQtWwgUBCxErGCiOJhXHYMuRkU4r7XAHj3aYhTAaC4rakI9dNkMMSWPBhMSsRKmjRKIyuuZ3Bzfe32crnGlVJJReQ+Vc3HcdyuqgPD4re3ib1ZHfhmVcDuYO4JxNaYetI0HYvmMen91WqVqo1YNqVdW2uutz9NSp3KTNpcxMEYgjEYVNULmvVxiwLVu09D/BoAAcAZXL6j7F9SBVRgiUwPkRJYCQaqrEoMWrrqp4WN2ZfmxXGtWq7UqFwuJyJyP4A5cRw/qKryelywNw7ck+58I336ZvtR1Uaj0XgewMEicl+5XPblcpXqtXJtk33x1KUr/6MAbnKdgQKsDFUVMTtUYFWBvpLvohRX7orZqyJU192K6tSz9Qv5HPcQaCpBZyvjRSiyEFIVkDioiBbL1W3LglGduWJ9LKDExnAtCIJEVU/w3t/MzIfsbiD2dn0jHbkrF+1qSPZkXHY3MMNX59ydaB5ePdNoNLZUqlVfrpSxOvO4earr5xvqvm8iGfggBFNIyiGYQwwQ4xwABqqLhmo+c885eJVf7NUx0gDE4iv9Q/JYc1+MDABvDJQs2DDYhlBmxD2Da6YNxOulW9dsr1TLWiqVtF6vrwawXFU/7Zz7TwB/FCf+MUuW1ylJmqY/F5GzVXVZvV5fWy6XaahU5q26asuA22L7hlbvR4a8NVAYKFsgMBACJZDm7mNHSZ41HpfujtdrovS7bkV58p/oRwpZ8zIIhwM0C0SLoBipCmqNnaHAhq3L7MT9D9mfhjIrrYRt3nu0fG9VAKd673+Npq8t82a5cW9ADdOb4bZdljfbRWSpNt9BeSJJknVDQ0MYHBqiwXRHd9+IriPvffpa4YBCE0I5grCFMRlSGFoF4DMt3ffDUtXLPfPxyzcEEADGnoNH01gWFLNmChQhgTJEOqiKQIQEAiPNU09+Zf3jfZNnH3yY9mVWasoFL16sMWVm3gzgNO/9KiJaq6qTdlfyewNv9+f+QNCGPz8qIgLgaFVdVK83egcGBk25UtWBel9f/4Q1x931yFUbYLWNIxgOoDYgDSJYE6IB8CEEjFKg1D2QdscVfHn9r/EaB+YeAdx8B9z0+Sgz8HxgeR6AMVB6hgzaVMk3Q/2JSQHvJOra+GTXlMPmfEi6o+d87NpTLyTeN5j5ZWae6b3fV0RuIaKZqmr3ZJ33BNzuAO4G0B7vMfOQiNyqzcBN8t7fN1QuN0pDJVQqJe2v9u2oTt9w0l0P/uNz3iQjghA2CMmEGXgOCSYDIqJuAk4AgHrDf7We6u/uPx97zO6x13fl1tyOtfucqRcXM+ZFAHNAmA2iu4gwRkBKos0jAVXy4vKvrHvslWlHHHZk2m1eQKJ5VfXOOauqG4Mg6FXVj4nIalVdpKoHqSrtsrzYed1VXAHsDaQ9caAQ0S0iMoqIPkBEDzWSZHWlXI6HBkvBUKWsQ2nf5uSA7SfeueTqFxPUxtpQAxMSmxBqAhKTBZhoBYALAUCBW3ZU/D6Lz8E1e8NprwACwKQv4nf1fvlUMWsJwEgC5oDpIVJ0EhGrJ6sAICCXuvYVqx8uzXj/YZPSWFbWelyHeA/nPRLvqwxa3XRN4COqugrNKPwx2ozifxVww1y3K4CvA95WAHdQ8xWHDwJY4b1/tlwupwNDVVTKQ9rfP6j19h3dsv+Ow29bdEWvUmO0CWBshowJCTZL3kQAW1pPTb1noPTK9oG0no7Cp9b/7LWi+6YAXP8zuMnn4rFG4kfnQ3MYgIgIU5jxDCmKCigBpE1xZlEfvPDSErffrFkU7BNQpSutxQ1PLo6zSerFi9RV/CvMXFXVQ1R1H1VdhGaIbxnAzgQ5u4vtLsUx8yMA7mPmbQAOJKI2VV2XJMlLtVqtViqVaLBUlUqpn0vloTofOhBVMptzv1h4dd4Yn7cR1GSJwwhiQhIbIjUBthBwJoC8ElzvUHqzKL5+/+l4zQuGu9Kbyplw4m04Ix/xjI68+W6r2gZifdI1dFSaEEtdOW2AJYG6hnqXEMaOnL7ptGO/+L5kjVks2/JjM5nIZKJAoihLmUyIIAjIGANjTEBEHSIyWUQ6RWSdqm5V1YqIpC3RDImoQETjiGgKM5eIaKOIDKpq4r2Hcw6NRgO1egzvUq3V6l5Hxhuys9OPP7T0lke7tj41nQNiG0FtBmojeBMR2yzIRNhKQh9U6L6kkMGq/7t6Ii8uXoDfvRE2bzprx0n/hc93FLiQi8x1zYq0CdAHvcdkV4V3Dupi9b6OgosR+wRGvU3PPuXSHcXcPiMGnvAvcJIZlwsjG2UzMESUzWa16SExZGxLGFS9sVbFK5SUAGBYWYoIMzN5BbnUgSCaph5xXCfvvSZJouVaw1NWejrfL3NK1a07frHwmpFsXcgRvA3hTRahNeRsHmKaXpZtIDoa0P0AoBb7SwZqEt+/AP/6ZnD5g/LGnHwbvtlZCAYzAYbzJwwo4U5xOl0aUB8jcDHUxUSuoQ4pJE0gmbCt9vFTLm4UM2NHDCxNlidDweiQOAyCUDkwFLBBEFhSZrVEqkDzHLEVAiA6PFBFE0pFkjhS9YjjVJ1Lkfg0sZ3SO+rI8NBSo7vvznuuz8S+lDMhwBbWhmRtVr3JgmwAmAhqAlolij+h5svfqMW4ZKiaFu49F1e/WUz+4MxFJ92GS3MR246M+bYSGEAizD8mJ4d6p+oa8L4OcQnUJzA+hhWnqU+gUdA2cPKxnylNHj/rmOrW9N7+F5JGOiQjyXIYcgC2zRejiVXFw5Np5Y3xMGxgxBMJPMSlFHtPUI1NG/eNmhNm8uODUzZse+nB+x78WVs9KXXaDMgYspyBNyG8iQATwIRZwIawYPOCQj4LICSFDNX9V6qJ5O5bgH/8Q/D4o3JnnfhzfC6yvM/IdvPXADpaLd0KoaJPNS+xmjSF1QYkTeEkVfYpGR8j9Q5WRKvjRkztPf5DC3j0iCkn+AQvlDdUu6rbXaPWn5KrCEEErTwXTTKALbDmRgSaGxNk26bmppoQc7p7ux546PE7ZHvfutHGUJ4DOGMRmEi9sSQcwgYR2GTgOCRvDFXVaJUU81sA9PcM+X92Trru+yT+8w/F4o/O3nbyrTiaGF8cUwgOIMZRreZegerDgB6YJiQSw0uqgYsh3sFrjMB5eE1gfAovHka9pjaM+ke2TxiaNnWujBkzOcxnO/KFXKHNBpnRAODSRm+lVh6q1odqPT0bkjXrnuW+oS3tLo1HsKGADIQDsAnhjEFAFgmHsDYCmYBSG4BMRgMQvQTQcYBOBwBVPN5TStd6hxvuPx9L/xgc3lL6u5N+hpGwuHl0u33a2N/nDiTSXxBIRHWCNMilMdQ7DSVF6h1YUxXvyKhD6h0CCKCCVLxa9YASKYlyK/AOIJAyCUFBDGImB4KlEEoMbywCCtQbQ8QhxFiEJqDYWLDJakBEm4g1UKFPDI/Rq16xY9AdZQzOXzgf/X8sBm85AeM5t8P0eXwtItYRbfZToOavCyDxKj81RCPgaKJ3iL1TAw9xCVgdvHcw6uBVm/pNvQIKpwJV2pkKBQCEFKoMYoKFITVGQQxPBsZYeLIwNoQQw3BAjiNEzNioQKzAebQzkJRW9lXcbXEqctx5uOryYUv1R9LblkP1+JsxjS1+MDJn7wkDuhKEHACQQqD4OUgExJPFq/EpqTglcXDqEXoPJYETDwbgROBVAQY7ABCIJQKYYQBYZogaWGMAMkhhEJiQPLMaG5BTlvWUsgXjvJahAxS1RqpfH6i5eYjxhfs/i7clj+rbm8VXQSf/HB8T4LOj2uwzgaF/0GZ2oeHuVqjq48zIQzHee4QiSLUZgwN4kDYdt0Kkqq38BM1XhYnAMMwKGDQ979y0rERIRbENQJWIPgDorF0m2Ei9Xt0/5N4njH+//zzc9XamRH5H0iAffiOC9gLOVeD8kXl7bxjyxYC+OqMv0VaoPsCEukAigNqg1EEEFlWBQKHUFC9SBoOYiEUhRDoIaInBiSgyBDpJoeN2m9qG2Mv1/SV3iir+s1zFbc9chLc97vgdzWR+uYIfugUnC/C3keUlHQXTaQiX7LUCox9en1XwIBENCqTcvM1FVe0gSAcMzYVgxN6a8IrrBit+IHFyrCF850Orcf/ll781Pfd69K7l0j/mJxhtLb4+ot2uDy3t1T30Vihxeml/2U1WxpVLPol3PA088O7/MwI6/ib819j2YDOb154vvBVSxfXdA+nEBz6Ns4G3T8e9Eb3mUOkdJsW++NT2UjpHVO/V5vrvrRfVh7f3pTNLdZyLdxE84N0HEEtOgMsRzukdcBUV2vRWwYOnbTuG3HZXw4J3wki8Eb2uQ/WdojW/RLz/n+CluKaZTMhzm4eJwB9aFHADFf1X7+X6h/4MG9+LubzrHDhM934KLyhoaSPB3/yx3Nco42+811UPfBbvWvD67vSu/0eb3enEn/K17RkeNExXvPHTvyfxeuVQQ0be9zn50hs//c7Re8aBw3T/Z+TScl3niuBm9cCbLLeXGjr3mA3yl+/1+N9zAEHQ6oA/rxLLBPF49o1Fl54vxVJ08Ge/kwvkN0vvPYAAHv8K6ur8BbVEnlNF6XUArNQS/ziJv2jJ5/Cm07W/k/SeWOE9UddvUJ5+pimpYhODTtyT1Y29fsOrv2fxhXj+vR3t7+l/BQcO0z2fc0ucEyeil+7OfV7xFYXI4gvx4Hs9zl3pPbfCeyA67cfmFiaziVX/BgCUcL1XGf27z/vz8S7vNN6I3t23oN8caW0//+lcF/0PC+4VIBJgZm2aPw3/y8AD/peJ8DAtOQEuZLfAQ0sK7Q0rbv6SE/Yen/L/017ojH8LZ5/xb+Hs93ocr0f/D6s769KBP+5xAAAAAElFTkSuQmCC\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic3bx5uF1Flff/WVV77zPeIQMJIYQxYRRBpBGcQFEEbVQQUXB6xW5tWx9+Cm07IYIitiJog2P7qu3UCN22aDs0KIIyg0CYyUhCyHiTmzucce+qtd4/zrkhQIIogz6/9Tz1nHP22buG715D1VpVS/gLktnZjg3P2wGz3RC/N9hBCHtjMgOxGjDZv3UAkyZim4EHQO4i2j3UkxXUb9kkcrb+pcYgz3aDNvLjOah7BSZvRnwLX/8D2axILM0Dtx9ODgGGt/P4GNgtqD6A764i3+iJ44dC9CD/jaRXyqzXrHs2x/OsAGhrL9sB8W/B7ARKwz/H7TAE2btAZj89DbAW6X4HHRknnzgW7KdU5fsyeMKmp6X+J6BnDEAzhLWXHYz4c3GlG8l2HgL3frDsmWqzR5KDfpnOykkIL8D4OHPecIcI9oy09kxUaut//CKCfp7qtMuwwb8DnrOd5nOcXIfJCryAOgEpASUwh5HiBMwKEAW6YF1chIghthtqL97uSxG5C8a/TXvsBLx9VGa/8Yane6xPK4C2/kd7EuWblIZ+itU+BMx93E1OFmLchqQpTmaDA0kAlyDSwSwizkAFfN84RAfOQASLCVACDVgAESOGDZjmSDwEtYO20bVVaPMCionjiPoe2eXNy56uMT8tAJp9I2X10GfxyQTJ4GtADn3UDd6NYv5niKtgyQxcYjinkCSYi3gHikeSLhDwXjHzTNlWB4hEYnRAgoUSmIIqxAQsYEFQA/JRNHZw+lqiTn90R/VGYuNKQl5j/cTH5JD3FE917E8ZQFv23b0oJf9GNnQd8PFH1y7rkORKLJ2BTxJcAqQOSQyXGEiC+IB4wZxHXMABeIeZQ3q/MBQRhdiDNGqCaMSiYTFBKIiF64FYKBbAigKLD6PFsYjt+uhOcwHdiUMRe5fMe8uSpzL+pwSgrfje+/CyK1n1dcBeW/7wMoZll4Kfhy95SARXMsSDSxxkIInifIK4gHpH6kAlggjOOwTBJEFV8FIQDcQCGIh6ighOFdMELQKYYF1Bg0KgB2ZuxG4kFqvw+cmoDG7V/cXk7Z9iulx2efvX/1wM/iwA7bLLPIe1v4m4RSTJuWDJI/+675OUB3ClCmSC9yA1cKn1dF3mSFLDRJEsAYk9wJxSTEzQGW3TXlEQC6G7sde/0gzDZ0Zlt5Ty9Arp4GBfnBWCgxghOkIhkBsxGK4QYgs0gHZAQxPtNLH41q2GH4jhNFRfwrzK20ROis84gLbkohLp4C/IuAXso1v+UFmLlK4gLc/Bl4AMfEWQDHzZIAFJhKQMLgWzhMayjTz0kyYbrt2T2Nq3a5WFE3HWqgYzNxeU81ao5ADVpJ2ldLI6G6YN+o3zStI+CF+9n1kvWcYub6hR330mIgEtIHSAYFgOVkDREegYmoO2QfOHCJ3X4thqDirn0rUX0Kr9rex/Uv6MAWhLLiqRDPwPafdBxN79SC3yS1y9S5JVoQpSEnylB5wrgSvTAzCt0Vqzmfs/32By8cs2xZ2vXGJHjo/KnlVz1aEsLZsXKVQkpIlXTHo6T8wFjU5iTELULFpEQmN8Gsub87lq2gy/5pUM7ftb9jtjgNJO07DQQHPBerMeYqsnztaGmBvabhNbVYivemRwfJWitADktbL7OztPO4C25KISvnolWWMN8OZHasj+L5Luih9QfAWSmvVEtwquAi4DyWay6aYHuO/8A1phYOkd9sbVTbdgVlYqJVk5wycpWZaRJY7E+2i44L2Y0Zv8CkgMCMQ0xOCKGAndnCJG8m5ueacba7pkw/Pksp2rvrkH+3/kXqY9fx+cbiA0HbEDdIzYBmvRE+12l7y1GRffsRUj/JBubR6xdbQsOK37tAFol13mOXjzzymNrQL+/pGnS1/FZXvg64IfAKkqSTnBVRRXEaQ8g7HFK7jnU/NHde7tC5N3RpKB4WqpKuVq2cpZSpJlkvrMSqVEvE8ty9JClSJJJIr44BwWY0xjNC9iWZ4HH2OUdrsjzpm1Wi3X6RTW7XZpdNviwsToQfodP92tPpj9z1nG8Pxd0M4mYtugEygaGdpWQgO05aCxFI3/+Mhg5WsUQ3vx0Npj5GVnh6cHwCVfv4x0ZAViH9py0WcXIpUDcPVIOiBIFZIauKrgKg6z2Sw8c0m72XK3uA+MuMq06dVqzSqlTGq1uqVp6iuVsqRpGtIsM++ceO8NEQQi0AGmuKAElAEfYxTAQowUeZBu0U3zTkc7nVzzvGOtVpdGqyHa3rTpUPvSjpVyreDgc/dGZB3aVrRlFE3QlmBtoxhXtPUQlr/nEVTceRQzd5c9/+GUpwygLf7Ke0jHykjrS1suavpV0vqeuDqkdUEGlKTsSQYUqcxmcvky7v38c+6yt/xqvHTwvEp90OqVTKrVitVqNcrVasiyTBLnUhEBGAXW9ssqYD2QA1MT3bQP4g7APGBOv0w3M4kxxm5RxG67nUxOTkq7ndPO29YYb9pQ55a1z3U/Opb9P3wXA7vtgbbXow1HbBk6aYSGoA2hmFgK+ggnxoEPYENR5r/3y382gHb/xQeQFm/Gr/0ImOuD9zXS6p4kg4ofBKkJyYD1gCzty9IfXlVsumuXm7NPrC6Xh6sDA1XJyhWmDQ1ampa0XM689z4FNgP3A0uBdr8vSk/veXpceFj/9y39/6ccAq7/vQLsCewHDMUYQ7fbtWaz6VqtXJutCWt3OtJubBo7rPjMXsmMA5cz/60vJ3TuJzQEm1TiREJsRsKEElqrcEWfE0UJcz4P2Q9kwfvv/ZMBtD98I6XW+QXZkj0Q9uzdLZcgA8OkdYdMF5KakgwK1AWfPof7Lr52vJk27qufVq0PDKTltOoGBqo2OFi3crmszrkB4CHgAXpc5vpgSf/7MFAD9gVe0AcHYAlwK3Af0AQm+gDrVmV2H8i5ZtZpt9s2MdGwRqNJq9WUVqsZ9m1f1BqqxAr7v++laH43sWHEhhAaRhjzMBkJExNgJ/XhWUK+YDVx9FWy/9nbnN4k27oIQK39FUqLFmLhlX1beB+U67jMIRlIGiF1kApen8PCzy0cYZ91Dw2/dadp9ZqrVWtFrVZLa7UyWZaVRGQc+D2wiR73zARKqhqccwqIquKcmzSz14rImUC135uWmZ0nIrf0fw+oqjjnhJ54d4AGPU6dJSL7VqvVoSRJOlmWSKmU+KxSssVjH6zOa/5g8453nn8bzz3tEHzpDrTrECeQKnjBJSmhdQ+izwEWkNx/Oez9ReB924Jpmxxo91+wl0rjjc4vO7d3wQKu8kP84CyyIUHqgh82/ICQVA7k3m9evybstXb98FtnD9Xrrj5Qp5RlWq/Xnfd+AFgILANSVfVAHZjVf4E1EZlhZs8VEWdm3xCRD/QBfqSjIhtU9SIR+XszUxG5T0Q2quokPV25EZjsv4wA7AI838wajUaDZrujk+NjyWSzyZzJ/3pwTnrffPb5u79B2wsJEylxUonjkTAhxPENaOcURBIADfM/6bT+H7L/6Uv/KIBmiN1z/tWS3TqESM81JMlXSQb3QIYgGwQ/CH5QoDaflT+7cXNjYGLl4Lt3qA/W3WC9rtVqxdfr9QxIVPVeYKNzzoCOqjpVLSdJUg0huCRJusC4qp5FT7wPAlYCP1XVkf5zs4DXAbsCtwN7OOfO7nNiRk+EWzHGwntvzrlSn0N3APYzszjZbE50Wu1sfGKcZqNtu05+ffO0aitlj+NeSphcTGyATkCcgDhmhInlWJziutst/E1D9v2nIx/rmH08gPd+7ijSpc/BRnpW1+RWXGUd6WCGH+5xXjJo+MFhRpcu6q6+a2jJzHO65UpdBgeqbmBgwMrlciYiqqqLnXOFqk7vc4WPMRpQTZJkB1U9FEhF5Fwzu9DMbnfO/VBEXqyqrwGmHKW5c+4XZnatqr5dRA7y3p+uqh8DVFVvE5H1QAtwIhLo6dSNzrkKsEeMMQ0h5GNjY9rpFH5iYizutemTrjTvuS0G91iANcYJE544HoljQpyMxOZcsAN7SM3+AHHPhbLvP/9ua7zc47jP7OPEhz+KdcA64MIdSJKBBxFFpPcGigmx1Tc/f9G0syZrtZofHKi5en3AyuVy0gfveufc5qIo6mZWAWaZ2Rzvfdl7/3AI4SozGzCztqqeG0L4ELCPql4QYzzezFRVH1DVB8xMY4zHq+qFwHwzOyOEcF6Msamqg865K2KMK0XE05vaTDOzUgihqqojqnqD9z5kWZYMDAwmpVJGvT4gS6af1baHbzyY0FQwD67nzBVngMNz4xYcbOXHLMRzzEy2CyB3nfdicQ/8FOvMRrsQu78A2xWJYCaY6zGwxf1Y+quwbNrp19QHB6u1Wo16vUalUk5EZGdVXaiqQ3meO+/9BhFZrqqr6OnAE83sH/ttV4HvAB3n3Plmttg5d6Zz7sNm9i0zu69fvqWqH3bOnWlmy83sAhFpiMh/0JtgO+/9e1X1TTHGATNbb2YPOefGVbUEDKvqQmBOqZS5wcE6lXpdqgODlWXTP/Iblv48RePeOOt5wrUvpZrvDt3/7WMxS9zi/+aezx2+NWSPssImeqbY4j23XJDSeszvBAJODFHBWcrIA1c1sr1zN7DHjqVKhUqlQqlUcsA8M3tQVV8MNEVkjqoGEVkLPKCqVwEvA3Iz+yDwG+BMEfmtmV0hIifHGM81e3T8Z+p3jBHgTu/9h4qiOM4591ERuTKEcIb0JCMTkSuAGWa2v3Nujpk5MxsFEhFZ7ZybWyqVHq6pOrQaJuPOcxvNve6sjy2+l+Gdd8EkIAJ9LFHWYFMLokXvN9vzQWCLE2ILgPbA53bS7qrfi3WP7l+6GvxcJPRG4Kwn5LE1l/FVu63e4fM31CsV6pWKZVlm3vshVb0S2BBjTAG890PAAar6ahE5EjjPzOohhLO89xeaWQ6caWZnAS/vA3Wlqv5eRFqPAbEmIkeIyCtCCAeKSEdEPqaqfw/MVdXTReRCEXHAe4B6COEqEbk7y7LNIQRCCEWaprO894eWsmyTxuhDCG7NzH8s77X+jBcyML2AsBIxQ0xwKgTdCcl/h9kRwAJh/fds4blz5aAzVz+aA7vtE527fi7WXz87/wCwO+ZAVIgFuODZuOqmkfrrJirV+k5ZkkiWZWRZtqOZbYgxHglUnXOJiKwMIdyRZdnVIYTXmdl6EXm7qq52zn1BRD6gqv8CnAtc65z7sarua2avF5GjeQyJCH3R/IaZLXLOvSHGeB498f+AmV2oqqu89/8nxjgmIrPSNL0qhDAjhPBiM9sdiCGEpvd+JE3TOTHGDZVyOSpUNtVe/fMZI9cNMn32LliMmBnmIs4ghnshHtHrybU7Iq9/A3DRFgDNEL1l/U6uUhzRN8wbUD+vF8PAepecoHEandburbkvv7mSpZTLFSuXywnQCSFc1xezIefcc83sOOCQoig+18fgy2Z2gZl92cyON7MvAb9wzl2vqqfHGKfW2rmqLnTOPaiqK1XVJUkyD9iD3grlPX0wNwIfU9WXmNmXzGyVmf1cRN7rnDtdVS+MMTpVfTcwA/gVsFBExsyscM4dVy6XnRmxiGqTw6/cYcbqKw9nmo6CbEREURFIDPW7IGETyAwIL9PO+oZZ7516gLOP/budTZfuIXp3FT89Q9yvcaUhpNTzKLuy4TJlcmLZWPqi+2P9wIFqtUq5XDLv/d5mdqeqntLXQaNm9gBwrIisMbN5fZ10sHPuJ8C7gbu9919X1XeZ2dH0ph8Xq+p/A8tFZJr1RObFIvICM9vVzB4Efq6ql5rZXSLyfOBvRWRXVf2EiMxxzh3vnPt2jPEdgJjZcF+kZznnvuGcK6vqwar6ehG5XUSeZ6YbBcOMxIrx20v5is2UXA0NPYesRgfqIf4Bs5l0H/yDWHUZq45eec63/jDpALTg1c7dNouox9Nafh1KgKRnrhWPakKINZrtlzSnv7ZWq5UlTb1kWVYG1hZFsczMFpnZ61X1VBFJrfeKvm1mL+nruLkhhKPN7MNmdngI4Twzu8Y59wHgWjP7sIhcaGbvV9WXqGpZVVv9UlXVl6rqaX0996GpZ/v68jwze4GZfTiE8BpgjpmdZWYvCSF818wwszSE8E4zO9HMVqnqMhFZm2VZ4n0qWVay1ow31Gg0DydaBTMH3qHiEGeoy2kuv4YY3wy3zUL1WOjLq133ritxP3w+2HSgQTrwc8p7DeBqDl/ueVxi1o7Nimza5VNFqVqlkmWSpulBqvrrT3/603bTTTed6pwrPVZ3/TWRqnZf/epX3/ye97znZzHGwVKpNEtEXhVCuL3b7dLpdGzmio9XZKBb4PIKoQXSNEKrS3dJh2LytfRiFqMWTvmDe+m3X5WYne30+kUbnFkvCC38L0U+HdZ2KO9uSMxw0Wh3xyanvXHCe79TImLee8ys1O125998881HOec46aSTfrd06dIlCxYsWHDvvfcu2n///fdutVqNPM8LYNqvf/3rI733d5xwwgnjU4MaGRnZuHHjxk2zZ8/eYfr06dMvvfTS/VV12pve9KbrZWrSvhWJCHfeeWdz8eLFr5kxY8YtL3/5y1t9cOyBBx5YMn/+/N2yLMtGR0c3XXHFFUclSTI8MDBw1THHHON/+ctf2hVXXPGyer1+/ymnnHJkURSrsiwrpWlqeZ6Lc07Gh49bOty4ZJB6UcXFnh+gvcbQfAbGlcDrwaabdcfMznYJt66Yha2+A3hLr4tuBGQIbWe0V7Yp7xlJY5mQPjfUD16YJok453DOzVTVmycnJ1/kvXdFUXROOumkF8cYJ0XkkBNPPPEgM/secBpwu4hc8etf//rI4447bvyEE07YDNwAvA04cqt535L/+Z//WTk5OZmedNJJP6PnsgJIY4xVYHfgqGOPPbZ26qmn8sY3vjE/4ogjqsB/xhhf6r1/N4D3/lxVPfG6667b0O12hy+++OJ9yuXywAknnHD9qaeeuujHP/7x604++eQlIvJKVf2ZiMzy3m/IshJh2iE1Ri/dn8I2o/lGOg8NQLeKCuDWYr04l0tX38rNuoOjU+zpdHHSW2EAKiWE0PO2FTW6K0qE8fWmyXBWqdfTNLM0TUVV91fVDVtzhqp+xjl3HHC+mVXN7FQz+5CZHaSqHwVYt27dGjN7jZmdr6r7hBC+r6qnq+q5ZrYsy7LEOSchhAtCCF/ql/PN7BwzO8HM/tBut7/Sr2uVqh5iZuc7514FnA98N8Z4ppnNz/M8AmRZNg04X0SOPeWUU64WkTkrVqy4sq8bVwP7eO/FewdpdQjxMwgTG2g9tAMxr6BqGBGTdAtO4QFH7ua7qLoXrjHvEQBtGmopph6LghZCc023M/yCr5mZgk2tCBpm1th61aCqJ5vZ9WZ2DvAZoGlm58cY/11V/y/ATjvtNBe4S1VPB74FvBG4EDhTVY9xzomqmpn9j6r+SFV/FGP8jZnd1Tcmx1er1dMAhoaGZqvqN/ov4D5V/ZCqvkNVvxljvDTLMm9mOOcuNLNPm9mNr3zlK1+oqvHKK698oZmpmY2LyIRzzkTEvEinW33ORbTXdqEbUQUjwUiINn0LTtaYi9meiWg8ACkO6GPQQaRCtBwfCyIlEEOHO6jf1bmkSJIkAENmtjaE8KrR0dHvAh/pc+FewMMxxt+IyDkicq2q/peIvK//tlm+fPnyGOMxwIV9Sz1mZpc75xYCRbPZfDO9KcjuZlYDMLMNZrZSRH6rqqsnJiYAzldVAd7br2d1COHMNE33VtW/FxFijIv6n2c6537rnFNVPRxYd9ddd80xs/NDCMc659aratk5twFI1dUXEJM2lnchRlwUMI+TKpCjZEjYT0PsJBJ1fxwHA2ByD6KG8wENAWcZBYKUM4b2ayeJK8eolqZJbma5qh5YrVbX98FLb7jhhi8fcsghpyZJsjbG+Enn3Dn9acyXVRURef+ee+65B70A0b/EGDc5514A/G0I4c0A1Wr1rna7japuvadwd9VHtkEPDAxM9EV4tYicEULY0Xv/9yJyboyRGOMX77vvPpfn+SnOObrd7qdKpdJbzGxP4JMzZsw4ft26dTNijEc65w4ErnbOdfO8GEqSxJLpB25ktFzDaUSDoNERDEQdjvsxDgQ7VFRDglgZo95TZPogJlUsRpCIRkEAqQ672ryuiQQRSVV1DzObEJH/k2XZJ/uK21988cXv74vyHqr6MTMbT9O0VSqVjqHn9OTyyy/fddWqVfckSXJ4lmU2PDxMjPFmVV3TbDZHNm3a9Gag+pOf/OSHSZL4iYmJep7n3YmJCdm8eXPsdDph06ZNOwMvufLKK1/5+9///tA8z2tFUQB0RCTz3n8QwLmesynP83enaTrovf+AmV04f/7862666aY9RWSWiLwjxniqiOyRJH5pURQastkxteowRW6g1tsVZgZiRHsIOBBjEI2VBNXe3kUAtSaQoma4QsEJFoQ0rbq0hjmX9yTKhoHRWq32yRUrVnyn2Wx+qlqt0g9R4pyT/pywBAx1u48E+WOMu91yyy27sR1Kkt7y/PLLL3/L9u6ZIhGZ3e12Z2/93LZo2bJl//2c5zxnLvAl4L4sy+7tdDoHxxg/MzAw8KlOp7PUzIaT3to+NwYMyUpYXiBqRAOHEdWDTiJTLkHFoYVsUYxCCwgQlRiFEAQtDEmHVBLnvS9UNdCLV7SA3efOnfuqkZER1qxZQ6PR2OJ6+muiJEkOFZHXmdmFIvK5I488UkZGRkoDAwOvA3YVkWY/LlOYmSVpDciGevtrQi/Or7FvPGg/YnCDc72NnvRKdEqkidDEaKK2DmMN4hLvk2az2eyISINe7GIDgPd+N4CiKNi4cSMPP/wwo6Oj9EXqr4IWLVp0D/B3RVG8Kc/z7+y9995Lgc7atWt9/5YNwKoYY8PMmnnezXGuRKBFpAk0UBqYTqDoFrxMSTDskTCJ1VCGCQgmZg4vZoZnMoa8UqkMls0siTHuY2YPbauzMUYmJiaYmJggyzJqtRq1Wu0JReyZoKIoaLVaNJtNfvCDH5RGRkZeOmfOnN/ssMMOyxcsWPBGEVl37bXXPnDyySc/L8Y4R0T2EZGFIhLE2ySiE5jMwBCi9QL+Ig7byotvWALxkXi/yRCQRMU5jFhYCXGaxDgeuk2DctL39TXpBcCfkPI8J89zNm/eTJqmU55rSqXS0w5oCIH+epZOp/MoCdiwYcPJ3//+9x/3zNe//vWFJ598MmY2ADRU1RVF4V0+TmahEYxpBBVBAog6ByJWe4ThIglqG3CyCmwepkNmKIZEwUV1DlTF8gZx3GBGGmN0fZ3x+B34j9Bm59xD9BjdqequRVEMbz2oJElI05QkSciyjCRJcM6RJAkissWCAqgqU/NIVSWEQFEU9L3M5Hk+NbnfFq1yzo1OratjjPvTC8azadOmV/bvmWNmDeecxBgTjZMSi9CIQYcR5zykeFUiINQQAZEHzeLaxDTeLUYKzEPcAWLcY6ZmipppjMFI8tEGzeXW9TtnWZYCrFfVA+jtBngUOefuEhEdHh5ePnPmTN2wYYMbHx8HWNV/BmDL4J9BUu/9LWaWzJ49e/nAwIB7+OGHa51OR60XtdsdyNevX39zrVY72Dl3dwghiTGaH1+UabGpFQszEg3OgSgizjnE9u8bkdscdqczC/ejyeL+Mm6WYQ3MopmoBefMJOk21laTxuKKmbrY83aPmlkSQrgeoF6v/wxARO4WkearXvWqHdI0PcE5d2KpVDrhqKOOmiEiLefcfc8kYluT9/7mUqk0/qIXvWh/7/1JaZqeOG3atGOPPfbYrohMAmumTZt23ZIlSxaaWaqqm83MVFVKnQdrne66ajRJXHSiEcQkGjaO0lvOkSxB7QHnE1lCHFyLWc+3H2l5xKtapqYuRpNue6ySFSsXhKCxv05tAHS73dWPEZ3k2GOPnT4yMjL3C1/4AqVSic9+9rOMjIzsfNRRRw3S24X1bJysXGlmpec973kvnT59euUTn/gE8+bN4/TTT+fee+89/MADD1zhnFvfaDRmttvtrK8eGj3VECzLVy0IjfGKRiOqOtRSU0vFaE3hRJi2nsKWJ2xuL9fa7Nc7t7HXtLNGjOwgaoWpYEoSY3emaPeAPG//CkoVEcmdc4QQ5l166aVXNxqNI5MkuQ6we++99yWHHXYYt912G7vvvjs33XQTCxYs4NZbb91XRK53zt1mZjOfcPhPkURk0+zZsx9cuXLlwcPDw6xcuZKhoSHuuusuDj30UFqt1jELFy5cVhTFXO990teteVEUxJi3he4+MXYjgDnUjAg4orWm9nJo2GGmm2bLEnnrzRP6k8MPe8QS41EwkwTFFIsaRIr25qt8vmY8uHlV770lSbIaOOKFL3zh/y5evPhKgOc+97nV733vexx++OEsXbqUiYkJ5s2bx2GHHcbvfvc7jj322MV33nnnI6HUp2nSLfL4PVJHH3307BtuuIHjjz+eK664gvHxcV7xilcwOTnJ9ddfnx1wwAE3NZtNt+uuu74GeKgoCosxGq1VY3lr9CoN7CjO1KI4ExEUxZNN4SSUXiwvu+YT/bCbbRLldoSDMTnSO1seogU1cTEXb2YyvvaOscHB31dG0zdrjFG893cDx+yyyy4bP/OZz5yqqqgqt9xyCz/72c94xzveQQiBgw46iO9973ssWLCA973vfe+cum/Kom5tXacAfeznFEBbfzrnEJFHfe87erdY8F/96lf86Ec/4rTTTuOaa66h0WhwzTXXcPDBB/O2t73tnd1u99uqOlNVfwVYu92VOe3rapMjCyei2lwfxXDOnMTgRQSTl4OB8QczVkJvcyPnvGnuJDI+ihWvAKaJ2kIzqmoiqhBUpNMZr8/ace8j1snzljrnvHMud87tF2Ospmk6bGZOVVmwYAGXXHIJeZ6zdOlS1q9fzx133MEZZ5xBrVZ7FHjbA3Lr348t2+Pex3KhiDBnzhyuuOIKGo0Gq1atYs2aNaxfv54PV+O33AAAECZJREFUfehDJEkSut1uDZihqre3Wm2X5+3unPy3x2548Kpu6sV7QROPpIL3XkYxDu8teev/Kjrjl+dctnpF71W1uFnjzvVHnIV+rSBCRDDBDDWl3GmM/LrUWTbS6XQkxqiqehuwT7fb/Y+pAe68885cfPHFpGmKc45SqcQFF1zAnDlzHgfSE5W+W2pLeTLPPBbsww47jLPOOot2u02e5+y1115cdNFFZFlGv8/7qOqt3W5Xut3cyvmDGzutkSs0UlLFMHEoiIlhbJjCR8PcIWLj1p4o90kvPegaSe/bC2wOcLsZY50CaRdYNzfJI6gbWL3LIe+euyh9+4ZSqSKDg3UnIieq6n3lcnm3vsf4cQP+YwA8lgufSISfSGwfW7z32/s93ul0NojIfFX9z0ajRbPZtP35wZyVt/zbKomTcyspVkqFSoaWUkSEGcCBmKy2uN9id9LCl8NWu7NU9UKsdHEf5YMFNgjgpLcBFpCiPbkTYbKgvXp9nnes3W6rmd0I7Nduty/dlsg9FpjtgbctDvxjL2B79W2v7anS6XQuN7MFwPV5nlur1RLXfXi9FRPtojM5B3D9bXwighNYh3Fgb65culgtfmEKty0A+qHWFZrvNG/LPEddI3WGiLkE8JiKt/TB2340viD9/X7tdtva7bZ18/xhM5swszfGGK/bnu7a3oC2J7pP9P2JxPaJ9KeZEUL4dYzxFBEZy/N89cTEpO902rpP6boDlt98adNjPsE0wSQRIxEDle4ULhp3nu/Xt696HIDy6qVd1Asql/ZMdXy7F5ssiUURvDcRr1i3NT5DWxsmKvnitZONhms1m1oUxdVmVu92u0NmNj4lftsb2JPVgU9WBTwWzG2B2O/ThjzPZ5tZmuf5NZONhmu22jpkS9fE9sho0R4fRhDvRRMxSxLDY2OIvq0nmXIJKm05bWn3cQACOHFna5hzX1+MM8ytS5yId+AEBMErsui671b3Gbjn2G6n02w0GrRarY6ZXQUc0Ol0rrZetOsJOWFbYG5Ld/4xffpk2zGzTp7nd5rZc4HfdDqdvNloWrc12dqrfs+rF1373YokvU2ECeC9+FTEsGQjPbcfxLlLXJJ/+lGYPcr0n3LPerS8D/DbHhfaWxOxsVKCeYdkHhMvRAvDS2/58Y0H1X9fGZ9o0Gw26Xa7m+htAH99nuc/2NoIbP35ZET6sRZ4ayCfTB3barvb7f48xvhK4A+NRmt0YmLSJpsNe/70m+tLbrrsRrUwLe0fB08F6Z2rtzGI7+jpPq6MoTRfTlo6sl0AATq+/U+az76h/1AVlZg6o5xg3uNSj6WefHz9kj1orrCdSsvWTUxMyOjoZtrt9lLgTjN7ewjhB8CfxYl/zpTlCUpeFMUlZnYicHur1Vo+OTnpGo0GOyVLVkvzIR1bt3yPzBNTJ5Y5LHGQelPE5T1JBHTObb7onvFYvB4HYO3kVWuIpRSTb/aXLSd6WJQ6c5nHSg4p9xqS+67+9tCe9QcPk+66NZOTEzI6Okqz1VpsZjer6luLovjZ1jrxyXLlE80Dnwy3TX0C62KMv1PVk4GbGo32srGxMRkd22x0143sOfTQYXf+5t/qpQQyJ5qmWOKQLDHxxiLM3tTXfV/TIknlnSselxXpcQACuEp+Tsx3HERp9Pz/7kWZp1tySEkg8bjUiROscsvln1v7kl2Wvrzb2LhhbGxSxsfGtNlsPqSqV5jZ60IIK1X1uq3r/1PB/BNBm6Lr8zzfqKqvUNX/nZxsPjw2ttmNj08Smps2vnju4iNv+cnn1qVi1VRESg5KhpQTXObIUXdEP/bRiPnsHZzaJ7aJ1bYuykkPt73xbbR+Zu8N2P6ibqycQrmMlZ1ZKYVyIi6VOHzzjz9/99ELlh8dWhvXjI6OsmnTJhsbG5sIIfwXsIOqHhRj/Da9I1mPom0BsD0An+iZrWg8hPDdEMLzgel5nv98fHx8cnRs1MbHN2unuWHkmL0ffMXNl3/+LtEwVErFlRJcmpiVykgpRcXcJrB9eqJbP9OL/5q8c8U2T7E/4WnN8J2df+iTtQKc3If7KyGyf7ONNbqUOoXQbFtoBaJKZc3hJ33igF89sOPVOcNzhoYGQpqWy/V6RUul8qCZHqGqS4CFMcZTVHvO2a0t67asLvC41cTUiuIxn+qc+w/ghc65nUTkd3mej7dardhqtbLJZlMzHXv41c/Z8MqbL/nMIrHG9EqCr1TEVVOjVibUM0g89wFTx14viWHHkLxz9du3h5Hf3h8A57x++i9jrLzV+ZZgzACe6+B3HqYhaIw4J+JMkbwIQyvuvHryZS9//i6tXB9YuSFMC0Xs5W2KoeVElgCY2dFmttjMfk7v8M1g//qWds1sm56XKRAfc20N8J/0gvgvBO4piuKOZrMZx8YmmZycYPPmUXYb3LT+pXtu+ptrf/iJEbHujEpKUstEaplRKxNrKaTCCoR3AB6RJTGfPekle/s5Px3bbuzhjx64bn9tx92yrPigS8b+AcgQNqP8ohuZ28zxrQ7SynHNDtIulE5wxaEn/PP6WJnDT24faJfSSlKtlsvV6oCVKxmlXgCpDOypqrur6m/NbF2Mcb6qvlh7Z+keJbZbr3+dc8E5d4OILPXe7ygiR3rvV5jZgzHGVp7ntNtt2p3cmq2m73ZbjeOfN1nz3TXx1h+fv2PJaVYtOStnSK2C1lJirUxeSlmPcQwwo7e9b+gifPlf5e1rthm+fdIAAoRvzXytSHcv51rn959aCdzcDcxsdPCtHNfuIO0ca+UaWwEGZu+16pDj3vc3Ny1Nfr1oXW1WuVz2pSyxUqki5XK2dSQuU9VhM9vFzKap6gozW2NmDVUt+qcvMxGpi8iOwK5JkkyIyEOqOm5mRVC1kOd0Ojndbod2u0On0427zypWHLlv53X3XfWD69ctv3V+NcPXMmeVBKplQqWEq5eQUsZalBfSOw2PhuqHTct3J+8e+dUfw+bJZ+345vR3kbTrkE8dR1iF8LsisGujQ2xFrNUmtrvUmwXdboHP1RUvPOmfNyYDO02//BbuGu9k8yqlMlk5w7uEUlYy55A0TcQliTkRw0xxHouxd2Cot3PT+kcbxEzEQIIG0SISo1qet6UoAt08aLvb1uGyrj/hMD0gTK7ZeP2PPj+zlGhazoiVBK1lZJUSRb2CVlMk9ayldzJ+DwBi9gG01pV3b3xS2Yz+pLwx8RvDn3RZdwzrgyhsxvhJMPZq52izLVmzwFq5SbdLaEeNndyZlOutw97wwU5anz39ZzeHO9dPMCvxaZp4j08z0iQlSz1Kz/XhxHdxRFR7ESvnPIqPUTPV6LwX63aDKLEXEy6ChVDkc4Z15G8Pyw4qJtZvvOm/v1ixTqNaTpRSySXVFF/NROslpJwY1RKWeBZjnIAw1BtP9gGKclXevfmzTxaTPzlzUfzawBnOhwSXn9dPDpZj9i1DDmp2oR0Iza5qJzhrFfhuR5NOQdENTpPKwNiBx5w6MXOXfY9YtiZcef097e7IhE2XLMlSvDjn8IngncfMgllvQ7JzIoqkGsRUC4kWpCiCodadPuw2vXi/cmXBTslRIyvvvfauX/37YNGdnFbNVDJHUi67WMmIlVSpJs5XMqRWwouzuzB5J5BhqMbkDGflivzD+JMG788CECB8tfZO8cVOzsd/4pF8pz9CGOjm1DoB1+yStgq1dk6RF851g/puTtE10iLSGNpx95EDjjrFTZu928taOXcvebizfMWabvfhTV3f6UI3qqC9o6WC0ywVyiXYeUYp7rZTWpq/c3WPWsYBm9c9ePU9v7lEx9Y/uEOaUMs8oZSQVTJXZF6tZ22dK2eESkYsJf2NU0I/LwKjhPSLEb8i+YfmD/5ULP7s7G321cphEXuv98XeiL2gf3kxwu8N269VENtdF9sFaadQ6/aSDaXdSMgLfKFoEUhiJCcpjw7OnDu+497P1xk77pqV6tNr5drAoE/THUSchLy7odOcnOg2Rpub1q3M1y26zU1sXD1E6ExPElLv0MzjspRQ8iSlhKKSkpRTJ6WUolZSygmZiNyH8VKmMs2Z3BhDtlyRf83e17r1z8HhKaW/m/jywIy65N+XJP4B9JGljsiPMTSiO3cCRTt32im01A3keeF8oap57nxhWsRA0lEwJQQlMcNCz502ldqk109BEwERJHEEcSQlB6knJN6lmdOYlpzLnMZKQpZlrltJ1ZVLpB63CiPF7PgtfTT3KYv+b0TKb5F/HN/852Lw1BMwXobX9dmZmJrL9K3Agv5fOSr/jmdGVJ2bF3Q76nxRqHZy52LUmCsuj06jqu8qgjmiEtTUmEqF0vumgDlx4h2Jd2qJgHcuJk592ROT1PlSopomzpW9xiwl896tQumAvZlH0gcs1sJd4oTAxnCenP3Udko8bTlU7SvMN/NfFW9XAJ9iKmVJL/vkDzEM0V0Ldb5bqObRuagaikBWmLMQCKAuGAFDowKO3gpASbwDBJcICeI08SSJU8kceZKSpGCl1EnqNWJuBYLD7C1bsmBCC+MTMcqrfIjvlQ+y/OkY99ObhNaQ+K8ch+OdPnG3KXwco7xVY/eqyY3iqRk6xyJZUFeEqC4oFOYExaKh9JzaPSMiGOLECw6HpKKWeMyLI/XqxVMIbq1Fmk7scIP9txphxymfiUGfD3zL/3/84ulMifzMpEH+Bmls8yaMU8y535rnNOnP8rdqeA3I1eKsbSolB4NRdFgMb0Y0wcR64mXSSzggggeCw40rTIizrqlUwF5msNOj+gArPVykhR6t8P27q1x2yHt42vcdP6OZzO1sXBjmKODDqvxeEjfN4ANP0JlRRG43GBdljN5OWDCrmWNYYAizgw2mP0EdXzJ0s1NeivDZZDNXP1U990T0rOXSty8wsyuchXMrTLjgmWhDjDNQ3a1kfEr+iY3PRBuPa/PZaGSKDKR9AZc47x6KulUuwqelbrnIqe5c+SdOFJ4+HffHaJse6WeKBKwyyVtDoQca/Eb7qbSfarHItVbovpUB3vxsggfPMoAAcjahW+KNVlhDlZVPGcDIWjFbF5U3yTNgJP7oeJ7tBqdo9F84wMOpivwjsmWS+6dSMLUvpsJ3Bz7CdpMkPpP0FwMQYPyznByUHXFy4Z9VgdrpzjE+7aN8+2nu2pOmvyiAAJvO44tmboNh5/1pT8qnPTpj+se3nRjx2aK/OIBmyKbP8FMzN6bY257MM+LkMjGtzQy89pmc4z0ZetaNyGNJBGtv4k2gczVy+5MwHHcRdVoROOkvDR78FQAIMO+LtIm8zYndr8bodqcrRkuwG73jXTudTeuP1/zM019chLemtWdzpCkvir2EZI8jBx9xCTfNOYvfbev/vwT9VXDgFM05m2sMgiinP477lNMd6F8TePBXxoHQW+6tOYsfFsrDpnyof/GiJGHmzp/mrc/2SuOP0bN7CvpJkICZ4+2rjF8G5RcmDDrHvjt7Xv3XBh78lYnwFMnZhOg5SRxF6hmhyUlyNs/o2dj/X9LKT7D/yo+y31+6H09E/w/wHJVcjfUH5AAAAABJRU5ErkJggg==\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHiczZ15vF1Fle9/a1Xtvc98780cEjJCAgkgiUwiIghCgqLIQyK2qI0D2g6PlmfbbaONCmo/habB4dF+tBX0KdC2PFGZB4GEgECYIQmZp5vc+Yx7qFrr/XHODSEkiDKuz6c+Z9+z9zlV9T2ratWwal3C6yh64YW8Y8Ex4431M4yhOQAtVMUBTOgRQRGEWvtBlJnRgGJABSthdIWHrIyI1l9y3339F154obxedaDXOsPGr2+anFL2TgXOJKZWEIYP5oslL0z7EtE8Zj4M0O49f5qGofKAF32GRTe1GnWTZOkR5DUi0muC0Nxaete7el/L+rwmALdde+34nOcPgei0ILK/j4rlLmbztyBMfkUyUGwT8f+ZNGojWeLeBchvjOR+Xvngqf2vyPe/iLxqABWg/p/+YqFR+WYQREsLXeUuMH8WQPhq5dmRVMRfUR8eqquTt7Din7rOOXsFAfpqZPaqABy88spjlPhfi8XytSbKfZxAB+3l0ZSZ7hXD6wkWCAggClURGWJSggUAUjivokRIoJpq6gkkyl5miOJYqNq9VO6xer3+UxfHp4P9l8Z+8pPLXum6vqIAhy+/fLYXXFksdd1go9wXQZjyggyJH1HDDyEIQ1iawMZAjAWTsWS55QHHbEREGQwPABAYZhLxzjDBIpOcQBx7BwhUvOtDkiXk/WGqcugeirbJxc1LGvXaqY5x7sTPf37NK1XnVwTgg1deGcwcHvkWOKpXuiqLiPjI3XIZJBv91lub59CORWBVAysmDKyQ8QgsQGSNsakaUhAYIAPqlE+hgHpRVeMB730IFcfOK8RbSVPHTghZCsn8IJI0hk/fA8WYXYuhovfVa8O3eudy69eWLzjsP87NXm7dXzbA6oXfnJNBflwaM+4uJrpgt6/fTlFwC+dyY7w1Fvk8KAwYHChHRsQGAVvrEBgSsGE2DtYATAwFE4MBQAUCgkBU4D3EewtRD3WK1FmkzsE7gstI00zQaoEynyFNNkucLCZg+q6lUpVLRoYGj1KWv53wla+sfjn1f1kA+750wblBYGcXunpOJcUBu3zrsIbBryhfnGaiyEghIoSRahiAcgEjyCkCqxwEFsY4BCHBEAnIIzAEsGEGRDVgYgXUiQAMcfAeEDXwqshSFe8t0syxcyRZTIgz0TSDacYkaapoNb2kySaK07MAVEaLqIRnGkNDv3ferR7/rxdd+ZoC1Asv5L6R+k9yudJTuXz+YgA7O3EOwquQjyoo5POI8oRCBC7m1YcRKIgIUUgcRSqhVUSRARtvrGEPkiRuVBsDw83B3m3Ot2KqDQ8TAJR7utXkcjpm4qSgOK4nn88XK1BlOC8iziBOPWeOkCTkk0SROqU0Jm00QEkKtFqqzVZLG406vHxol6q4pNn6XBw3jh23Zf3ZdN11/lUHuPpzn4t6Mr2h3DX2T8T85eeoYjuKuT9QobwPFfPQYp4oX4Tmc0AxD+RyanIBxIZk8hE8sx3cuLnv4ZtubD774MOz01bzQI4KjwTdXRt57NhhDYO00FXOAKA5UgsozUIZGOjOhkemSdI8NMwXnt7vsIVrDl20uDhu2tRxRtT5OCZOUvFJqhTHhFYMbbSIkpai3oSvN0Ct1lY0mqeAMHFn0UUuqo0MHDkU0Kn7X3FF8qoBXL34c1HXxNYNlZ5xawj0qZ03DN3I5UqMYqFgSiX4UhEo5IkKBUUhDxTy4FweEobF4R19g7f+6D8a29etPz6cPPGWniOOHM5Nn1qKyuUKk+UgCMSwigoLMwQARNr9ocucEUC9TzWu1WqNDRvrw8sf6HHbd7xz0uxZd5z0iY+VK+Mm9HCa1aXZJG01iZJYfbVJHDfV1BvwtQbQbDSlVsvB6+JdQHxvZLDvwDq5d8/86U/jVxygfu5zUV//4I1dXeN7QXTWzvdN+GNTKU5DpSJUKYKLRaBShi8UCaWCUr4Ijuy4NY8/9syNP/rPg6mYf3bSu9+zpTJj3/FhaG0YRWRsiCC0yFkLgAWgzFgSMkYBQL0n74iZJRDxnDiHLM3Ue4eklVCaZVlt3fr+3j/8YYrWG7NOOfeTT86cP+8AybIdaLRI63Uy9ZZKowodqZOv10G1WiLVxiD77CO7VPPqkaG+aSMjAyfvf+ONL0kTXxLAa9//fvN2p7+rlMdsYDbn7oQXhj+kSnkmd5cJlS5QpSwoly2KBUGpRFTIj92+YcP663/4g/2CSZMfnnrG6T6sdHUXopByuYKGoaEwDBGEIaIwJGOMhGHovKfEGPVBEGUAkGVJoEqGCGGaJoH3npPUiXcpxXGKJEmRpC3EcapxbWR463X/bZLebQvf95nPrpm4777TpNUYQD1WrtWdr9dCDI8IqjVItcpUra3WNPvMzjqpfH9kZOCACQGd/FL6xD8LUAHaccq7flEuj9/ARP+480YuvIzK3fO4uyzo6iLT1QXpKkMrZZhSyWTQiddd/r1n62lMsz/+sb6ouzKmmC+gWCxImMtTFAQmn89REEQuzIXKABtjhIgUAAPIOgkAAnQMlarCe8+i6tMkYeecbSapxs2WZGmszWaCRquOWt/g0Iaf/WxiJcxl7//8Z+ayoldrdaFaXTEyDD9cI1NtiBseUNtobHDN+FPP1Vkuqg4Pzph40w1nv2yAW4874dxKqZyzJnfZ6Hucz31fyuX9TM9YoKdC6KkIlbuYuyuiheLEbZu3rL3h5z87aNJp77lp7PyDpuZLZZTyEQqFvBaLRQRRzufzeVimoANsAMB2AFsAbAbQByDZDWAEYAKAKQD2ATAZwBhVhXMuSZ3nZrNhm/U6teIUraSl1ZGGDj3xWO/263+76D0f/chjEydPmaWN+nYdqTKGa0B1WGRomDA8QjpYfRZZ/HejdXRZfF6tWfeT77rte381wIHD3zpfQ/s3xfKYL0GVAQA2+iF3lWfpuDFK3V2Erh6Ysd2qlRJToTD3jzffcseza9dMm/s/P7clX6wUKpU8R/mCVkpFLRSKEgTWWmstgEEATwNYC6Cxh3IJgKM6fy9HWyu1c4861wUAswDMA9DtvXdpmvp6vW6arVQazRparRZqQ0Mjq//9+/vvt/9+a4898Z3v0Li1CsNV9cNVz4ODVoeqIsNDHsNDG5G5tiaqukZ96JLM61WT77/nqb8Y4JPz54djw8LN3eVxUxS6PwCA7a+op9JF3V3MEyYQuiqCMV2M7m4gCg/6f7/573uauUJj9t+eXSiXyzaXK3JXuaiFQh6FQsExcxnAJgDPoK1xhHbTpA6g7g6UgwAcDmB2pzhrANwP4KkO7CoA34HoOq9jOp/b13ufJEmi9XpDq9Um4rhOtVrVr/np1Y1CKy68532nH0NZ+gQGhtQPjxCGhlX7Bw2GB70OD4/A65JOvqtGagPbJrK8kx56aI/TPrM3gF8ZO/HfugpdG9W5U+EcyPmnqFioarEYULkCKhWEymVGpUxgc9A11123ws7cb+P+HzprXHdPt+0qV1y5VOCuroqGYRgZYzIADwJ4Fu2mWQHQIyIREUUAciJCRJSo6qeI6NsA7gZwO4ClqvppIrqpU7xIRPJElAfQg/YSWQJgG4AhZh5rrc1HUZgGATMRGRuGKM87yA5v2Tz02N33NOfPPeBQUfSyS1nTDEidauqIUie+Uffk3AQ4NzYy9prBZv307/bt+N1LBrh1zpwDQoTTAzIXtKdO4jSMbuJysULlippKCShXiIolImsPuf6m3y+LDpy7ZeYZp0+qlMvc091FpVJJy+UyBUFQYeanADwCIBWRkIjGiMg+qjqWmaep6nxVPY2IjlLVJ4joMACnAzihkxYR0TCAR1T1M6r6FiIqEVFFRHKqWlFVUlUhohjAWiLKjDEzoyhyxhhlMrCWbGn//UrVoaG1Ty9dGs+fvf+bSWgL0tSqy0BxBmSppSx7FnE8H84zvBzjnLvhf47r2XZpf//AnwWoAFXHTfivclSeR0RTAYA4uJIqpamolJkrJUi5ApTLxLlw9u0P3HdPOnnS0KwzzpjQVSlTsVhAuVxGoVAoMHMOwOMiMkBEkYgAABMRqSqJSAqgn5mfVtVFALYC+Bu0jchVAG4AcAeAJ1R1tqq+T1WfJaJ9mPknABqq6gDEABLvPYjIEFFJRFJVbTDz5CAIDDO1mNmoshRnzOgeWr9+oPfZZ9fOnDr1MFHpJ+cUzqv6lJH5IpL0VqgcAQChCcstFy+6ZKD/Z1/7cwA/PWfeCcWwMMhsP94mqiu4XAJVykzlElG5i6irCCrku9f2bnnk2aGB8rxzz43KlQrKpSKVyxUtFAqWiLyIPKWqCYBJqlpBu5/qEZGJzDybiN6jqkd77+8hoveq6nYi+g4RBar6QQDvBPAOAAuY+W4APyKiBao6A8AtqvpFAAtFBERUZOYKgPGqWlTVHiIa6qRyEAR5a61T9T4IIi4fOCdcd+/SfJGwprvctQ9EEnZi1XmFc6Qu60Ka1gGaDMI+AfFlQ2PG5i7p37F+V168u/YJ9KvWRl/a+UCY+xNyUai5HFMxUuSNIoyQthK6f+XKhQed9/fVUqlIpWKBS6WSz+dzQfurcC+AQe99WVXzqtqtqhNVNSKizap6u4iUVLVFRBc5574IYI6IXOK9f5+qiog8IyLPqKp4798nIpeKyH6qer6IfNN73xCRCjPfCmCt954BdAHoIiIjIkUR6RORZUSURVFki8VykM9HWiyW+KC//3zz/pXPLEySRCm0RiIryAVKuZyafJ45Ch4Y5WBN9EURf7HuZnifp4F/N3f+WwthfoTJvL/9Dt2ihahiKmVoscDIFxWFAlEQzvvtI38anvPpTy/vmjC+p1AqoburolEUBUQ01Tl3D9rW1DNzQ1VrzJyo6j4AFgFYSERLVfXdAC4HsICI3gvgSWPM94joZhFZq6pxpznfraq/Nsb8UUT2AfBhZh4G8FMAxxPRLar6BQBv8t73qeomADuYuQmARKSMdvew0FrTMIYVRAAQFvaf89T9N/x26gGTpkwn7/uQesCnUPHkY99NLlkNYH8AFct6SX+lB5cO9m/eCXZXgF79N4wJp47uvpgo2CxhYYoEgSIMlYKANLB2Ze+W28v7z0m7pk6elM/nUCzkuT20w1Tv/UYAbxWRhqqOrnhscs6tIqJbARyvqomq/j0R3QbgAiK6Q1VvJqKzvPcXqT5//2f0b+89ADxijPmi9/5dAL5MRLc4584H4IkoJKKbiWiMqh4gIlNUNWDmfhExxpgtzDwlDMMtBRHyzvvuqVPGl/af9ejqbVvs3DFjp0kQOAojFROQKQQqLtymSdrmwflPESenAjjxBQDXzZ8/iV1wO4CLAEAJd4kNpyCygAmI2KgGBkjdlCd3bJ+54NOfWFbI5xFFEedzOW+M6QFwFxFtUdXAew9jTMV7fygzv5uZ6977bxNRwXt/gTHmUlV1InIBM38VwDs6oG4RkbuJqLkbxAIRHUdEJzrnDgUQM/OXReQTAKZ0NPBSImIAnwJQAnAnET3mnBs0xkBVM2aeYK09wlo7kM/nDaA44MNnRw9dePFb9ytVMmbaAGtgQqPehIAJ9oFmfwTp20E4gNn+cu3MgybOWvfE9ucBDBJ5X7EY7dynZWOfhjEzjbWkAUOJiRRmxY5t901bfHItly9MyefzUiwW1Vo7SVV3OOcWoz2wtcaYDQBWiMidRHSqqm4jog+LyBZm/i4RnSci3yaii1T1Hmb+tff+QACnEdFJ2E2ICKq6XVWvVNWVzPw/vPff7IA8T1UvFZFNxpgPi8gIgAnOuduDIOgGcKyq7uu9Z+993Vq7PYqifYhou4jXLPOFKSce9/tH7vtTeUHPuGlK7MFWOTAiQQAT8FPe+bcDQCksjvVu5HQAPwQ6RkQB8mtXTwf47e3GQn1gM1WtgbckwqywRN6l4zYn8dsnvfWt46PQahAEFIYhq2rivV8G4Ceq+v9UdbWInOyc+4S1FqoKVf2Bqh6qqr8RkW3e+8tU9XYi+rKqHui9vwzAuWhb6UcA/MY5d7lz7nIAv0F7HNkD4FwiulRV91fVL3vv7/TeXyYiW4jotyJyqKr+H1UFM5Nz7hNEtNg5t0lE7iKiX6vqnUQUW2tNEIQU5iLd9x3vGLclTY7zWdoDqLbrzGBr4GH3VcWAAgDTcbJ29b6jP6xBW99nGJOfEYy0ijSmO1TCLRREPchFZIKANQyFQqPPpunq8C1HPjl+3oHlKMpRsVgQZj5AVR/z3p/V0egBVV0JYDEzbxORfQGMVdXDAPyaiD6JtrH4oYh8TFVPQnscdzkz/5f3fnVnnDiLiBYR0WGqKqq6XkRuNcb8ynv/eGew/W4imi4iXyGiyQBOZ+afiMhH2nqBsdQ2FhONMVeKSAzgMBF5LxGtYOZDiagPquS8mrReWxFv3jw4hrlIaQpJM1LvGN4ZdemDrDRG1qx9VLys+TvJNl8OjFgASGBO6Db58ar+fbxu3dUye78WDBMRVNgAAguHYJ1Lj15w8onLC4UchWGo1tqIiLap6urO2OwMVd3CzFd0xmY/VtVvA/gCgEtFZDERfUlVv+WcewuAW4wxfxCRt6vqlzpGAp0BN9BeUACAQzoJncEyVPW/jTF3O+feTUTfVFUB8CXn3BeIaKL3/nxmvkRV/5GIvq2qARF9QlWnA1gqImuMMbOCIAicc1k+r5h16ruKDz348JGzDe0gIAUzQExEVmBMitVr7obXsyObW5e6+O2Av5oAYC3s78YVxhypinEA1blS/i3PmlnWUtFSPgcuFCjOBc3l3RXz5i9/MQnDkHO5HIVheLCI3P6Nb3xDly9ffg4zR7v3XW8kEZHklFNOuf9jH/vY74IgKFhrJwE40Tn3aJIk2opjevDib+WPGm6lhTTNS6OpiJvQetPrug01P1x9L0ELRNgx0By4byb8aVYBXg+qimIcACj0FsTJOOzojbk40wMSiM90HezwtFNOrhtjJodhKNZaUtVCkiSz7r///hOZmc4888w/rl69etWcOXPmPP300ysPPPDAuc1ms56maQag55ZbbjnOWrvitNNOG2Fuj+H7+vr6+/v7ByZOnDh+zJgxY6655pr5ItKzZMmSezvN73lCRHj00Ucbq1ateteYMWP+dMIJJzQ6cPSZZ55Zvd9++80IwzAcHBwcuPnmm0+w1naXy+XbFy1aZP7whz/ozTfffHylUnnmrLPOeluWZZuCIMgZY5SIyDBj2knvfHbDdb8pz3U+r+otvCayfYeXZnOcQm8BcJoqJgCcKDzbTcCknA0fBnAWAKjyDiWUtZmEfv2mxM6a2VAbdG8L7SGHHzTv0SAIgPZofJyqPtJsNt9sjDFZlsVnnnnmMara6PR3C4noJ6p6HoCHmfmmW2+99bh3v/vdI2ecccYQgGUAzgZw3C7jvjU33HDDxlqtFpx55pk3ABhdUg+89wUAMwGcsHjx4uI555yDM844Izn++OMLAK7z3h9rjPlkB/JFInLm0qVL++I47r7iiisOyOVy5dNPP33pOeecs/K66657z5IlS54mopNV9XcAxllr+7xX2ufNC4sPXH/DvLnS6JPEx1i/UaXZKrR/S9422qtYGz603mUT2AP7Rhxyu89VcOADYnWqHuLSfLpxY5dWa9vZmO6wUCiTseC2+swTkY2jlSciiMjFqvouEbkEQE5VP6mq56vqId77LwPAtm3btqnqqar6HRGZB+BqEfmCiFykqqvCMDRERM65S5xzl3XSd1T1a6p6uqo+2Gw2vwcAvb29G0XkMFX9DjOfDOA7AH6mqhcQ0aw0TT0AhGHYA+A7RLT4Ax/4wB1ENHnDhg23qSpEZDOAedZaMobI5HIltcF4DNd28IZ1OfGuCCiUycP4YJRTyCEE2Jc9zFwY3rf9NqDe9DjhQAUW3oNcipH+HcmEgw/5PhEnDIWIKIC6qtZ362POArCUiP5FRL6lqjVVvURVfy4iPwKAKVOmTAbwsHPuCwB+5Jx7P4BLAVwgIouZmbQtN4jIr0TkV97721T1MREpiMj7SqXS5wGgp6dnkohc2fkBnhKRL3Ys8JUicl0QBKYzhLpMVb8BYOlJJ530NhHxt9xyyzGde0NEVG3rAKVgbk6cP/97IwPb1WWO4T2p91aFAxUaM8rJGp4CmDkWoDcTuON+RqmSlgyJI/Uq3nhKM9SisKX5cJYxrNbamjFmnKpuc869s6+v72cA/rGjhXMAbPbe30ZE/wLgHhG5jog+O6qp69atWy8i7ySiSzuWelhVrxeRx4wxSaPR+EC7K9GZqlrsXO9Q1Q1EdIeIbKnVagDwHVUlAJ9WVRDRJufcBcaYA7335xIRvPerOv3ol9FeFgPaq9a9jz766CTv/XcALGLm7QBCIhokIOJibkY1CJs5X/fshYyqcyQGRBEpUkBDEM0jaGYB7KekC6EEgj7JAOC9aHvnQSAW/cVSuO/cuYn3PmLmnDEm7UzDFlQqlb4OvGDZsmXfO+yww86x1m7z3v8LM39NVd/mvf9+54f77OzZs2eoatF7/x1rba+IHAngFAAf8N6jWCw+2mq1SER29SmcucvQBsVisQ4AW7du3UxE56vqJBE5tzOrEefcZStXruQ0TT/IzGi1Wl/P5/N/Q0SzmfkrY8eOPaO3t3ccgGNEZIGq3sXMKRH1QFXGzp2bbSyWypPTXqgXdd6BvRdVZEr6FBSHMnCYgBMLIA+lCgAo6RZRhFAFeQ8IQSwQR2FPYZ9JW4MgSK21LCJzRaRJRB81xnwNAIwx5oorrvhspynPEpF/BlC11jaiKDoZnd73+uuvn7Fp06bHrLVHhGGo3d3dEJGHsizbGsfxjoGBgTMBFK6//vqrjTFBtVottVqtuNFooL+/X7Msc0NDQ1MBvO3WW289aenSpUfEcVzy3ouIxEQUGmPOA4BRS++c+6SIVIwx53nvL91vv/3uXb58+SwAE4jooyJyjqrOtNauIiJXnLoPJ8Woy2cZwYuyqIoqoEgFWMvAoarUDaBgAfCo96sqWiA15L1CmMApICDJh/l8qcTGGFFVj/aUaqRYLP7L2rVrf9xoNL5eLBZ3aggzEzOHaO9VVJLkuU1+7/2MBx54YAb2Isa0V9h+85vf/Pk9WaKJrVZr4iisUWC7y9q1a389b968aQAuA/CMtfapOI4XeO8vLpfLX4vjeA3aa4gCQE0QqURRDmlKgEK8AAqCihK0iueGV8yA2tGOkYAUIIEoqfckzpPLHBAEFY4iEhFnjPGqOhbt3bGZ++6778l9fX3YsmUL6vX6zqWnN5JYa49g5lNV9d9E5FvHHnss+vr6cpVK5X0AphNRA8BY770AEJsLSYytqMtUUwd4bbMDRBWNUV6AWn6e87WSV9KmGtRhUGdwPYCpE1srzmVE1ErTNFHVLd77AQAwxswEgCzL0N/fj82bN2NwcBBZ9rKdP18xWbVq1ZPe+49nWXam9/4/DzzwwGcBpNu2bTMA0FmE3UpELWNM3RiTsA1CgOpgqpNBnRk1z9QU2J28aFT7dr5BmldFyQIqIPKq5OFh4evqfaiq+c48dC6AjXsqrPce1WoV1WoVYRiiVCqhUCigs+D6mkmWZWg2m2g0Grjqqqui7du3Hzt58uTbJk6cuHb27NnvJ6Kt995779NLlix5E4CJqjqHmR8CYLMkSQP4qgrKBJAwhKBgJVZyFjs9jwEreE4FhajIopRAGSAGkRG1CJwfcnEqlMtZIhIiqhNRcc9Ff07SNMXg4CAGBwcRBAHy7QVYRFH0igN1ziFJEsRxjDiOn9cCduzYcdbVV1/9gs/84Ac/WLFkyRKoahlA3XtPzjmbtVrMiWs66BgQiIWcQpVZQwJ17eQFwBLQC9UNIEyHoqJGYwhIIO21QvbepGktHhpyQXeFOyvNDeCFHvi7yBAzb+zkwSIyLcuynl0rZa1FEASw1iIMQ1hrwcyw1oKInmcQRGR0TREiAuccsiyDcw7OOaRpOrrcvyfZxMyD1PbBgfd+Ptq+NhgYGDgJAIhoHwB1dFQrHhlxuSyteUg3g4xv93VKYFJomaCA0hoCtliBPi4EQ8B0IZ1LgscZUKfwgIoQo7S9v9nYtDGMpkwxYWhtZ2B7SCfT5wkRPU5ErqenZ924ceNkx44dPDIyAgCbReTg0edGK/8qihhj/qSqZuLEiWvL5TJv3ry5GMexV9UxqjoTQNrb27u8VCq9mYgeFxEbxzE11m8Mcn39Dc8QFaiFAkSkKoZID1YlgOQhQB9lBT2tKqsAgBTjhaTmSb0C6kDGiQa8dWuhsWZ9TtVb55wBMKiqxjl3LwCUSqXfjsJj5vqiRYsmBEFwOjOfEUXR6SeeeOJYImow85OvJrFdxRhzfxRFQ0cfffR8a+2ZQRCc0dPTs3jx4sVpZ+q2paen596VK1c+pqoBgMHMeyYiaqxdVwy2bCtnQoEA5Nu+TJ6gI6o0BgC86rMCeoYZfnXm0+0Ytc3CDXiygASqYK9MNFwrNNavnZ2mKURERaQOAEmSbN1t2GIXL148pq+vb8p3v/tdiAi+/e1vo6+vb+qJJ57YjfbK81/syP1XyAZVjRYsWHDc2LFj81//+tcxffp0nHfeeXjyySffsmDBgg3MvKNer49LkiTqdA11FYFzHsn6TbN4qJYnBYmCFQjEU6BKzVFO4tMdgF9rCVib+Oy00OQAAMTUVFEVpUyhUKhFko5F4g5Mk+QOIgqIKGVmOOemX3vttXfW6/XjrLX3EpE89dRTxy5cuBDLly/HvHnz8MADD2D69Ol45JFHDmDmewE8rKrjXk16RDQwadKktRs2bFgYRRFWrVqFSqWCe+65B4cffjhardaihx9+eG2WZVOMMRYARCTN0hQuzRLN0nk+i58ASAOwCOANgcHaVGlb4KbPxjtgjd0fqK4Uf/ROWyxqASYlsVBWBbwCCPv778x6dwzQPvtMMsZoEASbVfVtRx111E3PPPPMzQBwyCGHFK666iocwXXn3QAAEZlJREFUe+yxePbZZ1GtVjF16lQcc8wxWLp0KRYtWrT60Ucf3Wl+X6lB954WXk866aSJy5Ytwwc/+EHcfPPNGBkZwTve8Q40Gg0sXbo0nD9//vJWq8UzZsw4RUQ2dgyVtjZtGgj6+m9X8EQAQlBWKBFYSBBqh1Mq/m3zgQvabrNAnwIPEHAEgBMIspJUIKTkARaFMQ89OlJbvjw/5vTT1DlHxpgnACyaNm1a38UXX/wxEYGI4IEHHsD111+PJUuWgJlxyCGH4Oqrr8acOXPwmc985m9Hnxu1qLta11Ggu7+OAtr1lZlBRM+7Hp3OjVrwG2+8Eddccw3OO+883HXXXRgYGMD999+PBQsW4Oyzz/5okiT/KSLjVPVGL6KNRoOaD64o24cfq3nIFEOkAlIDdYAnAb+jw+sBAtYBnV25v4OphWyHmPidALqgtEIIxbbnIkOg5KrDJT3ggLebgw9ew4aNtTYlonne+2IQBN2qyiKCOXPm4Je//CVEBKtWrUJvby9WrFiB888/H8Vi8Xnw9gZy1793T3vT3t21kIgwefJk3HzzzajX69iwYQN27NiB7du344tf/CKstS5JkhKAsSKyIm61qNVspcnd95yst92RWMAYYjEABYCxsIMKfQsAOPGXpz77w/cg6xkAArj7W65VHs1cGNsZpBYMQKAgiFIevX23tTas64vjmFqtFlT1QQBzkyT5xWgFp0yZgiuuuAJRFIGIEEURLrnkEkyePPkFkF4see+fl17KZ3aHfdRRR+GrX/0qms0m0jTF3LlzcfnllyMMQ2RZ9gtVnauqf0rTVFutBK11m/pNb98tqhIqoKTC3LbAqup3jPJpuVaXh/sTsIun0dMwf+zJd+0PxWRVPA7CjhSglgIJhDxAaSm/OffZT0+17z+9t1AscqlYZCI6Q1WfiaJoWmfF+AUV/nMAdtfCF2vCL9Zsd0/GmL39PRTH8QARzfYi1zXqdbRaseh//fe41uXf326ayZQA0BwMAlLJA8SKsUp4EwhbBlsjz86DPw7Yxb1NgEuc91e0C4qDFboDECK0zY4KyNdbU2iknqT9/X1JHEur1VIRuU9VD0iS5Jo9NbndwewN3p408M/9AHv7vr3lPZriOL5BVfcDsCxNErRaLcRbt/XpSD1zzXiSgtgAyvAwbSPSq4Q3AYB6fzmA/z3KbSfAEP7melqfNrppQqKtAIAhUAiAWD1BbO2XvxyJ7l52QL1eR6vVEieySVWr3vszvff37K3v2luF9tZ0X+z6xZrti/Wfqoosy2713p9FRMNpmm6u1eucpqkU7n/wwPov/m+DAGsgQgAZAhkoWDQZ5TKY1ueuh7/9BQD3BxLfNtHXdHxAPgyiEduZzzJAEFI3PDxO+3aMYP3GvlqtybWRKrz3d6hqMcuyblUdGm1+e6vYi/V7f8n13mDuCWKnTL1pmk4CEKRpeme90aBGKxa/ZsN237d9OKnWxrQdKEm4c9qbCcPKdLa2rcEvAG2c0nZofz7AdjOWC0fSxuiZCMuCHQYg204IIEoA9f7kqlLlqZXvTONGrd5ool6vp9r2OD04juO7te3L8qKa8FIMyksxIC81H1WNW63WU0R0sIjcFsdxVq/VEdfr9a6nn17c95OfFRhCFkohFBZqLKAQ7kfHi62aNtcmkG/syux5AA8GtntxBwB0BwAo0YcD5WFDpAYEA4YBQzPXvf26/75vzP0PlhuNmtZqDWo2m4MAHgLw3iRJfrWrEdj19aU06d0t8K4gX8p37CnvJEl+h7YP4oOtOB6oVqtaq9do3EMrituvuW4ZMt8TgikAwxBgicDKwyD9CAAocIsTN3th22N2zwABIIb8r6G0vqzd4jUnLD4E1HS2OgJAQ6KkuXrlfn79Rp9fs663Xq9ptVbzjUZjjao+AuCDzrlfAHhFNPFlal6aJMkvVfUMAA83m821tWrVjNTqlHt27WbevJmba9bMMgRvALWAsBIFgIA1BRAqgKG08TBBzt+d1wsALgS2irhAIT8CFFCcYYFVAUHzgIYAWRKyUF37o590j9u89ah0247eWrXKw8MjaLZaq1V1uYj8TZZlN6jqyN60cW9a+WLjwJeibaOvALamaXoPgLMUWN5KkjXDw8M0ODSk0rejf0LvjiPX/OA/ihaAhUoHIIekxJCVpLqkzUB+KOLsfOAFUZH2eNDmQ9ClcG5J3uZmKBBCKWcNDQsQCEQ8wSiIRMT0Llvef9CCQxZsjIKnHbikIgKgGQTBJlU9xTm3GsBqANN2b3Z7et2TMdh1/Lf7GHBPY0IigjHmHu89EdERAG6O47hvcGjI1OtNifsHBg5cv/HYJy765gbrfaVAxCGBilDNEWyOTaxCbwJhHIGqQ0ltex/kcz9re9/+eYA/BtynYaqG6HHDZhEIE6D6EClXAHgHYgAsBPLqo833P7Bm4cELjt1A9GhGWnaqEOdbzPS0MWauiExX1Z9re+Qf7KkP2xO43QHuCmhP0Dpz4CERuVZVTyYicc7dPlStJrWRmtZrVY37h/oXbu094bFvfXuFSZOxEZGNFDYCfA6MHDGp6nYiHA8Aqc++lIn//RGQPUb32OtZuR9C1nxc/GcjGz5JoIMBmk+svyPQBAACUqiSVVJIlhW3LLt/5eGHLzx8o8NTKUkh88555wNAN1pr+1T1VBFZx8w3icjB2j6a9bwmt2uzHJU9QdqLBgoz/1xVJxDRUQD+2Izj1fVaLalXq+FwraZZ//DWI/r7j3/4m998gprNSTkgyIE4YmgBLDkiWKUniXAOAAjwi2ra2Ocg+Ev3xmmvAAHgH6A31lz6oZzJEYCxUD6YCHczoUeU2DMMQCBSylzWtfHOu2tHHnXk9DjLHu/1bqzKzj4sZrarARUROUnbHq2/Q9tFrmtXiKPXe1p5GZ2K7QZvC4D/ApADcDSAJ0RkRa1Wy2rVBtXqVQwODMvkoZGtbxoZPuyBr3ytj5JkfJ5gQmWTJ0IR7AsEWKb1qvhIh8uq4WSkVYJ+6N/30HRfEsB/B9zHgWVO3fjIBAsIGhHRDIY+xNAygRVQgjBDlcW7YN0dd2bzDj7YTjXWPJVltVbqOUvTXOYz8c63VGU1M9cBHKKqU1T1NgB3S9uzfho68/Pdm+0uyRHR3UR0B4DtRHQgM5dUdV2apk83m83myMgI1RpNHR4aMLVavX5stV6sbNpaWP71i4qBl2IEaF6ZCyxSAEsemgXEW1RwGkGLANxI1riaVL4yp30YfK/ykmImPApzap7t3EKQ+067ctigwP2xYlwq4AYLN6AcgzQW9S0oeubM2fTmv/v0kY8Q3bUxF3bnCpHJBYFEUZ7CMEQYtnfjmDnsaOE07/047/16Vd3S8RZIvPdgZgugQkSTmHm6MWaYiDZ2oKejO3Rx6rXValAaJ9qMWzohTre9FXTy4z//5dLtDz64fx7gHLdHE3kYXxDhHINyxFsVeCsU+wKQetb6p1T844fA3/jn2LzkqB2PAOeUOSqHQefoP2EThP6YkUxvgnwLoi2QT1RLTZUkUTXehukxXzp/MJw8acyt3j1RZTOhkM/bMBeQNYHmcyEAAxMwhUEgaLurdcJmAUTtU/LS9kfU9jUABrIkJS8eBGiSJJRlmaZpqtV6S3oEAydZO6+1bVvfsv99yVjj0jBH5AsgHzKFecAVwFIAyCi2EeMoKGYBQJrF5zUlSQ4G/s9L4fIXxY15BPhqV5AfsRx2INIQAdd7yP4tQJseQQvQJpRarC4VlRQQU+5qHP2Fz6e5CRPG3dGsr9geBOMtIQzDnLBlCkyAIGBSJTWGoKoZGePEOQ8AbK1R7y0RWSfKDEWaeiiJpnEK5zJkzmfjVftOyOXfnG7v237vv12Wk1qtEAKImGxOyOZBvgClvAHyIDWglar0PwjtfjiV9Lx61iq8CfjWS2XyF0cuegz4+zyHYRTkvwmAFUiJ8WNRPTSGaqzwTYEkDI2hpuVhHSFLoGq7K4NHfvQjtfEHHHj8mlZ828NJq9HnsnHGhNYGhgwD1rYNhaq6dgwZoDM5sCKs4jLy4uEyp+KzrDsIB95aKJSmBdEJ2598+s4HfvaziqtWe3IgChQ2b8hHUJ8TQp5h8gREIEtKjwP4KLU9yKSZxV9IJPmL4P1VAAHgUeAjAduppaD0v9CJd2oIvxKlcgJfjAHTFLIxqcaKLGXlTGBa0CxTsWK40TNjZt9hH1hiumfMeEcs+sS6VmP1hjh221KniXrK4I2gHXiH4cnA+DwZnRQEPCOfs7ML+f0j4gMH1qy/88Hrfikj69aPZ9FijthZUBAxfCQkOYYtClHI6nNgH4AalrThFWd2CAwOp41/F3FrDwV+/pey+Kujtz0EHMXgT48NS3NBGI0XuAqge5TkgFhJEqiPgSCBaiJwKSRogbxTNY7IZ6rGkWYmFw10T506MnXBm3X89Olhvru7GJXLlSCKxgNAliR9rWp1JBkZafZt2JBufPghHtmyuUviZEygFAREYlXZErkIGoTgNMewEYhCIMsBlCMERvkpJX07FPsDACnuG0rrazzk8gXAn/4aDi8r/N2TwJgU+HlPWHqQiL/y3B39NROJE0yJiVwM0VQ0TIE0ZVgH+EzEZKAsgwYegIdmClivnCl5B2KFSHv8xWyhQlBjLElIgCOQDUFqAR9AA8vsQ4ADgc8BgWVKc2DOqQbM2KSqAYHeN1pCUflaNa0fTsCHDgGG/loGLzsA47WA2Q/4Z8sWXUHhQ9o+nAwFUkB/ysAYrzQ1IU0cqYmVxAGcifoU4IwhHmIgTI4FIuQEUAWI2lNGKFQIHZcxVmuElVgQgL0RmBDwAZOxgIQEEylcpBQxY6MRJI4weo4PAJ5pZM1rMnHuTcA36bnjZH+VvGIxVB8HZjvghyVbvCkw9hsKLXRuCSn+rzIEqtMzgnEKTQnkPJwzCJwCHuqkbTUcAeIERNSeAajCWoZqe2XcctuqBJahgUdmGUEAeEswgcIR0XoVWGqD43ZFqZn49CtN11rkgE8d3g7487LlFQ1CqwCtgDmVIH9bDIsPWeJ/1vYUa/T+kwq9j5WKRDQ5UwmFkHmAPRSOQOpJhSHtlcT2fI6gCiYigWGjsAplkBoAAWAMOBPVbSBtEOhotCMZjVYwdioXN9LGmwX844Xwv38lQyK/KmGQHwQCMmYJRD6UM7nbQms+D6V9n/cQ6VYAdwLUIiBSRQVAt5IaArwHgbQ9jFESNu1ItKbtLIFhIlQ73UQOwAlQmrRbzTaIc5c3fHyiMv9Cvb/2sOdicb1i8qpGMleAH7T2BPL6T9aYu3Im7CHQeXsvDQ1CZQVAI6QY0fZ0DqRaVEIXoF0gXgDVMXv7CoVeFvt0KPP+WGPoWwucu/Pl9nMvJq9ZLP0HgXFg89U8h+uZ7SWvRh4i7vyGZNNZ3DcOA171MPDAawhwNL8/sfllzuQ2M/EL9hdejojq5bFrTT1M/RmvVtj3Pclr/t8c7gRswdjfFzjHoOfCh7wcIcU9dYlj6927Xo1+7sVkz0d7XkU5HnDq3ftjadWVdNPOU6J/bSLd1pBWb+bdktcaHvA6AASAo4Bq5s1Xmz79tQKpoN3L/6XJAy7x2VXkzdfe9jJmEy9HXheAAHA00sdJ6QFR/Ye/FmCq+g9edeURSF8z5/Xd5TXvA3eXZRxcam00zMDukeX+nHwj88nYt/jnIvC+HvK6A1SA7jPhb4wJqtSOofBS5NpMsuKtLn3Pha/iGO+lyOsOEACWAXm1we+Ywm4iLPwzjz/mJNuSufT049vHJl5Xed36wF3laKClLjhbJHtEQdW9Gw2qZ97f5505940AD3iDaOCo3GNzx4HxVlZz0Z7ui8o/KrLlxzr3x9e6bHuTN4QGjsrbXHwXRJywnP8C7WP5AkHkjQQPeINpYEfoHpP7hRrapEr/0H5LL2fR8W/z8d/gNZymvRR5IwLEnYBlW/i9gJiACNBYXfOU41/ExeL1kjdUEx6V4wEnLlxCkCogfeqaZ74R4b3h5d6wNP/esDT/9S7Hi8n/B3LrBEUxxEM2AAAAAElFTkSuQmCC\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"strokeWeight\":4,\"strokeOpacity\":0.65},\"title\":\"Route Map - OpenStreetMap\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" } }, { @@ -90,10 +94,11 @@ "resources": [], "templateHtml": "", "templateCss": ".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 200px;\n white-space: nowrap;\n}", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('tencent-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('tencent-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('tencent-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.24727730589425012,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.8437014651129422,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.7558240907832925,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second Point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.19266205227372524,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.7995830793603149,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.04902495467943502,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.44120841439482095,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"tmDefaultMapType\":\"roadmap\",\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"showTooltip\":true,\"autocloseTooltip\":true,\"tooltipPattern\":\"
${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details
\",\"markerImageSize\":34,\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"color\":\"#fe7569\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', amount = percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"labelFunction\":\"var deviceType = dsData[dsIndex]['Type'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}, ${energy:2} kWt';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}, ${temperature:2} °C';\\r\\n }\\r\\n}\",\"tooltipFunction\":\"var deviceType = dsData[dsIndex]['Type'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}
Energy: ${energy:2} kWt
';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}
Temperature: ${temperature:2} °C
';\\r\\n }\\r\\n}\",\"mapProviderHere\":\"HERE.normalDay\",\"provider\":\"tencent-map\",\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\",\"showPolygon\":false,\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"mapPageSize\":16384,\"useDefaultCenterPosition\":false,\"draggableMarker\":false,\"disableScrollZooming\":false,\"disableZoomControl\":false,\"useLabelFunction\":false,\"useTooltipFunction\":false,\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"showCircle\":false,\"useClusterMarkers\":false},\"title\":\"Tencent Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('tencent-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-map-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.24727730589425012,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.8437014651129422,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.7558240907832925,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second Point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.19266205227372524,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.7995830793603149,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.04902495467943502,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.44120841439482095,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"tencent-map\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"gmDefaultMapType\":\"roadmap\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"useCustomProvider\":false,\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"mapProviderHere\":\"HERE.normalDay\",\"credentials\":{\"app_id\":\"AhM6TzD9ThyK78CT3ptx\",\"app_code\":\"p6NPiITB3Vv0GMUFnkLOOg\"},\"mapImageUrl\":\"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==\",\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"tmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"
${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details
\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#fe7569\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"showPolygon\":false,\"polygonKeyName\":\"coordinates\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.5,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"useClusterMarkers\":false,\"zoomOnClick\":true,\"maxClusterRadius\":80,\"animate\":true,\"spiderfyOnMaxZoom\":false,\"showCoverageOnHover\":true,\"chunkedLoading\":false,\"removeOutsideVisibleBounds\":true},\"title\":\"Tencent Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { @@ -108,10 +113,11 @@ "resources": [], "templateHtml": "", "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('here', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('here');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('openstreet-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"markerImageSize\":34,\"useColorFunction\":true,\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"useMarkerImageFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', amount = percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"color\":\"#fe7569\",\"mapProvider\":\"HERE.normalDay\",\"showTooltip\":true,\"autocloseTooltip\":true,\"tooltipFunction\":\"var deviceType = dsData[dsIndex]['Type'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}
Energy: ${energy:2} kWt
';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}
Temperature: ${temperature:2} °C
';\\r\\n }\\r\\n}\",\"labelFunction\":\"var deviceType = dsData[dsIndex]['Type'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}, ${energy:2} kWt';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}, ${temperature:2} °C';\\r\\n }\\r\\n}\",\"provider\":\"here\",\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"showCoverageOnHover\":true,\"animate\":true,\"maxClusterRadius\":80,\"removeOutsideVisibleBounds\":true,\"zoomOnClick\":true,\"draggableMarker\":false,\"mapProviderHere\":\"HERE.normalDay\",\"credentials\":{\"app_id\":\"AhM6TzD9ThyK78CT3ptx\",\"app_code\":\"p6NPiITB3Vv0GMUFnkLOOg\"},\"mapPageSize\":16384,\"useDefaultCenterPosition\":false,\"disableScrollZooming\":false,\"disableZoomControl\":false,\"useLabelFunction\":false,\"useTooltipFunction\":false,\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"showPolygon\":false,\"showCircle\":false,\"useClusterMarkers\":false},\"title\":\"HERE Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('here', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-map-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"here\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"gmDefaultMapType\":\"roadmap\",\"mapProvider\":\"HERE.normalDay\",\"useCustomProvider\":false,\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"mapProviderHere\":\"HERE.normalDay\",\"credentials\":{\"app_id\":\"AhM6TzD9ThyK78CT3ptx\",\"app_code\":\"p6NPiITB3Vv0GMUFnkLOOg\"},\"mapImageUrl\":\"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==\",\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"tmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#fe7569\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"showPolygon\":false,\"polygonKeyName\":\"coordinates\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.5,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"useClusterMarkers\":false,\"zoomOnClick\":true,\"maxClusterRadius\":80,\"animate\":true,\"spiderfyOnMaxZoom\":false,\"showCoverageOnHover\":true,\"chunkedLoading\":false,\"removeOutsideVisibleBounds\":true},\"title\":\"HERE Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" } }, { @@ -126,10 +132,11 @@ "resources": [], "templateHtml": "", "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('image-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('image-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('image-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See advanced settings for details\",\"markerImageSize\":34,\"useColorFunction\":true,\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAA3vSURBVGiB7Vt7cFzVef+dc+/d90OrJyO/JSO/4ncxxfULMCYIAyEW08amJJgmM4GmnZjJdNq4gcSGzLQxk3bsaWcaaIHyR8CJrWAbpjgG/AhINsbYxkaSDY6xJFvSrrS7Wu3uvfecr3+cu1pbXhkJs/4nujNndufec77f+d7fd+4uw8gvIxwOfocBaz0e91yXyx0BgKyZiWUz5kcEvBKPJ18EYI+C5rWvkpKSyZGS8LGHGtbQR8ePUUdnB50/f57OfnqWWlpbaN++39O99fdQpCR0NBKJTBwJTfZFE4LBYLmh8+YXXvifKctWrEBPTze9+cbu8/3JVMoWNjwer3/ZsuUTvV4P239gP36yceNZW9CtyWQyei262hcB+7zurU/99Ge3r1nTgJdfevFsqr8/Wlc3rWbGzFkV8+fPr1iwYEEJgLadO3cmbr/jjohh6KXHPjxamsmar39pjoPBYHl5aUnnqZY2/b1Dh9LdPd39kUgk6PP5PD6fH36/Dz6fDx6PF+fOfdZ9+pPTgbq6Ou+aBx+0k/0DVYlEIjYcbX4tYM5pxeK/WKIDwM7Gxt0TJox/dtLESXC53JuHzvV4PBVHDjfvAYDZs+fonMsV16R9rYeM8XG1tbUAgMrKsrDP659DRJ5gMNhbaH5NTU0IAMaPHw9IPv5LAxORy+31AgBcLsO41lwAcLu9BgAYLheIkftLAxfzGgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4D/lME1ke7gDF8ltbOHe3W923oEwYi1jxftWfZWgAziwacZkd2pfyN96XN5IIu7dMtIKA9/TI+zqCnFps2Alg5UlojFnVqIHZUlO2sl4RyC4CU+SEEylux8Z/iyc7mrxw4U7UnYwvGpXMYKIgNGdwXC/76C48oRw3sDWfnCgIkARJXcpwbvpA1e6T0Rq5jDr8EAHKA6OpjUOJwfeXAJAEhAXAGgEPKq+dIMVJqowDO4RAAC0rHV21u5LijAJaABAOIAY5Oh15iFMgj1zEpcUuuXjpIWeCouxjAtnIZcGKA5AVFbRfazPUC50QrKe8+Qy8qiqjBYIODA5DgBd1pBO9WRg9sy7yOhXBca+icYrgTOUGOiKnIVdCdisAxJGBTPsYW0nHRrJqgfNmGVtiqaeR1xchF7Vgz40q/BUNmISlcL7CUgJAMnOUiVwEdF0PURIAAVHaC8ucbAiwcQAb1KQpwXMjFrhtYMcOVO8lhOB457ujcKZd9hBguSYwcelTupKyaQWKYJFEU4xJw/Dhfcw29ilSBcNjEoTucFnSnkeOOvvTJpcVC1cYoGB5NAGEQTukjMAzHoghJghyWCRjenYoTuZjKx8xJiwU4LrSZ6waWpIoBjTuRqxDHRUkSUMWAJAZp6QU5FqOw65HHapG3bGVcBTZXDI5VnFaFgBL1yC34uoBJqEJeIwD2MMY1ilZidAFEMlDOqm9UdpJ0ZawumI+LU9ArwhyqWxyNz14XsBAMUnLVH0ttGB0XococdCGWE3XhOV85MF1WV2OY3omK0S2SkxgYAZYYJoAUpcqEEjG/Ru80isA1ysMXYNCnCum4aKUPgTu90w3sFinXL6nO/MadCAhiKloxBjFMeSuK0S1Kylv1cE1bUVoYyHwhoI6bCswpjjuxK5u2G2lcti2jzNCRTluioHEVw52EBA5/2LKsLBL+h2gs/o+Fjpa+MqtmjCbkqQJSYFF3T3zRsPMvA75i7UiBA4FApa6z5+fNnbd6/frHADghk7QdlhAHdMY0KXkZAHAuozaRMDRtKYMdAYDVq1fjcHPTD860nZlsS3qsv7+/+6pNDr0RDAanGTrf85Onnq75/uNPIJ1O4+dbnj34Ot6B4eFLqksqUeEvgcflAREhZabR09+Li/EorLQ4eFv317D2oW8t0XUdu3a9jud/9auztqD6ZDLZOixwOByeouv8D1u3brtpxYrb0XS4Kfbj3//8VHC8d0nDLXfj67OWIeQJgDGADfoOAxHQl05i14l92PHBXiTPp/c/OrFh9vwF8yMnjp/A5s2bOqXEbX19fX+8CriqqspvmunDTz/10xkr71qFnY07Tr1i7aqsLg2Vb6h/GOPCpdAYgTPlNLmF5AzpvBRp74viX3a/hO6+ge47+hZG61fVTz9y+DCee27Lx15fYFFHR8cAcNkPuw2DPXfP1+vvvf+BB7Br967WX9Mbk70eCn33zlWoCrsgKAFBCdgy/2nLBCyZgCUSMGUSpkzC0G1MrKzE0XMt/la9I0QnM+cWL15cmkwmK1tOnwpksuabg8YVifjnhEOlj69dtw6nT51Kv2q96fYG4fG7gbJwFhn7cxicIJgEZwAfEiokGASpWG1KhvIwg1/91ti1N9DEJ7ZOzKxdt87T1Nz8A67jv2Kx/o85AJDk//zXjzzCAeA/D7zU6PZjkkuXcBuEjN2OrGiHabfDFB2w7HZYoh3mVaMDWWdu1m6Hy5Bw6RIuP6b87+HXdgDAww8/zIXgGwFADwQCFYFA4BuLFi3CoUN/6LRmyL/y6gSXTtC4QDTVgQo/B5iEJFJ6Rt64lI6Vfi3JYBFHd1JA5wIunUNIQvpr/C+bm5u65s9fWBnwe9dISWVc0/DNhQsX6gDwTuuhd3WNYOSGTjjSehGp7EVYsguWuJQfssu51wVTXIIpLsGWlzBgXsSRM5dg6Hk6uk787Zb39gHA7NlzDM7xoM4Yli5fvgJSSiRmmbP9HNA0Qm4D6axEc6uJ6eOzuCloQuOOjlneqiUx2BK4lDBwut2DTFaHoXFYGilaHEjMMOdKKXHb4tvw/nvvL9UZ+Lyb6+pw/PjxpOZhsziX0DigcYLG1QaEBD69ZKA7wRHx2/C7BDSNwEi9AEmZGmJJA/1Z9SJM12hwvcYBzgmaj89obW3pr62dGmCcz+cuQ68GgEtdl7oYU40CZwSeW+As1rmy5KzNkbY1WILDlOp71ubgnKA7czVO4NyhwQhcFS7o6urq5pzDMLRqnXEtCACpdCrFHOHlAsTgYEq0nCnj0jnBY6i8KCTLBxbmzB2yPkczmU4lAYAxHtKFECYAPeDzBQZD4GU+motMueXklECWc7QkSaVDGoTAVetz8AGfLwQAQoisbtt2N4BJZaVlpZQjkntdS8w5UFOFni0YLMGhWfny1rbVPVuoOVKyK9ZeTrMsUl7qAHdzkPyktzeG2tqbw8KihCQlPjVUl2hLBkswmDZD1mJIWxwDWTXSFkfWUs8sZ64QzlqHjiRA2tQ7ZcqUYCwWgyT6hBNjb+3ZvQehUIi52tje3M6FyHHIYNkOqM2RsTjS2cuAs+pe1uYKPLcBkduA+m60sH1+v5/t3fsWGGP/x6VkjR98cAQAMNc7bXJepAyWzWHaimjW4siYDGmTY8DkGMhqapgcaVM9yw5ugMOyeX4DkmGub1otABz/6DiI2O94IpE4E+3p+aCzsxP333PfAvOi2G8JBtMRbU68GZMj44Ao0BzXmgOsRk7spq1oWILB6rQP3nt3/byLnZ2IxWKH4/H4pxoAeFzuC21tretW3rUKnk5mtWiflzAGxhgDQ66IYyrnOnqzBFfDZjAdLk1HMnkpMWRNLldmFomamtrIL/71F+iPJ/8mnc2e4QDQm0jsOXfu3L6TJ0/ivtX3T607M26P6SzMWI5eB7ktPHLPc/MV5xwTjpe9sfLOu2pOHD+JCxc+fyeWSLyZdzCoWsvjNpqef/6F8KTJU/DDLT/a3jM90eDWCS5dqmDvxF7NCRSAOikQhCuMUXHMEDjm3v7jb/+oIRrtxpMbnuzNmvatiUSi7QpgAAiFQneXlZbs3rGjUauorMSmLc+8dShy7HbDELqeA3bC4GCScHxWSMDOgVuaPb2t+t3vPfK9O1P9A/j7v3vC7ov318fj8bdyWFf8YCSbzZ7VNHb+tVdfrV911ypt/bcfq52J2uTBg+//LhWwZ0nJYTtWf6WrcccDGFgLdn5nwkPVD9Q/MLOzsxNPbvhhNpUc+G5vPL7jcqxBjonozwEsBzD5lVde9jy5YcPqTZufKX90/WOwbRv7330nsffDt08dSB41EkZyHPfwmwBAZuTFsBm48GeuWfai2oUzp02fFjKzJhp3NuLFF/+765e//Pfd31q71gLwGYC3GWNNAMCIaBKAJwBUO3uQnZ2d/MyZNv1vn/j+LUuXLq/Z/MyzCIfDTmxW8Y+IVFyWqjKRQkDYNqKxGDb97GkcOXLk7LZt/9F8c12dqKqqYM4LYALQCWAbI6J/A1AGgKK9vSBhoa8vEe+N9TwejcZYU1MTfrN9O6puqkJDw0NYtnwFpk6dCsZUMrFtG22trTiw/11s3/4aotEo1jQ04NZFt6KsrJTCoZKtJaWRiGG4KBKJ5BJWnw4gDedAx+0yMJCywLnQGWOSMabV1NbikUfX40J7B367sxFbt25DMhGHZZkgAC7DhWAojOpx4zF3wS0YP64aVZUVYCoQSN2la4bhIsNlcOS73H5GRBUAHgcwBYABAD09PZROp1gq2V8WTybq4vH4xEQ8oSWSSfSnUkinM7As9RdUw9Dh9XoR8PsQCgYRCodESTj0x1Aw2OrxBXsDgYBdXl6eM2IB4CyAbZcb12wASwBMB1Dq7C4ACJZIJHstM5PWdC2TTmcom80wEtySAFwupum6wbxeDxeCuT0et8/v94UBTTrSJABRAKcAHGCMnbrKjy/bRBjAHAATAFQ5NuAF4IFqAtyOKzKo83MLgAkgA2AAQB+ADgCfAzjBGIsPxfh/6wbDK7xbMFYAAAAASUVORK5CYII=\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"useMarkerImageFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', amount = percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"color\":\"#fe7569\",\"mapImageUrl\":\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiCiAgIHhtbG5zOmlua3NjYXBlPSJodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy9uYW1lc3BhY2VzL2lua3NjYXBlIgogICB3aWR0aD0iMTEzNC41MTgzIgogICBoZWlnaHQ9Ijc2Mi43ODI0MSIKICAgaWQ9InN2ZzIiCiAgIHZlcnNpb249IjEuMSIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMC40OC41IHIxMDA0MCIKICAgc29kaXBvZGk6ZG9jbmFtZT0id2ljaGl0YW1hcC1ub2xpYi5zdmciPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0IiAvPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBpZD0iYmFzZSIKICAgICBwYWdlY29sb3I9IiNmZmZmZmYiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgYm9yZGVyb3BhY2l0eT0iMS4wIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTp6b29tPSIwLjM1IgogICAgIGlua3NjYXBlOmN4PSI4OS45MDc4NTciCiAgICAgaW5rc2NhcGU6Y3k9IjQ1My43ODI0MSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ibGF5ZXIxIgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzNjYiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iNzIxIgogICAgIGlua3NjYXBlOndpbmRvdy14PSItNCIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iLTQiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBpbmtzY2FwZTpvYmplY3QtcGF0aHM9InRydWUiCiAgICAgaW5rc2NhcGU6c25hcC1nbG9iYWw9ImZhbHNlIgogICAgIHNob3dndWlkZXM9InRydWUiCiAgICAgaW5rc2NhcGU6Z3VpZGUtYmJveD0idHJ1ZSIKICAgICBmaXQtbWFyZ2luLXRvcD0iMCIKICAgICBmaXQtbWFyZ2luLWxlZnQ9IjAiCiAgICAgZml0LW1hcmdpbi1yaWdodD0iMCIKICAgICBmaXQtbWFyZ2luLWJvdHRvbT0iMCIgLz4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE3Ij4KICAgIDxyZGY6UkRGPgogICAgICA8Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+CiAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+CiAgICAgICAgPGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPgogICAgICAgIDxkYzp0aXRsZT48L2RjOnRpdGxlPgogICAgICA8L2NjOldvcms+CiAgICA8L3JkZjpSREY+CiAgPC9tZXRhZGF0YT4KICA8ZwogICAgIGlua3NjYXBlOmxhYmVsPSJMYXllciAxIgogICAgIGlua3NjYXBlOmdyb3VwbW9kZT0ibGF5ZXIiCiAgICAgaWQ9ImxheWVyMSIKICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjcuMDcxNDI4LC0zMDcuOTAyOTkpIj4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM3ODciCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzY0ZTU5O3N0cm9rZS13aWR0aDoyLjk5OTk5OTc2O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmUiCiAgICAgICBkPSJtIDkwNi4wMzMxNSw3MDYuMTMzNjcgMy40MjkyLDE3Ljc5NTUyIE0gMjguNTcxNDI4LDc2NS4wNTA2NyBjIDE1MC40MzUyMDIsNi44MzM0MiAxNDYuMzkyMzIyLC0yNi4zMzQxNSAxNjYuNDM0NTQyLC0yOS4zMjAwOSAzNi4xNDM3NSwtNS4zODQ3NiAxMTQuMjg2NzYsLTYuNTI1NCAxNDguMzI1MDgsLTguNjIzNTQgNDMuMzc4MDgsLTIuNjczODUgMTQxLjc2MjIxLC0xMS4yMzA5OSAxODguODU1NzgsLTE5LjgzNDE4IDM5LjgxMTM4LC03LjI3Mjg0IDIyMS4zNjk5MSwtMC44NjIzNSAzMTkuMDcxNDEsLTAuODYyMzUgNzAuODI3MzUsMCAxNDYuOTE4NjcsLTEuNzI0NyAyMTguMTc1ODYsLTEuNzI0NyAtMzEuNjE5NywwIDExNy44NTUyLC0yLjU4NzA3IDg2LjIzNTUsLTIuNTg3MDcgbSAtMjUuMDkwNywtNjguMTI2MDYgYyAtNTIuNzk5NiwzNC43ODQ4NCAtNjUuODk1MSw1MS43NDg2NSAtOTUuNjM5LDgxLjQ5MjU4IC0yNC45MzEzLDI0LjkzMTI3IC0xNDAuMzk2NTMsLTE5LjEzOTIgLTE3OC45Mzg3MSwzNi42NTAwNyAtMTIuMjgxNCwxNy43NzcxNSAtNDcuMDAyNTcsNDYuNTQ2NTMgLTY1LjEwNzgzLDU5LjA3MTMzIC0yMC4xMDUsMTMuOTA4MTggLTU2LjAzNjcyLDQ0Ljk1NjY0IC02Ny43Njg4NSw3My4wNzgyNyAtNC44MDE0NywxMS41MDkwMiAtMTMuMzgwNDYsMzUuOTkyOTggLTIzLjQ0OTQ5LDQ2LjA2MjAxIC0xMC40OTY5OSwxMC40OTY5OSAtMzguMzc3MzMsNi4zODU2OSAtNDQuMDIzNDUsMTcuNjQ3NjQgLTE5LjAwNTAyLDM3LjkwODEyIC0yNS40NjUzLDEwMC45MjM1MiAtNjcuNjE3ODksMTAyLjA1MTAyIG0gMTkuMjgxNTEsLTYyNC4wMTQ2NCBjIDM0LjY1OTM0LC0xLjg3MzgyIDg0LjAyNzMzLDcuMzkxMzEgMTA5LjkwMDcxLC00LjI4NTQ1IDEzLjI4MTcyLC01Ljk5NDA4IDQxLjQwNzIxLC0yLjQ2MTM1IDY2LjgyODY2LC0yLjMyMDQ2IDM1LjMyMjM4LDAuMTk1NzggNjQuMzgyNDksMC42MzQ3NyAxMDEuOTE2Nyw1LjAyMzIgMjUuMDMwMzYsMi45MjY1IDQ0LjY2MjczLDM0LjI4NzIyIDU4LjUyNjk4LDUwLjY0MzkgMTcuMDk4NzgsMjAuMTcyNjggNjIuNzYzODYsLTEuNzE0NjcgNjYuMzA1NjYsMzIuMTM0MzMgNS4xMDI3LDQ4Ljc2NTg3IC02LjMyODQsNzguNjM3MjUgNi4xNDExLDk3LjM0MTUgMTkuOTY5MiwyOS45NTM3OSA1MC40ODY0LDE3Ljg1NTc5IDQ0LjYxOTMsODMuOTcxMTkgTSA1ODkuMTAyMjcsMzA5LjcyNzE1IGMgNC42NDM0NiwyMy43MjkyMyAxNS4wNjkwNCw3Mi43NzU3NSAxOS4wNjEyOCwxMzAuNjQyODggMC44NzIwNiwxMi42NDA0OCA1LjQ0NzE4LDI0Ljk5MjUzIDQuMjIyMzEsNDUuMjc3NTcgLTIuNTE3MjEsNDEuNjg3NSAtMTUuNzE3MDYsNDMuNjc3MjcgLTE1LjA5MTIyLDYwLjM2NDg2IDEuNDMxOTUsMzguMTgyMjQgMzAuNjEzNjEsOTMuODM3MTkgMzAuNjEzNjEsMTM5LjcwMTU0IDAsMjQuMTgwOCAtMi42Njk2NCwxMTUuMzkwNDUgNy4zMzAwMSwxMzUuMzg5NzYgMC4xNTkxMSwwLjMxODIxIDEwLjA2NDc2LDM1Ljg4MzMyIDEwLjc3OTQ1LDQ5LjE1NDI0IDAuOTQzNzgsMTcuNTI0NjkgLTI0LjQ3OCwzOS40NzAwOCAtMjguMDI2NTUsNDYuNTY3MTYgLTUuNDc3NywxMC45NTUzOSAtMzYuOTczMjQsMTAuODgxOTcgLTQwLjA5OTUsMjQuMTQ1OTUgLTMuODY4ODQsMTYuNDE0NTEgLTMuODY2Myw0My43OTczNSA0LjA0NjQ3LDU5LjQ0MTI5IG0gOTcuMzM3MzQsLTY5MS4wMDk0MSBjIC01LjAxMzMyLDM1LjUxNTk1IC00My42NTkwMSwxMS4zMTY1MiAtNTguNTM4NjEsMjMuNzgxMzEgLTIxLjMzMDE5LDE3Ljg2ODUyIC02Mi40OTk2NCwzMS40MzIxMiAtNzAuMTI0MzcsMzUuMzY3MDggLTM1LjA4NzYzLDE4LjEwNzkzIC0xMTAuNDcyMTUsLTE1LjE0MTk2IC0xMjUuNjE0MSw0LjI2ODQzIC0xNS45NTA2MywyMC40NDcwMyAtMC4wNzM1LDYxLjQ2NjQ4IC05LjE0NjY2LDg0LjE0OTI0IC02LjAzNTcsMTUuMDg5MjYgLTE4Ljg3NjcsMjMuMDE3MzQgLTI3LjQzOTk3LDMyLjkyNzk4IC0xOS43NDgyOSwyMi44NTU1NSAtNjkuOTc0MjgsNjkuODI0MTkgLTg0Ljc1OTA0LDEwMC4wMDM0NiAtNy40OTc0MSwxNS4zMDQwNCAtMy4yODQyNiw0NC40MjA0MSAtMy40NzA1Myw2My4zNDI4NCAtMC4xMjc5MywxMi45OTQxNCAtMC44MTAxNSwyMy4xMDM4NSAyLjQwMzQzLDI4LjI3NjE4IDQuOTYxNTgsNy45ODU4MSAyMy43MjA1LDI4LjExMjA3IDI0LjIzODY1LDUwLjYxMTQ5IDAuMjk0MTEsMTIuNzcxNDYgMC4wMTMzLDc4LjU5MTAxIDMuMDQ4ODgsODcuNjU1NDkgMi4zMTI1Niw2LjkwNTQ2IDQuMjIwMDQsMjYuNTY0OTcgMTAuMjEzNzcsMzYuNTg2NjIgMTEuMzU0MDEsMTguOTg0MTUgNC4zODczNyw0MC4xNTY2MiAyNy44OTczLDUzLjUwNzk1IDE5LjA1MDEyLDEwLjgxODU5IDQ2Ljg3NzgxLDEyLjIxODYyIDgxLjkyNjE4LDE0LjQ2MDU0IDMzLjcwMzQ1LDIuMTU1ODkgNjEuNTEyMTcsLTEuNDMwMzUgNzYuOTIwNzcsNi4xNDExIDExLjU4NTA4LDUuNjkyNjYgOC41ODE1MSwxNy45MzM0NCAxNC4yOTU0MSwyOS4zNjEyMyA1LjY0MDQyLDExLjI4MDg1IDMxLjUwMjYzLDExLjE1NjI3IDQxLjgwNDA5LDQzLjQ1NDg3IDcuNjA1OSwyMy44NDcxIDMuMDg1OTMsNDQuMTU2OSA2LjcwNzU1LDY1Ljg4NjYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2Nzc3NzY2Njc3Nzc3NzY2Nzc3Nzc3NjY3Nzc3Njc3NzY2Nzc3Nzc3Nzc3Nzc3Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Im0gNDMuMjc3ODgxLDUxNy45NDY3OSBjIDAsMCAyMzAuODQ4Mjg5LC0zLjYzODA1IDI1MC4wMDg2MzksLTMuNjU4NjcgNy40ODIyMiwtMC4wMDggOC42MTk1NCw1LjE1MTk0IDE0LjAyMDksMTEuNDU4NjkgMjQuNTk2MDgsMjguNzE4OTMgOTMuOTA5NjYsMTEyLjkzNTg1IDkzLjkwOTY2LDExMi45MzU4NSIKICAgICAgIGlkPSJwYXRoMzc4OSIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJtIDM1Ljk2MDU1NSw1NzcuNzA0OTQgYyAwLDAgMTY1LjUyNDU2NSwtMS42ODQ1NCAyNDguNzc5NTY1LC0xLjY4NDU0IDQuOTQ3NDksMCA3LjcyOTkzLC0yLjg4MzMgMTAuNTM3NzEsLTUuNzI5NzcgOS42NjEwNywtOS43OTQxNiAyNS42MzE5OSwtMjguNTg5OTUgMjUuNjMxOTksLTI4LjU4OTk1IgogICAgICAgaWQ9InBhdGgzNzkxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzguMzk5NjYzLDY0MS43MzE1NSA0MzEuNzA1OTMsNjM3LjQ2MzExIgogICAgICAgaWQ9InBhdGgzNzk1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzkuMDA5NDQyLDcwNC41Mzg1OSA1MjMuMTcyNTMsNjk3LjgzMTA0IgogICAgICAgaWQ9InBhdGgzNzk3IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzAzLjk1NzYyLDY4Mi41ODY2MSAxNDYuNzk1NDIsMS44MjkzMyBjIDEwLjUzNDAzLDAuMTMxMjcgMTQuMzQzNzQsLTIuNjM3MzkgMjUuNDg3MTUsLTYuMzcyOCAxMC40MTIxMiwtMy40OTAyNyAzMS40MjQxNSwtMi42OTg5NiA0MS4zODUzOCwtMi43NzM4NSBsIDQwNS41NjA3OSwtMy4wNDg5IgogICAgICAgaWQ9InBhdGgzNzk5IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgaWQ9InBhdGgzODA0IgogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDQyNi4yMTc5NCwzMTQuODkwOTggYyAyLjA2NzU0LDkuMDUyNzMgMS44NDE3Nyw1MS43Mjc3NyA2LjUwNzk0LDc0LjgzNDY2IDEuNjc0NzUsOC4yOTMzNiA4LjY3NTA4LDE0LjA2NTk4IDEwLjA1NTQxLDE0Ljg1ODYyIDQuOTAxNDcsMi44MTQ2MyAxMC44MTQ3OSw4LjE0OTgyIDEzLjA0NTc5LDE2LjA4ODMxIDYuNzU3NzksMjQuMDQ1OTEgMC44Nzk3Miw2OC40NTIxMiAwLjg3OTcyLDExMC42ODkzIDAsNi4wOTc4MiAxLjY2MDEsMzAuMTQ2NiAtMi4xNTU4OCwzMy45NjI1OSAtMi41NDA4NSwyLjU0MDgzIC0wLjI4MTYzLDEyLjk5MDY5IC0zLjQzNjc1LDE2LjE0Mzc3IGwgLTkuODQ5NDQsOS44NDMxMSBjIC0xMC4zNjcxNSwxMC4zNjA0NyAtMTEuNTkwMTcsNi41MjYxNCAtMTcuNzM4NDgsMTguODIyNzYgLTMuNTY3NzIsNy4xMzU0MyA1LjQwMjM1LDIwLjY3MjEgNy4zNTQzMiwyNC41NzYwMiAxLjkzMjE0LDMuODY0MyAtMS44NDIxNiw0Ljc3NzczIC0xLjc5MjM1LDcuNDQ2MjYgMC4yNTI4NiwxMy41NDQ4MyAyLjI5NzUsMzczLjkyNzEyIDIuMjk3NSwzNzMuOTI3MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2NS4yNDAyMiw1MTkuNzc2MTIgNC4xMTU5OSw1MDIuMTUxNTgiCiAgICAgICBpZD0icGF0aDM4MDYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMTYuNTMxNjUsNTA0LjE4Njk5IDMuODgwNTksMzEwLjk2NDM2IgogICAgICAgaWQ9InBhdGgzODMxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM4ODkiCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzE3LjY3NzYsNTc2LjQ4NTM5IDEzMC4xODc0MiwxLjUyNDQ0IGMgNC41MTA3OSwzLjI0MTY5IDIwLjM0NDcxLDcuOTY4NTMgMjcuNzQ0ODYsNC4yNjg0NCAzLjE1NTQ2LC0xLjU3NzcyIDkuNDE5LC01LjM4ODE3IDE0LjAyNDg5LC0zLjk2MzU1IDQuMjY2OTgsMS4zMTk4MSA2LjAxNjg5LDMuMTE2MzIgMTAuMzY2MjEsMy4wNDg4OSAxMC4zMDQwMywtMC4xNTk3NSAyMC4yMTE3LDAuMzg3NDEgMzAuNDg4ODYsMC4zMDQ4OSAxNzcuODkwOCwtMS40MjgyNyAzNTYuNTkwMzUsLTIuMTMyNDcgNTM0Ljc3NDU2LC0zLjA0ODg4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDc1LjMwNTAxLDU4Mi44ODgwNSBjIC0zLjQ0NDE4LDExLjM1MDY2IC0yLjEwMzQzLDEyLjQzMzczIDMuNjU4NjUsMjEuMDM3MzEgMy43OTQ0NSw1LjY2NTY0IDUwLjg2MjYxLDEzLjAzODQ1IDQxLjQ2NDg1LDI3LjEzNTA5IC0xMC41MzY5NywxNS44MDU0NyAtMjIuODk3NDUsLTUuNDc3NzIgLTMzLjg0MjYzLC0xLjgyOTMzIC01LjQ1MjM2LDEuODE3NDUgLTcuMzQ5MDEsNS40NTYzMSAtMy42NTg2Niw5LjE0NjY1IDIuODA2ODMsMi44MDY4NCA0LjA0OCwxLjgwMzk2IDYuNTIwMzQsNS4xMDA0MSIKICAgICAgIGlkPSJwYXRoMzkxMCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjAxMDgyLDYzNi44NTMzMyBjIDguMzE4OTksMTMuMTEwMTYgMTguODQ2MjEsMTQuNjM0NjUgMzUuNjcxOTYsMTQuNjM0NjUgMi45Mzg2NSwwIDcuODY5OTgsLTAuOTMzNzEgMTAuNjcxMTEsMCAxMS4zNTkxNywzLjc4NjM5IDI3LjE5Mzk4LDEwLjI3NTc3IDM2LjIwMTkzLDIxLjEyOTQ4IDguMjgwMDIsOS45NzY2MSAxMC4yNTI3OCwyMy44ODMwOCA3LjcwMjAyLDM3LjEwNDI0IC02LjE2OTg5LDMxLjk3OTk4IC0xNi43MTQzMSw1Ni45ODg1MyAtMTkuMDQzNTUsODYuNTY5MDUgLTEuMzQ3OTgsMTcuMTE4OCA0LjUwOTU3LDIyLjUzNTIyIDExLjA3MTQzLDMzLjkyODU3IDEwLjY3MDIzLDE4LjUyNjcyIDguNzI0NTMsMTQuMTk5NTUgOC41NzE0MywzNC4yODU3MiAtMC4xMzk2MywxOC4zMTk0NCAwLDYwLjI2Mzg1IDAsODAuNzE0MjkiCiAgICAgICBpZD0icGF0aDM5MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUyOC41MDgwNiw2NTguOTU3NzYgYyAtMTAuNjgxMjMsMC45MDQ1NCAtNy4xMDgwNCwtNS42MDI1NSAtMTAuODIzNTQsLTguMDc5NTYgLTQuNzg0NTQsLTMuMTg5NjkgLTEyLjIyNzA0LC0xLjI1MTA0IC0xNi43Njg4OCwtNS43OTI4OCAtMC42NjYxMiwtMC42NjYxMiAtOC44MDk2OSwtNC4xMDg3NyAtMTAuMTc0NDcsLTIuNzQzOTkgLTguMzY0NTksOC4zNjQ1OSAtMy4wNDg4OCwyMC41NTE4OCAtMy4wNDg4OCwzMy41Mzc3NCBsIDMuMDIyLDMzOS42OTc0MyIKICAgICAgIGlkPSJwYXRoMzkxNCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA1MTcuOTg5NDEsNjUxLjAzMDY1IGMgLTAuMjIxNzEsLTIuNzAxODQgMS45MDM0NiwtNS41NjIxMyAzLjM1Mzc3LC03LjAxMjQ1IDEuNzk5NDMsLTEuNzk5NDIgNi45MjI5NCwxLjAwNDE5IDguODQxNzgsLTAuOTE0NjYgMC4yODc2NSwtMC4yODc2NiAwLjg0MzI5LC0xMS4xNjQxIDAuMjI4NjYsLTEzLjU2NzUzIC0yLjA2NDgzLC04LjA3NDE2IC0yLjA1ODAxLC0yOC42NTY1OCAtMi4wNTgwMSwtMzguNzIwODYgbCAwLC03My4xNzMyNiIKICAgICAgIGlkPSJwYXRoMzkxNiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNTI4LjY2MDUsNjc1LjQyMTczIC0wLjQ1NzMzLC0zMS41NTU5NiIKICAgICAgIGlkPSJwYXRoMzk3NCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc2Ni4zMTYyNSw1NzkuNjQ0MzEgMC40MzExOCwxMy43OTc2OCBjIDMuMTM2NDMsNC42NjkxNSAzLjAxODI0LDkuNjAwNjggMy4wMTgyNCwxNi4zODQ3NSBsIDAsMTU3LjM3OTgxIgogICAgICAgaWQ9InBhdGgzOTgyIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMTEyMi45MDAxLDc2NS45MTMwMyBjIC0yMDIuMzA2NjksNC42OTA1IC00MDMuNzQ0MDUsLTEuMTEzODEgLTYwNS45NTQ1NCwzLjM1MzkgLTEwLjg2MzYyLDAuMjQwMDIgLTMuMzYxNDcsLTguNTg2MyAtMjguNTM2OCwtOC41ODYzIgogICAgICAgaWQ9InBhdGgzOTg0IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA4NjAuMDA4MDUsNzM3LjA2NjUxIGMgMCwwIC05Ny40NDc1LDAuODU4MDYgLTE0Ny41Njg5MiwwLjg1ODA2IC01LjI2ODYxLDAgLTQuNTE1NDYsLTguMzI5ODYgLTcuMzAwODksLTguMzI5ODYgLTMuOTc0MzUsMCAtOC42MjkyNSwwLjAyMDEgLTEwLjUwOTQ4LDAuMDM1OSAtMi4zMzQ3NywwLjAxOTcgLTEuODEwOTQsOC4zNjU5NyAtNC4xNDU4LDguMzY2OTIgLTQ2LjE2ODk5LDAuMDE4OCAtMTY3LjQwNzY3LC0xLjMwNzk5IC0xNzUuMDUyNjMsLTEuMzA3OTkgLTQuNDI5NTUsMCAtOC41NzYyNywtNi40Mzk3MiAtMTMuMTMxOTgsLTYuNDM5NzIgLTEuMzYxMTUsMCAtNi4yMzg3MywwIC0xNC4zOTQ2NywwIgogICAgICAgaWQ9InBhdGgzOTg2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJNIDY3NS4wMDcwMyw4MzEuMTc0MDIgNjc0LjM5NzI1LDMwOS40MDI5OSIKICAgICAgIGlkPSJwYXRoMzk4OCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc5OS40MDE1NywzMTMuMDYxNjUgMS4yMTk1NSw0OTUuODY2NTMiCiAgICAgICBpZD0icGF0aDM5OTAiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA3MzYuNTk0NTIsMzEyLjQ1MTg4IC0xLjIxOTU1LDcxNi40ODgyMiIKICAgICAgIGlkPSJwYXRoMzk5MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUzMC4wMzA5NCw2NDMuNDU4NTkgMzkyLjM3MTU5LC0zLjAxODI1IgogICAgICAgaWQ9InBhdGg0MDQ4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gODU5LjQ1MDYsMzE0LjkwMTI4IDEuMjkzNTQsNTA3Ljk4MDU4IgogICAgICAgaWQ9InBhdGg0MDUwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjAuOTk5OTk5OTRweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gOTIxLjU0MDE3LDMxMC41ODk0OSAxLjcyNDcxLDUzMS43NTIyNyIKICAgICAgIGlkPSJwYXRoNDA1MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDczNi4yODk2Myw0NTMuMzEwNCAxODUuNjc3MTUsLTAuMzA0ODkiCiAgICAgICBpZD0icGF0aDQxODciCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMDYwLjgxMDUsNTE0Ljk2NzY3IGMgMCwwIC0zNjMuMjgxMjYsLTUuNjI2MTggLTU0NC42NTA0MiwyLjUyMTc4IC00LjE3Nzc2LDAuMTg3NjkgLTEyLjUwMDQ0LDEuMDY3MTEgLTEyLjUwMDQ0LDEuMDY3MTEgLTEuNTcwOTUsMC4xMzQxIC0yLjAwMDkzLC0yLjMyNDk1IC0yLjU5MTU1LC0zLjUwNjIzIC0wLjA5NjcsLTAuMTkzNDMgLTcuMDYwODEsLTEuOTMzNCAtNy42MjIyMSwtMS4zNzE5OSAtMi44OTMxNCwyLjg5MzE0IC03LjYzMTY3LDQuMjQ4NjkgLTEyLjE5NTU1LDQuMTE2IEwgMzY5LjIwMTcsNTE0LjUzNjUiCiAgICAgICBpZD0icGF0aDQyNjEiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzOTkuODE1MzEsNDc5LjYxMTEyIDExLjY0MTgsNS42MDUzIGMgMi45ODQxMiwxLjQzNjc5IDYuNTI4NzgsLTAuNDc3MTIgOS45MTcwOCwtMC40MzExOCBsIDEyNy4xOTczOSwxLjcyNDcxIgogICAgICAgaWQ9InBhdGg0MjYzIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gNTE5LjI1MTUxLDUxNy4xMjM1NyA1MTguODIwMzIsMzA4LjQzMzYyIgogICAgICAgaWQ9InBhdGg0MjY1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjkyNTQ5LDM4OS43MTQ5OCBjIDExLjA0NDk2LDAgMzUuNTMzMDcsMC42MTkyNyA0Mi41Nzk3OCwtMS4wMDM5NyA4LjQwNTIyLC0xLjkzNjE4IDcuMDY2LC02Ljk1Mzc4IDE0LjE5NzEyLC02Ljk1Mzc4IDcuODA5NSwwIDYuNTQyOTEsOC4wNjIzNyAyMC4xNDE3LDguMDYyMzcgMTMuOTkwNjgsMCA0NC45NzY4OSwwLjM3ODg2IDYzLjkzOTkyLDAuMzc4ODYgMTIuMDgzOTUsMCA4Mi4wMDI2NiwwLjMwNDg5IDkzLjYwMDgxLDAuMzA0ODkgOC43NjA0NywwIDEzLjE1OTcsLTIuMjg4MjcgMjEuMzQyMTksLTcuMDEyNDMgNy4xOTUxNSwtNC4xNTQxMyAyLjA1NDU5LC05LjQ5MTM3IDIwLjQyNzU0LC04Ljg0MTc3IDIzLjE0NTQsMC44MTgzMyAxMi42NDMzNCwxNC4wMjQ4NyAzMi4zMTgxOSwxNC4wMjQ4NyAyNS4zNTk1NCwwIDEzMC45OTkwMiwwIDE1MC45MTk4NSwwIDE0LjMzMjQ0LDAgLTQuMTE5MTEsLTEzLjExMDIxIDI5LjI2OTMsLTEzLjQxNTEiCiAgICAgICBpZD0icGF0aDQyNjkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU4OC42Nzk1NyIKICAgICAgIHk9IjczNS44MDQ2MyIKICAgICAgIGlkPSJ0ZXh0NDMxMCIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMiIKICAgICAgICAgeD0iNTg4LjY3OTU3IgogICAgICAgICB5PSI3MzUuODA0NjMiPkxpbmNvbG48L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY4Ni4zOTg1IgogICAgICAgeT0iNzY1LjYyODQyIgogICAgICAgaWQ9InRleHQ0MzEwLTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNiIKICAgICAgICAgeD0iNjg2LjM5ODUiCiAgICAgICAgIHk9Ijc2NS42Mjg0MiI+SGFycnk8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgIHk9Ii04MDIuMzc3MzgiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgiCiAgICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgICAgeT0iLTgwMi4zNzczOCI+V29vZGxhd248L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU2Mi4xMTkyNiIKICAgICAgIHk9Ii03NzEuOTY4MTQiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yIgogICAgICAgICB4PSI1NjIuMTE5MjYiCiAgICAgICAgIHk9Ii03NzEuOTY4MTQiPkVkZ2Vtb29yPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTguMzA0ODciCiAgICAgICB5PSItNzM4LjM2NjQ2IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yLTkiCiAgICAgICAgIHg9IjU5OC4zMDQ4NyIKICAgICAgICAgeT0iLTczOC4zNjY0NiI+T2xpdmVyPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICB5PSItNjc3LjIwMzk4IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00IgogICAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICAgIHk9Ii02NzcuMjAzOTgiPkhpbGxzaWRlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTcuMzI3MDkiCiAgICAgICB5PSItODYyLjYxNDA3IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNS0zIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgtMi05LTQtMSIKICAgICAgICAgeD0iNTk3LjMyNzA5IgogICAgICAgICB5PSItODYyLjYxNDA3Ij5Sb2NrPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1ODcuMzcwMTgiCiAgICAgICB5PSItOTI2LjEzNjYiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTktNy01LTMtMiIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00LTEtMyIKICAgICAgICAgeD0iNTg3LjM3MDE4IgogICAgICAgICB5PSItOTI2LjEzNjYiPldlYmI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9Ijg3MS4xNjEwMSIKICAgICAgIHk9IjYzNy41NzUyIgogICAgICAgaWQ9InRleHQ0NDY1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDY3IgogICAgICAgICB4PSI4NzEuMTYxMDEiCiAgICAgICAgIHk9IjYzNy41NzUyIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICB5PSI1NzcuMDMyNDciCiAgICAgICBpZD0idGV4dDQ0NjUtMyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00IgogICAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICAgIHk9IjU3Ny4wMzI0NyI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgaWQ9InRleHQ0NDkwIgogICAgICAgeT0iNTEwLjI2MTgxIgogICAgICAgeD0iODc1Ljk2NjQ5IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSI1MTAuMjYxODEiCiAgICAgICAgIHg9Ijg3NS45NjY0OSIKICAgICAgICAgaWQ9InRzcGFuNDQ5MiIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MjFzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iODgxLjMxNjU5IgogICAgICAgeT0iNDUwLjE5ODc2IgogICAgICAgaWQ9InRleHQ0NDk0IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDk2IgogICAgICAgICB4PSI4ODEuMzE2NTkiCiAgICAgICAgIHk9IjQ1MC4xOTg3NiI+Mjl0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNjE1Ljc5MjQ4IgogICAgICAgeT0iMzg3Ljc0NzE2IgogICAgICAgaWQ9InRleHQ0NDY1LTMtMSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00LTEiCiAgICAgICAgIHg9IjYxNS43OTI0OCIKICAgICAgICAgeT0iMzg3Ljc0NzE2Ij4zN3RoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MTkiCiAgICAgICB5PSI0ODEuNjUyODYiCiAgICAgICB4PSI0ODQuNjkwMzciCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjQ4MS42NTI4NiIKICAgICAgICAgeD0iNDg0LjY5MDM3IgogICAgICAgICBpZD0idHNwYW40NTIxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj4yNXRoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NjMuMDQ2NzUiCiAgICAgICB5PSI1MTMuMzYxMzMiCiAgICAgICBpZD0idGV4dDQ1MjMiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1MjUiCiAgICAgICAgIHg9IjU2My4wNDY3NSIKICAgICAgICAgeT0iNTEzLjM2MTMzIj4yMXN0PC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MjciCiAgICAgICB5PSI1NzcuODk0ODQiCiAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTc3Ljg5NDg0IgogICAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgICAgaWQ9InRzcGFuNDUyOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzMSIKICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICB4PSI0MzMuNTgwNzUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICAgIHg9IjQzMy41ODA3NSIKICAgICAgICAgaWQ9InRzcGFuNDUzMyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+QW1pZG9uPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI0MDUuNTMwOTgiCiAgICAgICB5PSItNTIzLjU0MDE2IgogICAgICAgaWQ9InRleHQ0NTM1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDUzNyIKICAgICAgICAgeD0iNDA1LjUzMDk4IgogICAgICAgICB5PSItNTIzLjU0MDE2Ij5BcmthbnNhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzOSIKICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICB4PSI3NDUuNDg0NjIiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICAgIHg9Ijc0NS40ODQ2MiIKICAgICAgICAgaWQ9InRzcGFuNDU0MSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+V2VzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTk2LjcyODMzIgogICAgICAgeT0iLTUzMS4yNTkyOCIKICAgICAgIGlkPSJ0ZXh0NDU0MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NDUiCiAgICAgICAgIHg9IjU5Ni43MjgzMyIKICAgICAgICAgeT0iLTUzMS4yNTkyOCI+V2FjbzwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU1NSIKICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICB4PSI1OTUuNDM0ODEiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICAgIHg9IjU5NS40MzQ4MSIKICAgICAgICAgaWQ9InRzcGFuNDU1NyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+TWF6aWU8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgIHk9IjE2Mi4wNjg3NyIKICAgICAgIGlkPSJ0ZXh0NDU1OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMC43MDcxMDY3OCwwLjcwNzEwNjc4LC0wLjcwNzEwNjc4LDAuNzA3MTA2NzgsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjEiCiAgICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgICAgeT0iMTYyLjA2ODc3Ij5ab288L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjI0MC41ODk5NyIKICAgICAgIHk9IjU3NC40NDU0MyIKICAgICAgIGlkPSJ0ZXh0NDU2MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU2NSIKICAgICAgICAgeD0iMjQwLjU4OTk3IgogICAgICAgICB5PSI1NzQuNDQ1NDMiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU2NyIKICAgICAgIHk9IjUxMS42MzY2MyIKICAgICAgIHg9IjIwNi4wMzE3NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTExLjYzNjYzIgogICAgICAgICB4PSIyMDYuMDMxNzUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPjIxc3Q8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjYyMC40NDMxMiIKICAgICAgIHk9Ii01MDYuNjgyMTkiCiAgICAgICBpZD0idGV4dDQ1NzEiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NTczIgogICAgICAgICB4PSI2MjAuNDQzMTIiCiAgICAgICAgIHk9Ii01MDYuNjgyMTkiPk5pbXM8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU4MyIKICAgICAgIHk9IjY5OC44NDAwOSIKICAgICAgIHg9IjM3MC4yMTY4NiIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNjk4Ljg0MDA5IgogICAgICAgICB4PSIzNzAuMjE2ODYiCiAgICAgICAgIGlkPSJ0c3BhbjQ1ODUiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1hcGxlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSIzODQuMDg0MiIKICAgICAgIHk9IjY4MC44NTEzOCIKICAgICAgIGlkPSJ0ZXh0NDU5OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDYwMSIKICAgICAgICAgeD0iMzg0LjA4NDIiCiAgICAgICAgIHk9IjY4MC44NTEzOCI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzNjcuOTA4MTcsMTAwOS45NTk2IDI2My4wMTgzMywwIgogICAgICAgaWQ9InBhdGg0NjA1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDciCiAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgeD0iNzM2LjI2NzQ2IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgICB4PSI3MzYuMjY3NDYiCiAgICAgICAgIGlkPSJ0c3BhbjQ2MDkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1lcmlkaWFuPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ5NzkiCiAgICAgICB5PSI2NDAuMjA1MjYiCiAgICAgICB4PSI1NzIuODMyMTUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjY0MC4yMDUyNiIKICAgICAgICAgeD0iNTcyLjgzMjE1IgogICAgICAgICBpZD0idHNwYW40OTgxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NzUuMDg5NjYiCiAgICAgICB5PSI2NzAuOTAzNSIKICAgICAgIGlkPSJ0ZXh0NDk4MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDk4NSIKICAgICAgICAgeD0iNTc1LjA4OTY2IgogICAgICAgICB5PSI2NzAuOTAzNSI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNDk5LjQ4OTYyIgogICAgICAgeT0iMTAwOC42MDY5IgogICAgICAgaWQ9InRleHQ1MDQ3IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5IgogICAgICAgICB4PSI0OTkuNDg5NjIiCiAgICAgICAgIHk9IjEwMDguNjA2OSI+NDd0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iMjE2LjY0NTQzIgogICAgICAgeT0iNzI1Ljk4Mjk3IgogICAgICAgaWQ9InRleHQ1MDUxIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDUzIgogICAgICAgICB4PSIyMTYuNjQ1NDMiCiAgICAgICAgIHk9IjcyNS45ODI5NyI+S2VsbG9nZzwvdHNwYW4+PC90ZXh0PgogICAgPGZsb3dSb290CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgaWQ9ImZsb3dSb290NTA1NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6MThweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIj48Zmxvd1JlZ2lvbgogICAgICAgICBpZD0iZmxvd1JlZ2lvbjUwNTciPjxyZWN0CiAgICAgICAgICAgaWQ9InJlY3Q1MDU5IgogICAgICAgICAgIHdpZHRoPSIzNDMuNTcxNDQiCiAgICAgICAgICAgaGVpZ2h0PSIxMDMuNTcxNDMiCiAgICAgICAgICAgeD0iMTkuMjg1NzE1IgogICAgICAgICAgIHk9IjE3LjE0Mjg1NyIKICAgICAgICAgICBzdHlsZT0iZm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIgLz48L2Zsb3dSZWdpb24+PGZsb3dQYXJhCiAgICAgICAgIGlkPSJmbG93UGFyYTUwNjEiPjwvZmxvd1BhcmE+PC9mbG93Um9vdD4gICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDYwNy03IgogICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgIHg9Ijc3NC44NzU2MSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgICAgeD0iNzc0Ljg3NTYxIgogICAgICAgICBpZD0idHNwYW40NjA5LTciCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1jQ2xlYW48L3RzcGFuPjwvdGV4dD4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzY0LjE1OTk5LDY1OC40Mjg5MSAyOTkuNTEwMjMsLTEuMDEwMTYgYyA2LjQ5ODcyLC0wLjAyMTkgNi45NzcxOSw5LjI1NDEyIDE2LjU5NjMxLDkuMzkyNDcgMTIuMDU0MjcsMC4xNzMzOSAyOS4xMTA4MywtMC41MzU3MiA1NC4xMTQzNywtMC4zMDExIgogICAgICAgaWQ9InBhdGg1NDQwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgIHk9Ijk0NC4zNTc1NCIKICAgICAgIGlkPSJ0ZXh0NTA0Ny05IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5LTMiCiAgICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgICAgeT0iOTQ0LjM1NzU0Ij5NYWNBcnRodXI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDctNy0xIgogICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgIHg9Ijc4MC44NDYwNyIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgICAgeD0iNzgwLjg0NjA3IgogICAgICAgICBpZD0idHNwYW40NjA5LTctOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+U2VuZWNhPC90c3Bhbj48L3RleHQ+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2Ny42OTU1Myw1MzcuMjEwNiAxNDEuMjgzMDMsLTEuMDEwMTUgYyA2LjQ4OTk5LC0wLjA0NjQgMTIuNzgxMTQsNy4yMzU0NSAxOS4xOTI5LDcuMzIzNiA1NS45MjM2MiwwLjc2ODkgMTU4LjY4OTk3LC0wLjE3MzMzIDIzNi41MTQwMiwtMS4wMTAxNSA3LjgzOTU2LC0wLjA4NDMgMjIuNjMxNDcsLTE5Ljg1MzU1IDMwLjMwNDU3LC0yMC40NTU1OSAyMi4yNjU4OSwtMS4zNTE4MSA0NS4xNzk0NSwtMC41MDUwNyA2Ny42ODAyMiwtMC41MDUwNyAxNi4xNDczMSwtMC42MzI0MSAzLjYxMDE2LDIwLjcwODEzIDI2Ljc2OTA0LDIwLjcwODEzIGwgMjQzLjQ0Njc5LC0xLjAxMDE2IgogICAgICAgaWQ9InBhdGg1NDk2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzY2NjY2MiIC8+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI2ODUuMjA4MTMiCiAgICAgICB5PSI4MjcuNTMwODIiCiAgICAgICBpZD0idGV4dDQzMTAtNy04IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtNiIKICAgICAgICAgeD0iNjg1LjIwODEzIgogICAgICAgICB5PSI4MjcuNTMwODIiPlBhd25lZTwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0iTSA1NTQuMjg1NzIsNzIxLjQyODU3IDU1MCw1NDMuMjE0MjkgNTQ3LjE0Mjg2LDEwMi41IDU0Ni43ODU3MiwyMy4yMTQyODUiCiAgICAgICBpZD0icGF0aDU1MTkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIiAvPgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTI5LjYyNTMxIgogICAgICAgeT0iLTU1MC44NDc3OCIKICAgICAgIGlkPSJ0ZXh0NDU0My01IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU0NS0wIgogICAgICAgICB4PSI1MjkuNjI1MzEiCiAgICAgICAgIHk9Ii01NTAuODQ3NzgiPkJyb2Fkd2F5PC90c3Bhbj48L3RleHQ+CiAgPC9nPgo8L3N2Zz4K\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"showTooltip\":true,\"autocloseTooltip\":true,\"labelFunction\":\"var deviceType = dsData[dsIndex]['Type'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}, ${energy:2} kWt';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}, ${temperature:2} °C';\\r\\n }\\r\\n}\",\"tooltipFunction\":\"var deviceType = dsData[dsIndex]['Type'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}
Energy: ${energy:2} kWt
';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}
Temperature: ${temperature:2} °C
';\\r\\n }\\r\\n}\",\"provider\":\"image-map\",\"showTooltipAction\":\"click\",\"mapPageSize\":16384,\"draggableMarker\":false,\"disableScrollZooming\":false,\"disableZoomControl\":false,\"useLabelFunction\":false,\"useTooltipFunction\":false,\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"showPolygon\":false,\"showCircle\":false},\"title\":\"Image Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('image-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-map-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"image-map\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"gmDefaultMapType\":\"roadmap\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"useCustomProvider\":false,\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"mapProviderHere\":\"HERE.normalDay\",\"credentials\":{\"app_id\":\"AhM6TzD9ThyK78CT3ptx\",\"app_code\":\"p6NPiITB3Vv0GMUFnkLOOg\"},\"mapImageUrl\":\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiCiAgIHhtbG5zOmlua3NjYXBlPSJodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy9uYW1lc3BhY2VzL2lua3NjYXBlIgogICB3aWR0aD0iMTEzNC41MTgzIgogICBoZWlnaHQ9Ijc2Mi43ODI0MSIKICAgaWQ9InN2ZzIiCiAgIHZlcnNpb249IjEuMSIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMC40OC41IHIxMDA0MCIKICAgc29kaXBvZGk6ZG9jbmFtZT0id2ljaGl0YW1hcC1ub2xpYi5zdmciPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0IiAvPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBpZD0iYmFzZSIKICAgICBwYWdlY29sb3I9IiNmZmZmZmYiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgYm9yZGVyb3BhY2l0eT0iMS4wIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTp6b29tPSIwLjM1IgogICAgIGlua3NjYXBlOmN4PSI4OS45MDc4NTciCiAgICAgaW5rc2NhcGU6Y3k9IjQ1My43ODI0MSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ibGF5ZXIxIgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzNjYiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iNzIxIgogICAgIGlua3NjYXBlOndpbmRvdy14PSItNCIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iLTQiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBpbmtzY2FwZTpvYmplY3QtcGF0aHM9InRydWUiCiAgICAgaW5rc2NhcGU6c25hcC1nbG9iYWw9ImZhbHNlIgogICAgIHNob3dndWlkZXM9InRydWUiCiAgICAgaW5rc2NhcGU6Z3VpZGUtYmJveD0idHJ1ZSIKICAgICBmaXQtbWFyZ2luLXRvcD0iMCIKICAgICBmaXQtbWFyZ2luLWxlZnQ9IjAiCiAgICAgZml0LW1hcmdpbi1yaWdodD0iMCIKICAgICBmaXQtbWFyZ2luLWJvdHRvbT0iMCIgLz4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE3Ij4KICAgIDxyZGY6UkRGPgogICAgICA8Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+CiAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+CiAgICAgICAgPGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPgogICAgICAgIDxkYzp0aXRsZT48L2RjOnRpdGxlPgogICAgICA8L2NjOldvcms+CiAgICA8L3JkZjpSREY+CiAgPC9tZXRhZGF0YT4KICA8ZwogICAgIGlua3NjYXBlOmxhYmVsPSJMYXllciAxIgogICAgIGlua3NjYXBlOmdyb3VwbW9kZT0ibGF5ZXIiCiAgICAgaWQ9ImxheWVyMSIKICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjcuMDcxNDI4LC0zMDcuOTAyOTkpIj4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM3ODciCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzY0ZTU5O3N0cm9rZS13aWR0aDoyLjk5OTk5OTc2O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmUiCiAgICAgICBkPSJtIDkwNi4wMzMxNSw3MDYuMTMzNjcgMy40MjkyLDE3Ljc5NTUyIE0gMjguNTcxNDI4LDc2NS4wNTA2NyBjIDE1MC40MzUyMDIsNi44MzM0MiAxNDYuMzkyMzIyLC0yNi4zMzQxNSAxNjYuNDM0NTQyLC0yOS4zMjAwOSAzNi4xNDM3NSwtNS4zODQ3NiAxMTQuMjg2NzYsLTYuNTI1NCAxNDguMzI1MDgsLTguNjIzNTQgNDMuMzc4MDgsLTIuNjczODUgMTQxLjc2MjIxLC0xMS4yMzA5OSAxODguODU1NzgsLTE5LjgzNDE4IDM5LjgxMTM4LC03LjI3Mjg0IDIyMS4zNjk5MSwtMC44NjIzNSAzMTkuMDcxNDEsLTAuODYyMzUgNzAuODI3MzUsMCAxNDYuOTE4NjcsLTEuNzI0NyAyMTguMTc1ODYsLTEuNzI0NyAtMzEuNjE5NywwIDExNy44NTUyLC0yLjU4NzA3IDg2LjIzNTUsLTIuNTg3MDcgbSAtMjUuMDkwNywtNjguMTI2MDYgYyAtNTIuNzk5NiwzNC43ODQ4NCAtNjUuODk1MSw1MS43NDg2NSAtOTUuNjM5LDgxLjQ5MjU4IC0yNC45MzEzLDI0LjkzMTI3IC0xNDAuMzk2NTMsLTE5LjEzOTIgLTE3OC45Mzg3MSwzNi42NTAwNyAtMTIuMjgxNCwxNy43NzcxNSAtNDcuMDAyNTcsNDYuNTQ2NTMgLTY1LjEwNzgzLDU5LjA3MTMzIC0yMC4xMDUsMTMuOTA4MTggLTU2LjAzNjcyLDQ0Ljk1NjY0IC02Ny43Njg4NSw3My4wNzgyNyAtNC44MDE0NywxMS41MDkwMiAtMTMuMzgwNDYsMzUuOTkyOTggLTIzLjQ0OTQ5LDQ2LjA2MjAxIC0xMC40OTY5OSwxMC40OTY5OSAtMzguMzc3MzMsNi4zODU2OSAtNDQuMDIzNDUsMTcuNjQ3NjQgLTE5LjAwNTAyLDM3LjkwODEyIC0yNS40NjUzLDEwMC45MjM1MiAtNjcuNjE3ODksMTAyLjA1MTAyIG0gMTkuMjgxNTEsLTYyNC4wMTQ2NCBjIDM0LjY1OTM0LC0xLjg3MzgyIDg0LjAyNzMzLDcuMzkxMzEgMTA5LjkwMDcxLC00LjI4NTQ1IDEzLjI4MTcyLC01Ljk5NDA4IDQxLjQwNzIxLC0yLjQ2MTM1IDY2LjgyODY2LC0yLjMyMDQ2IDM1LjMyMjM4LDAuMTk1NzggNjQuMzgyNDksMC42MzQ3NyAxMDEuOTE2Nyw1LjAyMzIgMjUuMDMwMzYsMi45MjY1IDQ0LjY2MjczLDM0LjI4NzIyIDU4LjUyNjk4LDUwLjY0MzkgMTcuMDk4NzgsMjAuMTcyNjggNjIuNzYzODYsLTEuNzE0NjcgNjYuMzA1NjYsMzIuMTM0MzMgNS4xMDI3LDQ4Ljc2NTg3IC02LjMyODQsNzguNjM3MjUgNi4xNDExLDk3LjM0MTUgMTkuOTY5MiwyOS45NTM3OSA1MC40ODY0LDE3Ljg1NTc5IDQ0LjYxOTMsODMuOTcxMTkgTSA1ODkuMTAyMjcsMzA5LjcyNzE1IGMgNC42NDM0NiwyMy43MjkyMyAxNS4wNjkwNCw3Mi43NzU3NSAxOS4wNjEyOCwxMzAuNjQyODggMC44NzIwNiwxMi42NDA0OCA1LjQ0NzE4LDI0Ljk5MjUzIDQuMjIyMzEsNDUuMjc3NTcgLTIuNTE3MjEsNDEuNjg3NSAtMTUuNzE3MDYsNDMuNjc3MjcgLTE1LjA5MTIyLDYwLjM2NDg2IDEuNDMxOTUsMzguMTgyMjQgMzAuNjEzNjEsOTMuODM3MTkgMzAuNjEzNjEsMTM5LjcwMTU0IDAsMjQuMTgwOCAtMi42Njk2NCwxMTUuMzkwNDUgNy4zMzAwMSwxMzUuMzg5NzYgMC4xNTkxMSwwLjMxODIxIDEwLjA2NDc2LDM1Ljg4MzMyIDEwLjc3OTQ1LDQ5LjE1NDI0IDAuOTQzNzgsMTcuNTI0NjkgLTI0LjQ3OCwzOS40NzAwOCAtMjguMDI2NTUsNDYuNTY3MTYgLTUuNDc3NywxMC45NTUzOSAtMzYuOTczMjQsMTAuODgxOTcgLTQwLjA5OTUsMjQuMTQ1OTUgLTMuODY4ODQsMTYuNDE0NTEgLTMuODY2Myw0My43OTczNSA0LjA0NjQ3LDU5LjQ0MTI5IG0gOTcuMzM3MzQsLTY5MS4wMDk0MSBjIC01LjAxMzMyLDM1LjUxNTk1IC00My42NTkwMSwxMS4zMTY1MiAtNTguNTM4NjEsMjMuNzgxMzEgLTIxLjMzMDE5LDE3Ljg2ODUyIC02Mi40OTk2NCwzMS40MzIxMiAtNzAuMTI0MzcsMzUuMzY3MDggLTM1LjA4NzYzLDE4LjEwNzkzIC0xMTAuNDcyMTUsLTE1LjE0MTk2IC0xMjUuNjE0MSw0LjI2ODQzIC0xNS45NTA2MywyMC40NDcwMyAtMC4wNzM1LDYxLjQ2NjQ4IC05LjE0NjY2LDg0LjE0OTI0IC02LjAzNTcsMTUuMDg5MjYgLTE4Ljg3NjcsMjMuMDE3MzQgLTI3LjQzOTk3LDMyLjkyNzk4IC0xOS43NDgyOSwyMi44NTU1NSAtNjkuOTc0MjgsNjkuODI0MTkgLTg0Ljc1OTA0LDEwMC4wMDM0NiAtNy40OTc0MSwxNS4zMDQwNCAtMy4yODQyNiw0NC40MjA0MSAtMy40NzA1Myw2My4zNDI4NCAtMC4xMjc5MywxMi45OTQxNCAtMC44MTAxNSwyMy4xMDM4NSAyLjQwMzQzLDI4LjI3NjE4IDQuOTYxNTgsNy45ODU4MSAyMy43MjA1LDI4LjExMjA3IDI0LjIzODY1LDUwLjYxMTQ5IDAuMjk0MTEsMTIuNzcxNDYgMC4wMTMzLDc4LjU5MTAxIDMuMDQ4ODgsODcuNjU1NDkgMi4zMTI1Niw2LjkwNTQ2IDQuMjIwMDQsMjYuNTY0OTcgMTAuMjEzNzcsMzYuNTg2NjIgMTEuMzU0MDEsMTguOTg0MTUgNC4zODczNyw0MC4xNTY2MiAyNy44OTczLDUzLjUwNzk1IDE5LjA1MDEyLDEwLjgxODU5IDQ2Ljg3NzgxLDEyLjIxODYyIDgxLjkyNjE4LDE0LjQ2MDU0IDMzLjcwMzQ1LDIuMTU1ODkgNjEuNTEyMTcsLTEuNDMwMzUgNzYuOTIwNzcsNi4xNDExIDExLjU4NTA4LDUuNjkyNjYgOC41ODE1MSwxNy45MzM0NCAxNC4yOTU0MSwyOS4zNjEyMyA1LjY0MDQyLDExLjI4MDg1IDMxLjUwMjYzLDExLjE1NjI3IDQxLjgwNDA5LDQzLjQ1NDg3IDcuNjA1OSwyMy44NDcxIDMuMDg1OTMsNDQuMTU2OSA2LjcwNzU1LDY1Ljg4NjYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2Nzc3NzY2Njc3Nzc3NzY2Nzc3Nzc3NjY3Nzc3Njc3NzY2Nzc3Nzc3Nzc3Nzc3Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Im0gNDMuMjc3ODgxLDUxNy45NDY3OSBjIDAsMCAyMzAuODQ4Mjg5LC0zLjYzODA1IDI1MC4wMDg2MzksLTMuNjU4NjcgNy40ODIyMiwtMC4wMDggOC42MTk1NCw1LjE1MTk0IDE0LjAyMDksMTEuNDU4NjkgMjQuNTk2MDgsMjguNzE4OTMgOTMuOTA5NjYsMTEyLjkzNTg1IDkzLjkwOTY2LDExMi45MzU4NSIKICAgICAgIGlkPSJwYXRoMzc4OSIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJtIDM1Ljk2MDU1NSw1NzcuNzA0OTQgYyAwLDAgMTY1LjUyNDU2NSwtMS42ODQ1NCAyNDguNzc5NTY1LC0xLjY4NDU0IDQuOTQ3NDksMCA3LjcyOTkzLC0yLjg4MzMgMTAuNTM3NzEsLTUuNzI5NzcgOS42NjEwNywtOS43OTQxNiAyNS42MzE5OSwtMjguNTg5OTUgMjUuNjMxOTksLTI4LjU4OTk1IgogICAgICAgaWQ9InBhdGgzNzkxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzguMzk5NjYzLDY0MS43MzE1NSA0MzEuNzA1OTMsNjM3LjQ2MzExIgogICAgICAgaWQ9InBhdGgzNzk1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzkuMDA5NDQyLDcwNC41Mzg1OSA1MjMuMTcyNTMsNjk3LjgzMTA0IgogICAgICAgaWQ9InBhdGgzNzk3IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzAzLjk1NzYyLDY4Mi41ODY2MSAxNDYuNzk1NDIsMS44MjkzMyBjIDEwLjUzNDAzLDAuMTMxMjcgMTQuMzQzNzQsLTIuNjM3MzkgMjUuNDg3MTUsLTYuMzcyOCAxMC40MTIxMiwtMy40OTAyNyAzMS40MjQxNSwtMi42OTg5NiA0MS4zODUzOCwtMi43NzM4NSBsIDQwNS41NjA3OSwtMy4wNDg5IgogICAgICAgaWQ9InBhdGgzNzk5IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgaWQ9InBhdGgzODA0IgogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDQyNi4yMTc5NCwzMTQuODkwOTggYyAyLjA2NzU0LDkuMDUyNzMgMS44NDE3Nyw1MS43Mjc3NyA2LjUwNzk0LDc0LjgzNDY2IDEuNjc0NzUsOC4yOTMzNiA4LjY3NTA4LDE0LjA2NTk4IDEwLjA1NTQxLDE0Ljg1ODYyIDQuOTAxNDcsMi44MTQ2MyAxMC44MTQ3OSw4LjE0OTgyIDEzLjA0NTc5LDE2LjA4ODMxIDYuNzU3NzksMjQuMDQ1OTEgMC44Nzk3Miw2OC40NTIxMiAwLjg3OTcyLDExMC42ODkzIDAsNi4wOTc4MiAxLjY2MDEsMzAuMTQ2NiAtMi4xNTU4OCwzMy45NjI1OSAtMi41NDA4NSwyLjU0MDgzIC0wLjI4MTYzLDEyLjk5MDY5IC0zLjQzNjc1LDE2LjE0Mzc3IGwgLTkuODQ5NDQsOS44NDMxMSBjIC0xMC4zNjcxNSwxMC4zNjA0NyAtMTEuNTkwMTcsNi41MjYxNCAtMTcuNzM4NDgsMTguODIyNzYgLTMuNTY3NzIsNy4xMzU0MyA1LjQwMjM1LDIwLjY3MjEgNy4zNTQzMiwyNC41NzYwMiAxLjkzMjE0LDMuODY0MyAtMS44NDIxNiw0Ljc3NzczIC0xLjc5MjM1LDcuNDQ2MjYgMC4yNTI4NiwxMy41NDQ4MyAyLjI5NzUsMzczLjkyNzEyIDIuMjk3NSwzNzMuOTI3MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2NS4yNDAyMiw1MTkuNzc2MTIgNC4xMTU5OSw1MDIuMTUxNTgiCiAgICAgICBpZD0icGF0aDM4MDYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMTYuNTMxNjUsNTA0LjE4Njk5IDMuODgwNTksMzEwLjk2NDM2IgogICAgICAgaWQ9InBhdGgzODMxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM4ODkiCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzE3LjY3NzYsNTc2LjQ4NTM5IDEzMC4xODc0MiwxLjUyNDQ0IGMgNC41MTA3OSwzLjI0MTY5IDIwLjM0NDcxLDcuOTY4NTMgMjcuNzQ0ODYsNC4yNjg0NCAzLjE1NTQ2LC0xLjU3NzcyIDkuNDE5LC01LjM4ODE3IDE0LjAyNDg5LC0zLjk2MzU1IDQuMjY2OTgsMS4zMTk4MSA2LjAxNjg5LDMuMTE2MzIgMTAuMzY2MjEsMy4wNDg4OSAxMC4zMDQwMywtMC4xNTk3NSAyMC4yMTE3LDAuMzg3NDEgMzAuNDg4ODYsMC4zMDQ4OSAxNzcuODkwOCwtMS40MjgyNyAzNTYuNTkwMzUsLTIuMTMyNDcgNTM0Ljc3NDU2LC0zLjA0ODg4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDc1LjMwNTAxLDU4Mi44ODgwNSBjIC0zLjQ0NDE4LDExLjM1MDY2IC0yLjEwMzQzLDEyLjQzMzczIDMuNjU4NjUsMjEuMDM3MzEgMy43OTQ0NSw1LjY2NTY0IDUwLjg2MjYxLDEzLjAzODQ1IDQxLjQ2NDg1LDI3LjEzNTA5IC0xMC41MzY5NywxNS44MDU0NyAtMjIuODk3NDUsLTUuNDc3NzIgLTMzLjg0MjYzLC0xLjgyOTMzIC01LjQ1MjM2LDEuODE3NDUgLTcuMzQ5MDEsNS40NTYzMSAtMy42NTg2Niw5LjE0NjY1IDIuODA2ODMsMi44MDY4NCA0LjA0OCwxLjgwMzk2IDYuNTIwMzQsNS4xMDA0MSIKICAgICAgIGlkPSJwYXRoMzkxMCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjAxMDgyLDYzNi44NTMzMyBjIDguMzE4OTksMTMuMTEwMTYgMTguODQ2MjEsMTQuNjM0NjUgMzUuNjcxOTYsMTQuNjM0NjUgMi45Mzg2NSwwIDcuODY5OTgsLTAuOTMzNzEgMTAuNjcxMTEsMCAxMS4zNTkxNywzLjc4NjM5IDI3LjE5Mzk4LDEwLjI3NTc3IDM2LjIwMTkzLDIxLjEyOTQ4IDguMjgwMDIsOS45NzY2MSAxMC4yNTI3OCwyMy44ODMwOCA3LjcwMjAyLDM3LjEwNDI0IC02LjE2OTg5LDMxLjk3OTk4IC0xNi43MTQzMSw1Ni45ODg1MyAtMTkuMDQzNTUsODYuNTY5MDUgLTEuMzQ3OTgsMTcuMTE4OCA0LjUwOTU3LDIyLjUzNTIyIDExLjA3MTQzLDMzLjkyODU3IDEwLjY3MDIzLDE4LjUyNjcyIDguNzI0NTMsMTQuMTk5NTUgOC41NzE0MywzNC4yODU3MiAtMC4xMzk2MywxOC4zMTk0NCAwLDYwLjI2Mzg1IDAsODAuNzE0MjkiCiAgICAgICBpZD0icGF0aDM5MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUyOC41MDgwNiw2NTguOTU3NzYgYyAtMTAuNjgxMjMsMC45MDQ1NCAtNy4xMDgwNCwtNS42MDI1NSAtMTAuODIzNTQsLTguMDc5NTYgLTQuNzg0NTQsLTMuMTg5NjkgLTEyLjIyNzA0LC0xLjI1MTA0IC0xNi43Njg4OCwtNS43OTI4OCAtMC42NjYxMiwtMC42NjYxMiAtOC44MDk2OSwtNC4xMDg3NyAtMTAuMTc0NDcsLTIuNzQzOTkgLTguMzY0NTksOC4zNjQ1OSAtMy4wNDg4OCwyMC41NTE4OCAtMy4wNDg4OCwzMy41Mzc3NCBsIDMuMDIyLDMzOS42OTc0MyIKICAgICAgIGlkPSJwYXRoMzkxNCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA1MTcuOTg5NDEsNjUxLjAzMDY1IGMgLTAuMjIxNzEsLTIuNzAxODQgMS45MDM0NiwtNS41NjIxMyAzLjM1Mzc3LC03LjAxMjQ1IDEuNzk5NDMsLTEuNzk5NDIgNi45MjI5NCwxLjAwNDE5IDguODQxNzgsLTAuOTE0NjYgMC4yODc2NSwtMC4yODc2NiAwLjg0MzI5LC0xMS4xNjQxIDAuMjI4NjYsLTEzLjU2NzUzIC0yLjA2NDgzLC04LjA3NDE2IC0yLjA1ODAxLC0yOC42NTY1OCAtMi4wNTgwMSwtMzguNzIwODYgbCAwLC03My4xNzMyNiIKICAgICAgIGlkPSJwYXRoMzkxNiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNTI4LjY2MDUsNjc1LjQyMTczIC0wLjQ1NzMzLC0zMS41NTU5NiIKICAgICAgIGlkPSJwYXRoMzk3NCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc2Ni4zMTYyNSw1NzkuNjQ0MzEgMC40MzExOCwxMy43OTc2OCBjIDMuMTM2NDMsNC42NjkxNSAzLjAxODI0LDkuNjAwNjggMy4wMTgyNCwxNi4zODQ3NSBsIDAsMTU3LjM3OTgxIgogICAgICAgaWQ9InBhdGgzOTgyIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMTEyMi45MDAxLDc2NS45MTMwMyBjIC0yMDIuMzA2NjksNC42OTA1IC00MDMuNzQ0MDUsLTEuMTEzODEgLTYwNS45NTQ1NCwzLjM1MzkgLTEwLjg2MzYyLDAuMjQwMDIgLTMuMzYxNDcsLTguNTg2MyAtMjguNTM2OCwtOC41ODYzIgogICAgICAgaWQ9InBhdGgzOTg0IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA4NjAuMDA4MDUsNzM3LjA2NjUxIGMgMCwwIC05Ny40NDc1LDAuODU4MDYgLTE0Ny41Njg5MiwwLjg1ODA2IC01LjI2ODYxLDAgLTQuNTE1NDYsLTguMzI5ODYgLTcuMzAwODksLTguMzI5ODYgLTMuOTc0MzUsMCAtOC42MjkyNSwwLjAyMDEgLTEwLjUwOTQ4LDAuMDM1OSAtMi4zMzQ3NywwLjAxOTcgLTEuODEwOTQsOC4zNjU5NyAtNC4xNDU4LDguMzY2OTIgLTQ2LjE2ODk5LDAuMDE4OCAtMTY3LjQwNzY3LC0xLjMwNzk5IC0xNzUuMDUyNjMsLTEuMzA3OTkgLTQuNDI5NTUsMCAtOC41NzYyNywtNi40Mzk3MiAtMTMuMTMxOTgsLTYuNDM5NzIgLTEuMzYxMTUsMCAtNi4yMzg3MywwIC0xNC4zOTQ2NywwIgogICAgICAgaWQ9InBhdGgzOTg2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJNIDY3NS4wMDcwMyw4MzEuMTc0MDIgNjc0LjM5NzI1LDMwOS40MDI5OSIKICAgICAgIGlkPSJwYXRoMzk4OCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc5OS40MDE1NywzMTMuMDYxNjUgMS4yMTk1NSw0OTUuODY2NTMiCiAgICAgICBpZD0icGF0aDM5OTAiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA3MzYuNTk0NTIsMzEyLjQ1MTg4IC0xLjIxOTU1LDcxNi40ODgyMiIKICAgICAgIGlkPSJwYXRoMzk5MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUzMC4wMzA5NCw2NDMuNDU4NTkgMzkyLjM3MTU5LC0zLjAxODI1IgogICAgICAgaWQ9InBhdGg0MDQ4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gODU5LjQ1MDYsMzE0LjkwMTI4IDEuMjkzNTQsNTA3Ljk4MDU4IgogICAgICAgaWQ9InBhdGg0MDUwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjAuOTk5OTk5OTRweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gOTIxLjU0MDE3LDMxMC41ODk0OSAxLjcyNDcxLDUzMS43NTIyNyIKICAgICAgIGlkPSJwYXRoNDA1MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDczNi4yODk2Myw0NTMuMzEwNCAxODUuNjc3MTUsLTAuMzA0ODkiCiAgICAgICBpZD0icGF0aDQxODciCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMDYwLjgxMDUsNTE0Ljk2NzY3IGMgMCwwIC0zNjMuMjgxMjYsLTUuNjI2MTggLTU0NC42NTA0MiwyLjUyMTc4IC00LjE3Nzc2LDAuMTg3NjkgLTEyLjUwMDQ0LDEuMDY3MTEgLTEyLjUwMDQ0LDEuMDY3MTEgLTEuNTcwOTUsMC4xMzQxIC0yLjAwMDkzLC0yLjMyNDk1IC0yLjU5MTU1LC0zLjUwNjIzIC0wLjA5NjcsLTAuMTkzNDMgLTcuMDYwODEsLTEuOTMzNCAtNy42MjIyMSwtMS4zNzE5OSAtMi44OTMxNCwyLjg5MzE0IC03LjYzMTY3LDQuMjQ4NjkgLTEyLjE5NTU1LDQuMTE2IEwgMzY5LjIwMTcsNTE0LjUzNjUiCiAgICAgICBpZD0icGF0aDQyNjEiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzOTkuODE1MzEsNDc5LjYxMTEyIDExLjY0MTgsNS42MDUzIGMgMi45ODQxMiwxLjQzNjc5IDYuNTI4NzgsLTAuNDc3MTIgOS45MTcwOCwtMC40MzExOCBsIDEyNy4xOTczOSwxLjcyNDcxIgogICAgICAgaWQ9InBhdGg0MjYzIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gNTE5LjI1MTUxLDUxNy4xMjM1NyA1MTguODIwMzIsMzA4LjQzMzYyIgogICAgICAgaWQ9InBhdGg0MjY1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjkyNTQ5LDM4OS43MTQ5OCBjIDExLjA0NDk2LDAgMzUuNTMzMDcsMC42MTkyNyA0Mi41Nzk3OCwtMS4wMDM5NyA4LjQwNTIyLC0xLjkzNjE4IDcuMDY2LC02Ljk1Mzc4IDE0LjE5NzEyLC02Ljk1Mzc4IDcuODA5NSwwIDYuNTQyOTEsOC4wNjIzNyAyMC4xNDE3LDguMDYyMzcgMTMuOTkwNjgsMCA0NC45NzY4OSwwLjM3ODg2IDYzLjkzOTkyLDAuMzc4ODYgMTIuMDgzOTUsMCA4Mi4wMDI2NiwwLjMwNDg5IDkzLjYwMDgxLDAuMzA0ODkgOC43NjA0NywwIDEzLjE1OTcsLTIuMjg4MjcgMjEuMzQyMTksLTcuMDEyNDMgNy4xOTUxNSwtNC4xNTQxMyAyLjA1NDU5LC05LjQ5MTM3IDIwLjQyNzU0LC04Ljg0MTc3IDIzLjE0NTQsMC44MTgzMyAxMi42NDMzNCwxNC4wMjQ4NyAzMi4zMTgxOSwxNC4wMjQ4NyAyNS4zNTk1NCwwIDEzMC45OTkwMiwwIDE1MC45MTk4NSwwIDE0LjMzMjQ0LDAgLTQuMTE5MTEsLTEzLjExMDIxIDI5LjI2OTMsLTEzLjQxNTEiCiAgICAgICBpZD0icGF0aDQyNjkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU4OC42Nzk1NyIKICAgICAgIHk9IjczNS44MDQ2MyIKICAgICAgIGlkPSJ0ZXh0NDMxMCIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMiIKICAgICAgICAgeD0iNTg4LjY3OTU3IgogICAgICAgICB5PSI3MzUuODA0NjMiPkxpbmNvbG48L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY4Ni4zOTg1IgogICAgICAgeT0iNzY1LjYyODQyIgogICAgICAgaWQ9InRleHQ0MzEwLTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNiIKICAgICAgICAgeD0iNjg2LjM5ODUiCiAgICAgICAgIHk9Ijc2NS42Mjg0MiI+SGFycnk8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgIHk9Ii04MDIuMzc3MzgiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgiCiAgICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgICAgeT0iLTgwMi4zNzczOCI+V29vZGxhd248L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU2Mi4xMTkyNiIKICAgICAgIHk9Ii03NzEuOTY4MTQiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yIgogICAgICAgICB4PSI1NjIuMTE5MjYiCiAgICAgICAgIHk9Ii03NzEuOTY4MTQiPkVkZ2Vtb29yPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTguMzA0ODciCiAgICAgICB5PSItNzM4LjM2NjQ2IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yLTkiCiAgICAgICAgIHg9IjU5OC4zMDQ4NyIKICAgICAgICAgeT0iLTczOC4zNjY0NiI+T2xpdmVyPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICB5PSItNjc3LjIwMzk4IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00IgogICAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICAgIHk9Ii02NzcuMjAzOTgiPkhpbGxzaWRlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTcuMzI3MDkiCiAgICAgICB5PSItODYyLjYxNDA3IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNS0zIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgtMi05LTQtMSIKICAgICAgICAgeD0iNTk3LjMyNzA5IgogICAgICAgICB5PSItODYyLjYxNDA3Ij5Sb2NrPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1ODcuMzcwMTgiCiAgICAgICB5PSItOTI2LjEzNjYiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTktNy01LTMtMiIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00LTEtMyIKICAgICAgICAgeD0iNTg3LjM3MDE4IgogICAgICAgICB5PSItOTI2LjEzNjYiPldlYmI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9Ijg3MS4xNjEwMSIKICAgICAgIHk9IjYzNy41NzUyIgogICAgICAgaWQ9InRleHQ0NDY1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDY3IgogICAgICAgICB4PSI4NzEuMTYxMDEiCiAgICAgICAgIHk9IjYzNy41NzUyIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICB5PSI1NzcuMDMyNDciCiAgICAgICBpZD0idGV4dDQ0NjUtMyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00IgogICAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICAgIHk9IjU3Ny4wMzI0NyI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgaWQ9InRleHQ0NDkwIgogICAgICAgeT0iNTEwLjI2MTgxIgogICAgICAgeD0iODc1Ljk2NjQ5IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSI1MTAuMjYxODEiCiAgICAgICAgIHg9Ijg3NS45NjY0OSIKICAgICAgICAgaWQ9InRzcGFuNDQ5MiIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MjFzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iODgxLjMxNjU5IgogICAgICAgeT0iNDUwLjE5ODc2IgogICAgICAgaWQ9InRleHQ0NDk0IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDk2IgogICAgICAgICB4PSI4ODEuMzE2NTkiCiAgICAgICAgIHk9IjQ1MC4xOTg3NiI+Mjl0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNjE1Ljc5MjQ4IgogICAgICAgeT0iMzg3Ljc0NzE2IgogICAgICAgaWQ9InRleHQ0NDY1LTMtMSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00LTEiCiAgICAgICAgIHg9IjYxNS43OTI0OCIKICAgICAgICAgeT0iMzg3Ljc0NzE2Ij4zN3RoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MTkiCiAgICAgICB5PSI0ODEuNjUyODYiCiAgICAgICB4PSI0ODQuNjkwMzciCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjQ4MS42NTI4NiIKICAgICAgICAgeD0iNDg0LjY5MDM3IgogICAgICAgICBpZD0idHNwYW40NTIxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj4yNXRoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NjMuMDQ2NzUiCiAgICAgICB5PSI1MTMuMzYxMzMiCiAgICAgICBpZD0idGV4dDQ1MjMiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1MjUiCiAgICAgICAgIHg9IjU2My4wNDY3NSIKICAgICAgICAgeT0iNTEzLjM2MTMzIj4yMXN0PC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MjciCiAgICAgICB5PSI1NzcuODk0ODQiCiAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTc3Ljg5NDg0IgogICAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgICAgaWQ9InRzcGFuNDUyOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzMSIKICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICB4PSI0MzMuNTgwNzUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICAgIHg9IjQzMy41ODA3NSIKICAgICAgICAgaWQ9InRzcGFuNDUzMyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+QW1pZG9uPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI0MDUuNTMwOTgiCiAgICAgICB5PSItNTIzLjU0MDE2IgogICAgICAgaWQ9InRleHQ0NTM1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDUzNyIKICAgICAgICAgeD0iNDA1LjUzMDk4IgogICAgICAgICB5PSItNTIzLjU0MDE2Ij5BcmthbnNhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzOSIKICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICB4PSI3NDUuNDg0NjIiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICAgIHg9Ijc0NS40ODQ2MiIKICAgICAgICAgaWQ9InRzcGFuNDU0MSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+V2VzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTk2LjcyODMzIgogICAgICAgeT0iLTUzMS4yNTkyOCIKICAgICAgIGlkPSJ0ZXh0NDU0MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NDUiCiAgICAgICAgIHg9IjU5Ni43MjgzMyIKICAgICAgICAgeT0iLTUzMS4yNTkyOCI+V2FjbzwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU1NSIKICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICB4PSI1OTUuNDM0ODEiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICAgIHg9IjU5NS40MzQ4MSIKICAgICAgICAgaWQ9InRzcGFuNDU1NyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+TWF6aWU8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgIHk9IjE2Mi4wNjg3NyIKICAgICAgIGlkPSJ0ZXh0NDU1OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMC43MDcxMDY3OCwwLjcwNzEwNjc4LC0wLjcwNzEwNjc4LDAuNzA3MTA2NzgsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjEiCiAgICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgICAgeT0iMTYyLjA2ODc3Ij5ab288L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjI0MC41ODk5NyIKICAgICAgIHk9IjU3NC40NDU0MyIKICAgICAgIGlkPSJ0ZXh0NDU2MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU2NSIKICAgICAgICAgeD0iMjQwLjU4OTk3IgogICAgICAgICB5PSI1NzQuNDQ1NDMiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU2NyIKICAgICAgIHk9IjUxMS42MzY2MyIKICAgICAgIHg9IjIwNi4wMzE3NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTExLjYzNjYzIgogICAgICAgICB4PSIyMDYuMDMxNzUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPjIxc3Q8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjYyMC40NDMxMiIKICAgICAgIHk9Ii01MDYuNjgyMTkiCiAgICAgICBpZD0idGV4dDQ1NzEiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NTczIgogICAgICAgICB4PSI2MjAuNDQzMTIiCiAgICAgICAgIHk9Ii01MDYuNjgyMTkiPk5pbXM8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU4MyIKICAgICAgIHk9IjY5OC44NDAwOSIKICAgICAgIHg9IjM3MC4yMTY4NiIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNjk4Ljg0MDA5IgogICAgICAgICB4PSIzNzAuMjE2ODYiCiAgICAgICAgIGlkPSJ0c3BhbjQ1ODUiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1hcGxlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSIzODQuMDg0MiIKICAgICAgIHk9IjY4MC44NTEzOCIKICAgICAgIGlkPSJ0ZXh0NDU5OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDYwMSIKICAgICAgICAgeD0iMzg0LjA4NDIiCiAgICAgICAgIHk9IjY4MC44NTEzOCI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzNjcuOTA4MTcsMTAwOS45NTk2IDI2My4wMTgzMywwIgogICAgICAgaWQ9InBhdGg0NjA1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDciCiAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgeD0iNzM2LjI2NzQ2IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgICB4PSI3MzYuMjY3NDYiCiAgICAgICAgIGlkPSJ0c3BhbjQ2MDkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1lcmlkaWFuPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ5NzkiCiAgICAgICB5PSI2NDAuMjA1MjYiCiAgICAgICB4PSI1NzIuODMyMTUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjY0MC4yMDUyNiIKICAgICAgICAgeD0iNTcyLjgzMjE1IgogICAgICAgICBpZD0idHNwYW40OTgxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NzUuMDg5NjYiCiAgICAgICB5PSI2NzAuOTAzNSIKICAgICAgIGlkPSJ0ZXh0NDk4MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDk4NSIKICAgICAgICAgeD0iNTc1LjA4OTY2IgogICAgICAgICB5PSI2NzAuOTAzNSI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNDk5LjQ4OTYyIgogICAgICAgeT0iMTAwOC42MDY5IgogICAgICAgaWQ9InRleHQ1MDQ3IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5IgogICAgICAgICB4PSI0OTkuNDg5NjIiCiAgICAgICAgIHk9IjEwMDguNjA2OSI+NDd0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iMjE2LjY0NTQzIgogICAgICAgeT0iNzI1Ljk4Mjk3IgogICAgICAgaWQ9InRleHQ1MDUxIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDUzIgogICAgICAgICB4PSIyMTYuNjQ1NDMiCiAgICAgICAgIHk9IjcyNS45ODI5NyI+S2VsbG9nZzwvdHNwYW4+PC90ZXh0PgogICAgPGZsb3dSb290CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgaWQ9ImZsb3dSb290NTA1NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6MThweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIj48Zmxvd1JlZ2lvbgogICAgICAgICBpZD0iZmxvd1JlZ2lvbjUwNTciPjxyZWN0CiAgICAgICAgICAgaWQ9InJlY3Q1MDU5IgogICAgICAgICAgIHdpZHRoPSIzNDMuNTcxNDQiCiAgICAgICAgICAgaGVpZ2h0PSIxMDMuNTcxNDMiCiAgICAgICAgICAgeD0iMTkuMjg1NzE1IgogICAgICAgICAgIHk9IjE3LjE0Mjg1NyIKICAgICAgICAgICBzdHlsZT0iZm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIgLz48L2Zsb3dSZWdpb24+PGZsb3dQYXJhCiAgICAgICAgIGlkPSJmbG93UGFyYTUwNjEiPjwvZmxvd1BhcmE+PC9mbG93Um9vdD4gICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDYwNy03IgogICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgIHg9Ijc3NC44NzU2MSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgICAgeD0iNzc0Ljg3NTYxIgogICAgICAgICBpZD0idHNwYW40NjA5LTciCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1jQ2xlYW48L3RzcGFuPjwvdGV4dD4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzY0LjE1OTk5LDY1OC40Mjg5MSAyOTkuNTEwMjMsLTEuMDEwMTYgYyA2LjQ5ODcyLC0wLjAyMTkgNi45NzcxOSw5LjI1NDEyIDE2LjU5NjMxLDkuMzkyNDcgMTIuMDU0MjcsMC4xNzMzOSAyOS4xMTA4MywtMC41MzU3MiA1NC4xMTQzNywtMC4zMDExIgogICAgICAgaWQ9InBhdGg1NDQwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgIHk9Ijk0NC4zNTc1NCIKICAgICAgIGlkPSJ0ZXh0NTA0Ny05IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5LTMiCiAgICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgICAgeT0iOTQ0LjM1NzU0Ij5NYWNBcnRodXI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDctNy0xIgogICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgIHg9Ijc4MC44NDYwNyIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgICAgeD0iNzgwLjg0NjA3IgogICAgICAgICBpZD0idHNwYW40NjA5LTctOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+U2VuZWNhPC90c3Bhbj48L3RleHQ+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2Ny42OTU1Myw1MzcuMjEwNiAxNDEuMjgzMDMsLTEuMDEwMTUgYyA2LjQ4OTk5LC0wLjA0NjQgMTIuNzgxMTQsNy4yMzU0NSAxOS4xOTI5LDcuMzIzNiA1NS45MjM2MiwwLjc2ODkgMTU4LjY4OTk3LC0wLjE3MzMzIDIzNi41MTQwMiwtMS4wMTAxNSA3LjgzOTU2LC0wLjA4NDMgMjIuNjMxNDcsLTE5Ljg1MzU1IDMwLjMwNDU3LC0yMC40NTU1OSAyMi4yNjU4OSwtMS4zNTE4MSA0NS4xNzk0NSwtMC41MDUwNyA2Ny42ODAyMiwtMC41MDUwNyAxNi4xNDczMSwtMC42MzI0MSAzLjYxMDE2LDIwLjcwODEzIDI2Ljc2OTA0LDIwLjcwODEzIGwgMjQzLjQ0Njc5LC0xLjAxMDE2IgogICAgICAgaWQ9InBhdGg1NDk2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzY2NjY2MiIC8+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI2ODUuMjA4MTMiCiAgICAgICB5PSI4MjcuNTMwODIiCiAgICAgICBpZD0idGV4dDQzMTAtNy04IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtNiIKICAgICAgICAgeD0iNjg1LjIwODEzIgogICAgICAgICB5PSI4MjcuNTMwODIiPlBhd25lZTwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0iTSA1NTQuMjg1NzIsNzIxLjQyODU3IDU1MCw1NDMuMjE0MjkgNTQ3LjE0Mjg2LDEwMi41IDU0Ni43ODU3MiwyMy4yMTQyODUiCiAgICAgICBpZD0icGF0aDU1MTkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIiAvPgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTI5LjYyNTMxIgogICAgICAgeT0iLTU1MC44NDc3OCIKICAgICAgIGlkPSJ0ZXh0NDU0My01IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU0NS0wIgogICAgICAgICB4PSI1MjkuNjI1MzEiCiAgICAgICAgIHk9Ii01NTAuODQ3NzgiPkJyb2Fkd2F5PC90c3Bhbj48L3RleHQ+CiAgPC9nPgo8L3N2Zz4K\",\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"tmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#fe7569\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAwgSURBVGiB7Zt5cBT3lce/v18fc89oRoPEIRBCHIUxp2ywCAgIxLExvoidZIFNxXE2VXHirIO3aqtSseM43qpNeZfYKecox3bhpJykYgdjDkU2mBAB5vCamMNYAgQyURBCoxnNPd39O/aP7hGSEUR24L/uqqf+zfR77/Pe69/Rv6kWwcgPLRIJfZUAa7xez2xd90QBwDSNZKlkHJHAK+l09mUA7BP4vPpRUVExMVoRef+L998njxx9X57vPi/PnTsnO850yPaT7XLXrrflqjtWymhF+HA0Gp0wEp/kHymEQqG4ptJDGzf+um5RUxMSiV7Z3Lyt88L5nozgHJWj4pGmpqZav99PWve04onHHuswmViQzWb7ruZX+Udgv8/z3A+f/NGye1evxssvb+wo5PMfTZs6bfqcuXNHL7hlweh58+ZVAOTUpk2b0p9dvjyqqmrs/b8ejpUMc+unzjgUCsXjsYruE+2n1JY/NedM0zCi0VjA7/d7/f4AAgE//H4/vF4fOjvP9h5695C/oaEhcN/q1SyTzVdnMpnklXzTq4EplUsXfmaRCgC7du3cOn78+KfGj59Add3z1Md1vV7vqPa2D1sA4MYbZ6qUiqVX9X21i4TQcfX19QCA6urquN/vn0kAPRQKpYbTnzRpUhgAampqAEFrPjVYSql7fD4AgK5r2tV0AcDj8WkAoOk6JJGeTw2+nocLdsEu2AW7YBfsgl2wC3bBLtgFu2AX7IJdsAt2wS7YBbtgF+yCXbALdsEu2AW7YBfsgl2wC76mh/ppjIQgXVloPxVSBRV0rBe455P6+kTKBYF3tonxY/IWarry7DvI298Tgp0PR9RzACaN1NeIS100+EdvKXW3cMZvF8wCK10Sq2it2NAzakmukP/wmoP/KuId3BRUMg5uCfCSNVSKVn1rNto7Un8jLrUVqJ4Fi2eEQiEYBzOsy3SYL37TNQdzi8Q5FxkqJIQBsNLlYMGF/zqAJWBxSEogDAY+DJibYqTuRg4WFgO3OKhCYTExbKk5G/mbkSPP2DQhLA5IO/NhSz1MMP882BDgnAFQwdiVSs2vPVhYDIJLUMkBgw1favM6lJoZDDAYhKbAYsOX+rqAhcXAuQSIAKzhSy2vS8YmB7NYH4WCfM7kw5VaWtdpOO3bfWZJZVXgPxMX898bVsm6RhkTIseX29yyIErm/J5z5vwr6pvmsLYjBgeDwSpVJS/OmT1n1de+9qANZgLc4q9Dyj2qQhUhSSUAUCL7GBcchCymTEYBYNWqVXj30MGHT586PZEJ+WAul7ts8bjspd9QKDRNU2nz4z94YtI3H3oI+XwB//3j/9m77eRUUJ9/0eh4APGoDz6vCi4ksgUTmYyBC4k8RLGwtzF+EGu+tHqRqqrYtm0rXnzhhQ7G5cpsNnvyiuBIJFKnqvSd55772eilS5fhwIH9ye+/dPaEf1T9otW3T8GtiyYgGNBBymYEgLSbvakidu8/h01vnkYhcab1gcVs5tx5c6PHjh7DU0/9qFsINPb3939UZg28X11dXR0Qwtr9g8efqGtc+Bn89re/O7FhR9BXNaFm+n98uxHTZ1SDKQqKAihweZlITUVtXQwNs8fg+Bmzdk+bnmPdf/7bwsbGeO2ECaED+9/5XCxWuTGbzVpDwJpGNtx+28o77rr7bmzZsu3k7z+cMlHzeiPrvnoTwtVhFAVQHAZY4HBEoiAAeDXUjI/gyJGeQEd6TFj2tHYuXNgYy2azVe0fngiWDLNloHNFo4FZkXDsoTVr1+KD4x8U/3Ci1qP5PV7N74FeFUbClKDEriy57A5JANL5a68hnqoINL8OAPqbXbNp7clTxTVr1/oOHjr0MFXxq2Qy9wEFACnoY//6la9QAHj+9Q/eUL2RWkVXoWgqkhZBypRImkDKBFIWkLIk+h1JWdL+zrmeNCWSDFB0DYquQvWG637TcnozAKxbt45yTr8PAGowGBwVDAbvmT9/Pvbu3dddijV9WdUUUE0BUQm6kwaCYe+ljK/w8ruUdsYCBLlMEUQhoJoCygWM+LIvHTx4sGfevIbqYMD3BSFkJVUUrG5oaFABoPXwhd1UVUBVahtpKtoOnEV/gSHHgBwDso5c6XO6yNF24CNQTbV9qBRUUenuwz1/BoCZM2dplOJeSggWL1myFEII9IeXziIKBVUUW1QKo2Ci41Anei9kkWcY6Ex5R8qfc0wi0ZPF6QNnYeQNB2j7IQpFOtg0WwiBxoWNIBKLVQI6Z8rUqTh69FiWaFNmEIWgLFShoM5TZbIzgVxvFp6ID5rfA6JQgBAIxsGLJkrpAsycAcH4gN1gX0QPTW9vP5Grr58cJJTOpbqmjgWAnp6ei4QSEEJAKAGh1BbHCS2DLAFmMAgmICwObjDnyYMMAtJL9oN89vRc7KWUQtOUsSqhSggA8sWivSEh9qBxTiCEAGRwQARUVaB67Hf5pZAQlA0Ayrq2LTCogVyhlLURNEw55yYABP2+4ED3vHSClBKQ9jiFdHqvEBCMQzAOKYSt6/RqSGnbDPJRbgT93hAAcM4NyhjrBYDKylhswEEZJgYJFxDchnGTwSqasIomuMnsIDiH5GKIzUAQTsCVlZUxB9xLIUVbKpVEff3kiLTMfimEA7HP5bZgHMJ07mnJAiuaYEXT3jcZDMLkTgBD7exgBKRp9NfVTQwnk0kIKduoJGRH8/ZmhMNh4skc3DnEkDlAi4GbtjDDguVAmZM1M6yB68JyKsCGBqD373s7GAySnTt3gBDyFhWCvPHee/8HAJhTU5g0BMg4uMXBTT4AZSUTrGjBKpiwCnablQbDbZuyfTmAuRPMegA4euQopCRbaCaTOd2XSLzX3d2Nu+64bR7PnP3LJSCDMBm4YW9FWcmyQYMytsW+Zpfdsm1MdimAdMc7K29bMedCdzeSyeS76XT6jLNI4PGf/+w5aLqOu25IjOOWKcSg0jJjcLZ2ecsZD5TdybqsOxC0ZYpbJ58frek6nn/+eVBJHgecjXkqk2nu7Ozcdfz4cdx556rJN5C3m8v3jBt2xpdnazjysawNy5lUbKkrbmtZsWL5pGNHj6Or62+7k5lMy5CFNRQKTfN6tAMvvvhSRe3EOqx/4oXXLvia7qO6CsVZrey5154KB5YpKSG5tHs+5/ZsZnEIk6Ei1fLH73373i/09fXi0fWPpgyTLchkMqeGgAEgHA5/vjJWsf2PmzYr1dXV+K8fP7vjLxduWkY8ilpetQZPg+UJxh63lzqlNDi7gTa3fuPraz6bzxXw79/5FutP51am0+kdZdaQ/2kzDKNDUci51179w8pbP3er8sAD6+pnVCWy+/fs21LAqBnlMT50qJXFLq2a2L/5gaVy7N133j69u7sb67/7iFHIFf4tlU6/Ppg1kLGU8hYAywBMeOWV33gfXb9+1Q+ffDL+4Ne/AcYY/tS8PbV5++4Dhy+MopY2ZrLiidQDgDBSp5TS+Y7psS65ZOHsW26++eYosxje2PwGNm586eKzz/x027+sXWsBOAfgbULIQQAgUspaAA8BGAfnsamrq4u0tZ0Q333kkdGmZS3f8JNnlBXLV0AOilRKCS7sWYlxjlKxgHw+j5Y3W/C/Tz/NQ6Hgjp9seKZ31py5ajwe4wAtz9zdAH5OpJTPAqgEgL5USkpu4eLFHloqFXniYh9t3bunauuWrStisSi5//4vYnHTEkyZOhWqokBICcuy0N7ehr2trXjt1VeRzqTl3ffc81bjgsZELF4pQ6EAqa4eI6UEicfj5dhTKoCikynx6Bop5C14dJ2XcjmouipvvGFGoSJaWfr738/7tmzdjl/88pfIZjKwnH2SpmkIhSMYW1ODhvmNGFcztjhudFXR69Wgck58Hg+XEorH5ylDJYA8kVKOckpdB0ADIBOJhOzv70OhUFILuTzPZLNcSE6SfSlvJp0O5A1DN0qGDxLS4/OUAh6PGQqHC5XxeJEQgkgoRH1+L/wBP6LRuIjH4+Uf8gSAUwB+MbhzzQSwCMA0p/QUQADgNJ/PJ/v7+wnnnFiWkJZhKCYzKADoqiZUXeW67iGcSxKPx2QoFAo7AybnuE8COAZgHyHkxGXjeFAQEQCzANQCqAIQBeAH4AXgcex052w45TMcyQHIAOgBcBbAUUJI5uOM/wcaHmf3g9UM7QAAAABJRU5ErkJggg==\",\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false},\"title\":\"Image Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" } }, { @@ -144,10 +151,11 @@ "resources": [], "templateHtml": "", "templateCss": ".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 200px;\n white-space: nowrap;\n}", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('google-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('google-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('google-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"markerImageSize\":34,\"gmDefaultMapType\":\"roadmap\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"useColorFunction\":true,\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAxNSURBVGiB7Zp7kFTllcB/5/a93dMz3T0PemYIDgoCPhZ5iaD4wNkFjQjRRMlLTNbSlKlyzZpobSVbFRPUbNVWSRCWuKvlxqybtbIrukp4SATZCAgospEBgeElj4EZ5t3d0+++37d/9O2ZnqEHQZzZSlXfqlMz/c253+875zvfOefeHuH8L6u83P+AwH0lJZ4pbrenEiCVSnYmEsndGl4NhSKvAJkLmPPcV0VFxZjKivKPv77wXr274WN9uvm0PnHihD5y9IhuPNioN216Vy+Yf6eurAj8b2Vl5aXnM6d8loLf7w9apvHhyy//29jZ9fW0t7fpdWtWN7Wdao4qpaiqDpbdXF9fV1paKpu3bGbxk08eSWXU9ZFIpOPirC33v7xs+TIdiUT0Pz239NjeaTOTHXXjdb4cuP6W5DOLFx/7aNdH+oknfqQryv0vXZTFfr8/GKyqaN7XeMhc//ba6NSfPFXqS6fESJ29jdGAX69+9KHY9OnTyxbec08mHInWhsPhzsHmNs4FNgxdf+NNN5sAh3/7n40dCxeKedUsOr6x8CzdsnBEQu9sPABwzTWTTMNQ9eec+1x/FDEuGTduHABXtreOKutJYyiFqq4tqD+5O3wJQF1dHSij7nODtdZuj9cLgMfGOpcuQInSFoDldqNFez43eCivIrgILoKL4CK4CC6Ci+AiuAgugovgIrgILoKL4CK4CC6Ci+A/B7B5vor6Mz4PNnbRYAAtoCQLUMMFVobuBWOALWdjVIGxiwbbZC3WkrXWLqAzJBZrR5T0LWTgdSHfdF1YcIlG57t8oM5nfov1OcCKPmDW1Rfi2IsA5yI5F9WFXF0o0i8arARwggsBu4BbhwaM6g0ujXY+9b+GLqrzLR5E5wsH2ziB5QRXoW8lCy3mosH553iwlDlEe9znai2DpMyhAJ+PxUNTJMhZm51+WM9xvsWFXD2kx0nl9rjQ4oYC3C+4BoEMnasl39Vn6wxRdcqbXApXpwupWBcEVgLKGLw6DU1w5bkaCjcChcYuHozuLYtqEFfroXC1TZ67GcbjlEuZWjSIHr6ozjZ7/y/VSWOLdgJIF9zjQl3JFwDOXn1lsYDOULm6X+YaROcLB6s8+LC2tzqvoc+Wx0L2nT/6wlIm5y6LQ9bs5TLXsO5x7jG192lxuJq9bCOg0aIRGcYEkt9lCsPp6lxlMsBlFE4ghcYuGoxznHKFYNjKYq7Zy5XFYW32lMtCBGzbLlwWLwB83m/2NNC44R0iFaP503+8jO1UqHz5wiwW0aNzvysgdPJTQr/7dFD9fHD+vecN9vl8NaYpv546ZeqCBx98CMhGbPXEqZRfcTWmyySTjuO2TMora/B4Sji+832OnWoGYMGCBez88IMfHD50eExG6Yd6enraBjJcAwf8fv+Vbsv1Pz9f/NT1y1esQCnNPz6zeGuy6WBN+MRRrwp1YMR6MOIJMqEuOj49xNFd2zh5aD9SVpr44PCJXVOmXXvpHfPm4fP7rtz98Z/usSz3+lQq1e/fnvuFSHl5+VjTNLb96lfPj6yv/0t2bN/eufJnj+37Uql1c/1Xv8WM279CaZn/rJcBGoj1hNm+7k22rF5JcyK1edp3Hps0bfq0yj0Ne/jFL55pVopZ3d3dx88C19bWlqVS8Z2Lf/7U1XNvu51Vb72x7/irz9fUBEcEv/03PyFYPRJDgZHt9XpvzG8QlAFnWppY+S9LaOnsaPPOWdhxx7z5V320cydLl/7yE2+pb+bp06dj/VxtWbJ03h13zr/r7rtZu2bNwVP/9cKYMiHwtW8+QNAbwOiOIN09SCiChCKQL+EIKhxBhcN4EGpGjuJww66yxNH9gePac+zGm26sikQiNY379/kSydT63uCqrCybXB6oeuS+RYvYv29f/OTKFz1+dIlXXFQrCznRjNhkRfdJzmIMEAExsqbUmh68holWGXf43deMg6NHJ+5btKjkgw8//IFh8lJnZ88nBoBWxpPf+e53DYC1Ly5bVSb6Mo8WSrQgx5uRY6cHSDMcz0q/vx/PSTNeJXi04EOPfe93L70JcP/99xu2bfwUwPT5fNU+n++rM2fO5P3332+uS3V9y9KCG8FSmtjRo3iN0uz+qqylemDnLhpDQDsFJGrHMG2F2xAyGi5Nhr65Y8f21unTZ9T4yrz3KqVHGC4X91x33XUmwN7N775nApbuk90nD5BpbUbaWqG9Dd3eju5o6y/t7dDehrS1kmltYffJ/ViA25nDBcbeLZs2AUyaNNkyDL5minDL7Nm3opSiNtQ0yUQwESydlXg6xc70Sf5CewliYSD9TqHu/anpIMUnJIiLjSVCGjAFTA21odNTlFLMunEWO7bvuMUUjKkTrriCvXv3RDyiJxpacGVXSc56W2uO6DhtKkmFFsocHchmtKhoukURNrJPG5YDdAEuDYaAV/TVjY0HesaNG+8Tw5hmuC1zFEBLS0urkQ3QPtFgILgQTC0IkAZSgEJQCClnTBwdF4KBOPf2iQBnzrS2GYaBZblGmWK4/ADxWCzqoS85iDOZDFiMS2ddV5Kz2EkGhgwECYLOzqOzxy0W7YkAiBgBw7btFIC3tMw/2JsrnS9OI5B2pPdt0AC9gdVZZxkBANu2k0Ymk2kDCI6oqsw1c/nNu8rVW8l+2ZFCkxRNzMhKUjQpNBlnv23nXfbAeTRQHayudMBtBlod6OrqZNz4CeVprcKqd4KsZBxgGk1KNEmBmGiijsScsZRo0s4CMnn3284CMqJCY8aOCXR2dqK0PmBokQ3r1q7D7/dLq7tyY8axMCOatDNZFqhJiCbuWNsLNrJjCUcnt4C0ZOew0WTQnDYr3/X5fLJx4wZE5B1DKVm1a9dHAIyYesPYjEBa+vYwJZAUSAgkHAtjookaWcl9Togm4eim8u5PS9YDNVNmXg7QsLsBreX3RjgcPtzW1rarubmZ+QvumtahXJvzrUzmWRvrZ61yxNnvPKuTA6xvt13bvjxv/tSW5mY6Ozt3hkKhoy4Ar6ek6dChg4vm3nY7oZJAJnG4oUIQESdD5Ud0v30XSBlZC1OGdjyTA/darwK3LcxcPm585ZJnl9ATinwvnkweNgC6wuF1x44d27R3714WfOWucZGrb3g7kee+eJ6LewPLcXU0bzwuuf2G3P3NoyevnzP3tsv3NOylqenkHzvD4fWQ197aikeW/nJJd1dnJ4//9On57V+a8Hoib7K4kQeUAWL0D7RcsJ2oqHv9wUcfu7Orq5MVK5Z3KS0P53j96lsgEPjyiKqKtW/891uu2tpalvzDMxsTW96s9yhMC8HUOCkxm07JO/fZk5A9dkmDTOSqWe/99fcfmRPtifHY3z6a6Q5F7gyFQhsKggFGjKh4wFviffG11153T59xHVu3bg3968/+7g9V3ae+0Zv0kX49l3ISjA2ccpe/NXvR9+uvnX5tRdOpJv7+xz9OxnpiD3d0d/97PqcXrLWeBcwGLnv11d96n3j88QVPPf108KHvPUwmk+HttWu71q96Y0dozzajJBUfXyqMA4gpfShmeY54JkzX19/6VzfMmDmjMpPOsOqtVbzyym9alz23fM23Fy1KACeAP4rIBwCitb4MeAQY5SxEt7a2qIaGBn70wx+OTKXTc5Y+t8w1d85cdN5KtdbYSqGVImPbJOIxotEo6/+wniXPPmsH/L4Ny5etaJk46Rqprq7JPTgooBn4Z9FaPw9UAHR1dSnbTsuZMy1GMpnItLZ2GFu3bq5d/fvVc0ZUjZB7F36d2fW3MmHCFZguF0pr0uk0Bxsb2bL5PV5fuZLuUEjfdffdG2+66ebW6mCVLvP5qa4OAoYEg8Gcg7tNIAIEADHdJnbcxmNZ6UQ05nK7TT1x4sRYRVV1/FTTqdLVa9bywgsvEImESKfSAFiWhT9QzqhL6rh25g3UjbokPnJkTaKkxFRaa8NtGbaIy+Up8eS2VgEx0VpXO66+HKfdbW9vV93d7RKNJl3xeNQOd4d1Mp0i3B3yRCKRsmgiYSVTaa9orS23lfR5vany8vKYLxCIeyxLKqoqtddbKh6PSVVVtQ4Gg5IHPQI8nx9ck4CbgSuBarJnvARsiUai4XBPmGQyqbWGRCxh2VrZAKYYLtNjZUyXSxsuU6oqyg1fwO91nhUSzvQdwB5gm4h8UvA4OYsoByYDY4EaoBLwAN7sYiDvZ4LsqUo60uNIK3AY2CMioYGM/wPREY0iGUY58wAAAABJRU5ErkJggg==\"],\"useMarkerImageFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', amount = percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"color\":\"#fe7568\",\"showTooltip\":true,\"autocloseTooltip\":true,\"labelFunction\":\"var deviceType = dsData[dsIndex]['Type'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}, ${energy:2} kWt';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}, ${temperature:2} °C';\\r\\n }\\r\\n}\",\"tooltipFunction\":\"var deviceType = dsData[dsIndex]['Type'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}
Energy: ${energy:2} kWt
';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}
Temperature: ${temperature:2} °C
';\\r\\n }\\r\\n}\",\"provider\":\"google-map\",\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\",\"mapPageSize\":16384,\"useLabelFunction\":false,\"useTooltipFunction\":false,\"useDefaultCenterPosition\":false,\"draggableMarker\":false,\"disableScrollZooming\":false,\"disableZoomControl\":false,\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"showPolygon\":false,\"showCircle\":false,\"useClusterMarkers\":false},\"title\":\"Google Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('google-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-map-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"google-map\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"gmDefaultMapType\":\"roadmap\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"useCustomProvider\":false,\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"mapProviderHere\":\"HERE.normalDay\",\"credentials\":{\"app_id\":\"AhM6TzD9ThyK78CT3ptx\",\"app_code\":\"p6NPiITB3Vv0GMUFnkLOOg\"},\"mapImageUrl\":\"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==\",\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"tmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#fe7568\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAwgSURBVGiB7Zt5cBT3lce/v18fc89oRoPEIRBCHIUxp2ywCAgIxLExvoidZIFNxXE2VXHirIO3aqtSseM43qpNeZfYKecox3bhpJykYgdjDkU2mBAB5vCamMNYAgQyURBCoxnNPd39O/aP7hGSEUR24L/uqqf+zfR77/Pe69/Rv6kWwcgPLRIJfZUAa7xez2xd90QBwDSNZKlkHJHAK+l09mUA7BP4vPpRUVExMVoRef+L998njxx9X57vPi/PnTsnO850yPaT7XLXrrflqjtWymhF+HA0Gp0wEp/kHymEQqG4ptJDGzf+um5RUxMSiV7Z3Lyt88L5nozgHJWj4pGmpqZav99PWve04onHHuswmViQzWb7ruZX+Udgv8/z3A+f/NGye1evxssvb+wo5PMfTZs6bfqcuXNHL7hlweh58+ZVAOTUpk2b0p9dvjyqqmrs/b8ejpUMc+unzjgUCsXjsYruE+2n1JY/NedM0zCi0VjA7/d7/f4AAgE//H4/vF4fOjvP9h5695C/oaEhcN/q1SyTzVdnMpnklXzTq4EplUsXfmaRCgC7du3cOn78+KfGj59Add3z1Md1vV7vqPa2D1sA4MYbZ6qUiqVX9X21i4TQcfX19QCA6urquN/vn0kAPRQKpYbTnzRpUhgAampqAEFrPjVYSql7fD4AgK5r2tV0AcDj8WkAoOk6JJGeTw2+nocLdsEu2AW7YBfsgl2wC3bBLtgFu2AX7IJdsAt2wS7YBbtgF+yCXbALdsEu2AW7YBfsgl2wC76mh/ppjIQgXVloPxVSBRV0rBe455P6+kTKBYF3tonxY/IWarry7DvI298Tgp0PR9RzACaN1NeIS100+EdvKXW3cMZvF8wCK10Sq2it2NAzakmukP/wmoP/KuId3BRUMg5uCfCSNVSKVn1rNto7Un8jLrUVqJ4Fi2eEQiEYBzOsy3SYL37TNQdzi8Q5FxkqJIQBsNLlYMGF/zqAJWBxSEogDAY+DJibYqTuRg4WFgO3OKhCYTExbKk5G/mbkSPP2DQhLA5IO/NhSz1MMP882BDgnAFQwdiVSs2vPVhYDIJLUMkBgw1favM6lJoZDDAYhKbAYsOX+rqAhcXAuQSIAKzhSy2vS8YmB7NYH4WCfM7kw5VaWtdpOO3bfWZJZVXgPxMX898bVsm6RhkTIseX29yyIErm/J5z5vwr6pvmsLYjBgeDwSpVJS/OmT1n1de+9qANZgLc4q9Dyj2qQhUhSSUAUCL7GBcchCymTEYBYNWqVXj30MGHT586PZEJ+WAul7ts8bjspd9QKDRNU2nz4z94YtI3H3oI+XwB//3j/9m77eRUUJ9/0eh4APGoDz6vCi4ksgUTmYyBC4k8RLGwtzF+EGu+tHqRqqrYtm0rXnzhhQ7G5cpsNnvyiuBIJFKnqvSd55772eilS5fhwIH9ye+/dPaEf1T9otW3T8GtiyYgGNBBymYEgLSbvakidu8/h01vnkYhcab1gcVs5tx5c6PHjh7DU0/9qFsINPb3939UZg28X11dXR0Qwtr9g8efqGtc+Bn89re/O7FhR9BXNaFm+n98uxHTZ1SDKQqKAihweZlITUVtXQwNs8fg+Bmzdk+bnmPdf/7bwsbGeO2ECaED+9/5XCxWuTGbzVpDwJpGNtx+28o77rr7bmzZsu3k7z+cMlHzeiPrvnoTwtVhFAVQHAZY4HBEoiAAeDXUjI/gyJGeQEd6TFj2tHYuXNgYy2azVe0fngiWDLNloHNFo4FZkXDsoTVr1+KD4x8U/3Ci1qP5PV7N74FeFUbClKDEriy57A5JANL5a68hnqoINL8OAPqbXbNp7clTxTVr1/oOHjr0MFXxq2Qy9wEFACnoY//6la9QAHj+9Q/eUL2RWkVXoWgqkhZBypRImkDKBFIWkLIk+h1JWdL+zrmeNCWSDFB0DYquQvWG637TcnozAKxbt45yTr8PAGowGBwVDAbvmT9/Pvbu3dddijV9WdUUUE0BUQm6kwaCYe+ljK/w8ruUdsYCBLlMEUQhoJoCygWM+LIvHTx4sGfevIbqYMD3BSFkJVUUrG5oaFABoPXwhd1UVUBVahtpKtoOnEV/gSHHgBwDso5c6XO6yNF24CNQTbV9qBRUUenuwz1/BoCZM2dplOJeSggWL1myFEII9IeXziIKBVUUW1QKo2Ci41Anei9kkWcY6Ex5R8qfc0wi0ZPF6QNnYeQNB2j7IQpFOtg0WwiBxoWNIBKLVQI6Z8rUqTh69FiWaFNmEIWgLFShoM5TZbIzgVxvFp6ID5rfA6JQgBAIxsGLJkrpAsycAcH4gN1gX0QPTW9vP5Grr58cJJTOpbqmjgWAnp6ei4QSEEJAKAGh1BbHCS2DLAFmMAgmICwObjDnyYMMAtJL9oN89vRc7KWUQtOUsSqhSggA8sWivSEh9qBxTiCEAGRwQARUVaB67Hf5pZAQlA0Ayrq2LTCogVyhlLURNEw55yYABP2+4ED3vHSClBKQ9jiFdHqvEBCMQzAOKYSt6/RqSGnbDPJRbgT93hAAcM4NyhjrBYDKylhswEEZJgYJFxDchnGTwSqasIomuMnsIDiH5GKIzUAQTsCVlZUxB9xLIUVbKpVEff3kiLTMfimEA7HP5bZgHMJ07mnJAiuaYEXT3jcZDMLkTgBD7exgBKRp9NfVTQwnk0kIKduoJGRH8/ZmhMNh4skc3DnEkDlAi4GbtjDDguVAmZM1M6yB68JyKsCGBqD373s7GAySnTt3gBDyFhWCvPHee/8HAJhTU5g0BMg4uMXBTT4AZSUTrGjBKpiwCnablQbDbZuyfTmAuRPMegA4euQopCRbaCaTOd2XSLzX3d2Nu+64bR7PnP3LJSCDMBm4YW9FWcmyQYMytsW+Zpfdsm1MdimAdMc7K29bMedCdzeSyeS76XT6jLNI4PGf/+w5aLqOu25IjOOWKcSg0jJjcLZ2ecsZD5TdybqsOxC0ZYpbJ58frek6nn/+eVBJHgecjXkqk2nu7Ozcdfz4cdx556rJN5C3m8v3jBt2xpdnazjysawNy5lUbKkrbmtZsWL5pGNHj6Or62+7k5lMy5CFNRQKTfN6tAMvvvhSRe3EOqx/4oXXLvia7qO6CsVZrey5154KB5YpKSG5tHs+5/ZsZnEIk6Ei1fLH73373i/09fXi0fWPpgyTLchkMqeGgAEgHA5/vjJWsf2PmzYr1dXV+K8fP7vjLxduWkY8ilpetQZPg+UJxh63lzqlNDi7gTa3fuPraz6bzxXw79/5FutP51am0+kdZdaQ/2kzDKNDUci51179w8pbP3er8sAD6+pnVCWy+/fs21LAqBnlMT50qJXFLq2a2L/5gaVy7N133j69u7sb67/7iFHIFf4tlU6/Ppg1kLGU8hYAywBMeOWV33gfXb9+1Q+ffDL+4Ne/AcYY/tS8PbV5++4Dhy+MopY2ZrLiidQDgDBSp5TS+Y7psS65ZOHsW26++eYosxje2PwGNm586eKzz/x027+sXWsBOAfgbULIQQAgUspaAA8BGAfnsamrq4u0tZ0Q333kkdGmZS3f8JNnlBXLV0AOilRKCS7sWYlxjlKxgHw+j5Y3W/C/Tz/NQ6Hgjp9seKZ31py5ajwe4wAtz9zdAH5OpJTPAqgEgL5USkpu4eLFHloqFXniYh9t3bunauuWrStisSi5//4vYnHTEkyZOhWqokBICcuy0N7ehr2trXjt1VeRzqTl3ffc81bjgsZELF4pQ6EAqa4eI6UEicfj5dhTKoCikynx6Bop5C14dJ2XcjmouipvvGFGoSJaWfr738/7tmzdjl/88pfIZjKwnH2SpmkIhSMYW1ODhvmNGFcztjhudFXR69Wgck58Hg+XEorH5ylDJYA8kVKOckpdB0ADIBOJhOzv70OhUFILuTzPZLNcSE6SfSlvJp0O5A1DN0qGDxLS4/OUAh6PGQqHC5XxeJEQgkgoRH1+L/wBP6LRuIjH4+Uf8gSAUwB+MbhzzQSwCMA0p/QUQADgNJ/PJ/v7+wnnnFiWkJZhKCYzKADoqiZUXeW67iGcSxKPx2QoFAo7AybnuE8COAZgHyHkxGXjeFAQEQCzANQCqAIQBeAH4AXgcex052w45TMcyQHIAOgBcBbAUUJI5uOM/wcaHmf3g9UM7QAAAABJRU5ErkJggg==\",\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAxNSURBVGiB7Zp7kFTllcB/5/a93dMz3T0PemYIDgoCPhZ5iaD4wNkFjQjRRMlLTNbSlKlyzZpobSVbFRPUbNVWSRCWuKvlxqybtbIrukp4SATZCAgospEBgeElj4EZ5t3d0+++37d/9O2ZnqEHQZzZSlXfqlMz/c253+875zvfOefeHuH8L6u83P+AwH0lJZ4pbrenEiCVSnYmEsndGl4NhSKvAJkLmPPcV0VFxZjKivKPv77wXr274WN9uvm0PnHihD5y9IhuPNioN216Vy+Yf6eurAj8b2Vl5aXnM6d8loLf7w9apvHhyy//29jZ9fW0t7fpdWtWN7Wdao4qpaiqDpbdXF9fV1paKpu3bGbxk08eSWXU9ZFIpOPirC33v7xs+TIdiUT0Pz239NjeaTOTHXXjdb4cuP6W5DOLFx/7aNdH+oknfqQryv0vXZTFfr8/GKyqaN7XeMhc//ba6NSfPFXqS6fESJ29jdGAX69+9KHY9OnTyxbec08mHInWhsPhzsHmNs4FNgxdf+NNN5sAh3/7n40dCxeKedUsOr6x8CzdsnBEQu9sPABwzTWTTMNQ9eec+1x/FDEuGTduHABXtreOKutJYyiFqq4tqD+5O3wJQF1dHSij7nODtdZuj9cLgMfGOpcuQInSFoDldqNFez43eCivIrgILoKL4CK4CC6Ci+AiuAgugovgIrgILoKL4CK4CC6Ci+A/B7B5vor6Mz4PNnbRYAAtoCQLUMMFVobuBWOALWdjVIGxiwbbZC3WkrXWLqAzJBZrR5T0LWTgdSHfdF1YcIlG57t8oM5nfov1OcCKPmDW1Rfi2IsA5yI5F9WFXF0o0i8arARwggsBu4BbhwaM6g0ujXY+9b+GLqrzLR5E5wsH2ziB5QRXoW8lCy3mosH553iwlDlEe9znai2DpMyhAJ+PxUNTJMhZm51+WM9xvsWFXD2kx0nl9rjQ4oYC3C+4BoEMnasl39Vn6wxRdcqbXApXpwupWBcEVgLKGLw6DU1w5bkaCjcChcYuHozuLYtqEFfroXC1TZ67GcbjlEuZWjSIHr6ozjZ7/y/VSWOLdgJIF9zjQl3JFwDOXn1lsYDOULm6X+YaROcLB6s8+LC2tzqvoc+Wx0L2nT/6wlIm5y6LQ9bs5TLXsO5x7jG192lxuJq9bCOg0aIRGcYEkt9lCsPp6lxlMsBlFE4ghcYuGoxznHKFYNjKYq7Zy5XFYW32lMtCBGzbLlwWLwB83m/2NNC44R0iFaP503+8jO1UqHz5wiwW0aNzvysgdPJTQr/7dFD9fHD+vecN9vl8NaYpv546ZeqCBx98CMhGbPXEqZRfcTWmyySTjuO2TMora/B4Sji+832OnWoGYMGCBez88IMfHD50eExG6Yd6enraBjJcAwf8fv+Vbsv1Pz9f/NT1y1esQCnNPz6zeGuy6WBN+MRRrwp1YMR6MOIJMqEuOj49xNFd2zh5aD9SVpr44PCJXVOmXXvpHfPm4fP7rtz98Z/usSz3+lQq1e/fnvuFSHl5+VjTNLb96lfPj6yv/0t2bN/eufJnj+37Uql1c/1Xv8WM279CaZn/rJcBGoj1hNm+7k22rF5JcyK1edp3Hps0bfq0yj0Ne/jFL55pVopZ3d3dx88C19bWlqVS8Z2Lf/7U1XNvu51Vb72x7/irz9fUBEcEv/03PyFYPRJDgZHt9XpvzG8QlAFnWppY+S9LaOnsaPPOWdhxx7z5V320cydLl/7yE2+pb+bp06dj/VxtWbJ03h13zr/r7rtZu2bNwVP/9cKYMiHwtW8+QNAbwOiOIN09SCiChCKQL+EIKhxBhcN4EGpGjuJww66yxNH9gePac+zGm26sikQiNY379/kSydT63uCqrCybXB6oeuS+RYvYv29f/OTKFz1+dIlXXFQrCznRjNhkRfdJzmIMEAExsqbUmh68holWGXf43deMg6NHJ+5btKjkgw8//IFh8lJnZ88nBoBWxpPf+e53DYC1Ly5bVSb6Mo8WSrQgx5uRY6cHSDMcz0q/vx/PSTNeJXi04EOPfe93L70JcP/99xu2bfwUwPT5fNU+n++rM2fO5P3332+uS3V9y9KCG8FSmtjRo3iN0uz+qqylemDnLhpDQDsFJGrHMG2F2xAyGi5Nhr65Y8f21unTZ9T4yrz3KqVHGC4X91x33XUmwN7N775nApbuk90nD5BpbUbaWqG9Dd3eju5o6y/t7dDehrS1kmltYffJ/ViA25nDBcbeLZs2AUyaNNkyDL5minDL7Nm3opSiNtQ0yUQwESydlXg6xc70Sf5CewliYSD9TqHu/anpIMUnJIiLjSVCGjAFTA21odNTlFLMunEWO7bvuMUUjKkTrriCvXv3RDyiJxpacGVXSc56W2uO6DhtKkmFFsocHchmtKhoukURNrJPG5YDdAEuDYaAV/TVjY0HesaNG+8Tw5hmuC1zFEBLS0urkQ3QPtFgILgQTC0IkAZSgEJQCClnTBwdF4KBOPf2iQBnzrS2GYaBZblGmWK4/ADxWCzqoS85iDOZDFiMS2ddV5Kz2EkGhgwECYLOzqOzxy0W7YkAiBgBw7btFIC3tMw/2JsrnS9OI5B2pPdt0AC9gdVZZxkBANu2k0Ymk2kDCI6oqsw1c/nNu8rVW8l+2ZFCkxRNzMhKUjQpNBlnv23nXfbAeTRQHayudMBtBlod6OrqZNz4CeVprcKqd4KsZBxgGk1KNEmBmGiijsScsZRo0s4CMnn3284CMqJCY8aOCXR2dqK0PmBokQ3r1q7D7/dLq7tyY8axMCOatDNZFqhJiCbuWNsLNrJjCUcnt4C0ZOew0WTQnDYr3/X5fLJx4wZE5B1DKVm1a9dHAIyYesPYjEBa+vYwJZAUSAgkHAtjookaWcl9Togm4eim8u5PS9YDNVNmXg7QsLsBreX3RjgcPtzW1rarubmZ+QvumtahXJvzrUzmWRvrZ61yxNnvPKuTA6xvt13bvjxv/tSW5mY6Ozt3hkKhoy4Ar6ek6dChg4vm3nY7oZJAJnG4oUIQESdD5Ud0v30XSBlZC1OGdjyTA/darwK3LcxcPm585ZJnl9ATinwvnkweNgC6wuF1x44d27R3714WfOWucZGrb3g7kee+eJ6LewPLcXU0bzwuuf2G3P3NoyevnzP3tsv3NOylqenkHzvD4fWQ197aikeW/nJJd1dnJ4//9On57V+a8Hoib7K4kQeUAWL0D7RcsJ2oqHv9wUcfu7Orq5MVK5Z3KS0P53j96lsgEPjyiKqKtW/891uu2tpalvzDMxsTW96s9yhMC8HUOCkxm07JO/fZk5A9dkmDTOSqWe/99fcfmRPtifHY3z6a6Q5F7gyFQhsKggFGjKh4wFviffG11153T59xHVu3bg3968/+7g9V3ae+0Zv0kX49l3ISjA2ccpe/NXvR9+uvnX5tRdOpJv7+xz9OxnpiD3d0d/97PqcXrLWeBcwGLnv11d96n3j88QVPPf108KHvPUwmk+HttWu71q96Y0dozzajJBUfXyqMA4gpfShmeY54JkzX19/6VzfMmDmjMpPOsOqtVbzyym9alz23fM23Fy1KACeAP4rIBwCitb4MeAQY5SxEt7a2qIaGBn70wx+OTKXTc5Y+t8w1d85cdN5KtdbYSqGVImPbJOIxotEo6/+wniXPPmsH/L4Ny5etaJk46Rqprq7JPTgooBn4Z9FaPw9UAHR1dSnbTsuZMy1GMpnItLZ2GFu3bq5d/fvVc0ZUjZB7F36d2fW3MmHCFZguF0pr0uk0Bxsb2bL5PV5fuZLuUEjfdffdG2+66ebW6mCVLvP5qa4OAoYEg8Gcg7tNIAIEADHdJnbcxmNZ6UQ05nK7TT1x4sRYRVV1/FTTqdLVa9bywgsvEImESKfSAFiWhT9QzqhL6rh25g3UjbokPnJkTaKkxFRaa8NtGbaIy+Up8eS2VgEx0VpXO66+HKfdbW9vV93d7RKNJl3xeNQOd4d1Mp0i3B3yRCKRsmgiYSVTaa9orS23lfR5vany8vKYLxCIeyxLKqoqtddbKh6PSVVVtQ4Gg5IHPQI8nx9ck4CbgSuBarJnvARsiUai4XBPmGQyqbWGRCxh2VrZAKYYLtNjZUyXSxsuU6oqyg1fwO91nhUSzvQdwB5gm4h8UvA4OYsoByYDY4EaoBLwAN7sYiDvZ4LsqUo60uNIK3AY2CMioYGM/wPREY0iGUY58wAAAABJRU5ErkJggg==\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"useClusterMarkers\":false,\"zoomOnClick\":true,\"maxClusterRadius\":80,\"animate\":true,\"spiderfyOnMaxZoom\":false,\"showCoverageOnHover\":true,\"chunkedLoading\":false,\"removeOutsideVisibleBounds\":true},\"title\":\"Google Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" } }, { @@ -162,10 +170,11 @@ "resources": [], "templateHtml": "", "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('openstreet-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('openstreet-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('openstreet-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"markerImageSize\":34,\"useColorFunction\":true,\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAyUSURBVGiB7Zp7kFRVesB/5/S9PdMz/ZoHMwo4MICDuoGVIYICIuzGcn0vC+oWGuNjs8mua9ySP4wpgyaiVVupbHYTsLJmNT7WNXExwqqzrq8g4oNxdXUgyEMQARmZd3fPTE/3vfd8+ePenhlgBsFlrFSqb9Wpvn3vd77f+b7zne87ffsqjv+wE4nYDQqWl5aWfDUcLqkAyOUHunID+Q8EnkilMo8C7gnoPPaRTCYnVyQT71+1bKl80PK+HGw9KPv27ZPde3bLjp075NVXX5FLL7lYKpLx9yoqKuqOR6f6PIFYLFZtW7r54YcfqV+4aBEdHe3ywm+e39eb6etzPZfS0kj5woUX1EUipWrj6xtZedddu11P5mYymc5j6Q19HrgsUrL67r/7+8VLly7j8cce3d3X29vZ0DB9yplnfWXcrFmzxjU2NiaBXevWrUsv/trXKmzbqnz/9+9VDuTyz35hi2OxWHV1ZbJ1245d1ltvvpFtb293Kyoq7LKystKysnLKy8soKyujtDTCxx/vSW3fsT3c0NAQWbpkiZvp7a9Np9Ndo+nWxwJrLYvmzV9gAaxbt/75urrxd592Wp0Oh0tWHSkbiUQSv3unuQlgxoyZltZm0TF1H+umUnrC1KlTAaipqUpESmMzFIRjsVj3SPJTpkyJA0ycOBGMnviFwSISLolEAAiHbftYsgAlJREbwA6HESUlXxg8lkcRXAQXwUVwEVwEF8FFcBH8/xhsnZC0ksw49eQPI5mmNtP54ccAIvqgqbz4aYn8zYoTUXXcFnueyZ8eXtleZt75iQnpU0VUvYiqB5mvu5p+XH9w8RtgnJMOLut/7rd4+fpRBcS52hz65csnHdxQ8clZnyuT3NV40sHRUnfq58mUWFJ70sEn+yiCi+AiuAgugovgIrgILoKL4CK4CC6Ci+D/Q+Djf/higk8Jzs0IMjIGYDGAp0AUeBbiHf3Xs/HGAHyYlYaRX0EYC4txNeIFugvWHyXzua8cnDjYGMBoQIFhRFfLmLjaCxqAw8iuHing/nCwGlLuMrKrveNfnccPFnyLtQ8c0a1jElye8sGFAYwUSCN54Q8GB4ljKKpHkBmLOZbB4FLgjhLVYxNcDFnkMXJUj03m0kOKR0sgYzLHRvlwpcDYI7oaGYvl5HB4ZRrJ1cf9fP5E/5NwQUKM7uoTOI4/ql38kmgUOCMnEHMCL819sag2jJJAxgIs+HNY6PGlpUxXDQWXw5dXjxH8SFZBPf7SyqKrMQLKG7b/OkpmTBJI0BSjbwTGYo6Ni5+ZjMJDj1wkxmQ5iV+VsBh9BzImKbNQFhWjp8wx21c7dKIV9A94IxaJsdplZt9574JQVcUdpr3rzlEHdzLASslpg19EofLMMa3dc0Z9c9YMXT+s7/GCo9FojWWph87+6tmX3XTTzT7XA/F4xutXr4fyOuQZVQUQ0tLphY1nlcn5YqgAuOyyy3inefOtH+36aLJr5Obe3t72o4w68kIsFptuW7pp5d33TPne928hm83yLz+6b9PVb/4niRK9QNfUoquqUaUREEEG+jGd7Zi2Dnpy3qYHGr7OFdcsX2BZFs899ywP/fznu11PLslkMjtHBScSiXrL0m+uXr3mlEWLFrN58+auxD+u2HZWhb0gcvkyShZ/Ax2N+70KPcVvJpMm999NZJ99mi1dzsb3rviLGbNmz6rY0rKFVavubTWG83p6ej4psAbfr66trS03xtlw98p76s+bN5+nnvzFtouevK/s1AnJM+I/vB37j6aDziJeCtxhzUkhTgoYwJpchz3zbJI7fj/pzA829f6iR/bPPW9e9aS6utjbb715YWVl1SOZTMY5DGzb6scXf+OSS6+48kqanntu55+99shkOyLx8uuvIjSuDEzq6Ob5TdzgPJ9GhT2sCbV4W1vK57R+FP9lOrT33PnzKjOZTM2OD7dFB3L5FwaDq6KifGYiXvn95ddey4fbtmWv2fhIiVUqpbpMEao2SH4fiKCMgAbRggSuVkKwEQz22q4iVKtQEYUtJvzdlvX6+bq67PJrr41sbm6+VVv8W1dX7/9oADH6b//0+us1QO/jD6xPhGWSCgsqLJj8PsTdjzj7Ma7fxDkAzn5wjry+H3H2YfL7UGGDCguJEqnPPf3YOoDrrrtOe56+C8CKRqPjotHoN+fMmcObb7zRelsk9W1lC4QFCRlM9yfoKnsoEgOLVWCxDLfYBRwwnXmwDIQVyoMbo6lrfrq5+dCsxsbaaHlkqTFSpUMhvjV79mwLwHvjldewBGxQlqBswXn3Y6T/EDhtiNOGuG2I2444QXPb/WtOGzhtmL7PcN7di7IFFegiJDq3+ZVXAWbMmGlrzRJLKc6/4IJFGGO4MdQ+gxAQEn/2LcH0u+Sa27HO0IRq/V+MSqnBOUZARMAD75DB2w4mq8AKWkggpPiOtJ3dYgznzTuPt996+3xLoc8+vaGBlpaWzFybrygtqCPgeODtcTFtBl1hUBHfGgl+wNGv8FIayWjE6KCfD1UhBVqotPWZO3Zs7506dVpUaT1Lh21rPED7oUNtKH8OUYLSoHTwWRiEAsmBDIA4gCPIAJh8YL3lyw7vi5JAJ7QdamvXWmPbofGW0qEYQL4/0zeYjdTRTQ0Oxp9/Svx9jvKAkBocsCh1dP9AZ76vNwOglI5bnuflAaukPBo9bM8UpMIjvxeiWAUbATHK3/yNJM/h30vKozEAz/Ny2nXddoCKyqrKwc5GDYFMUJmM8peLqyCvkH6FZP1zXP+eGBXIFvQcrquyqroyALdrxGzv7u5i6rTTE3lX0gUL/DIYPPfwFDh+k5xCBhSS1Ui/9s9zQ/cLz0rEGxqEGMWAK92T6yfHu7q6MCLbtSj1UtPzTcTjcfW0E3t5EBSkv0FgPgAMQgtWa/9azpcZHICrhvR48B+52CvRaFS9/PJLKKVe1Mao9e+++zsAtk9rnIwbLBFHIQ5IACWvkJxGBjSSDeDZ4HxAIznty+SV38chGIA/PXumzZoK0PJBCyLq1zqdTn/U2dHxbmtrKxddfmXj1r7QRr9jMH/5Ye4d8OdV+odZ3F+AqyG3F/oFelr62PQnl14667PWVrq6ut5JpVJ7giLBygfWrMYOh3ll/pLx4iojR7p3QMGgpQX4kPUE8OFuF0chrjIvzL78VDsc5sEHH0SLWkmQLuhOp5v27t376tatW7nk8iun/UN8VhM5BblASS5w53BowdXD4L7Lg8EG7Z6SM36z+MILp25p2cqBA/s3dKXTLxRSBeDvtUpL7M0PPfRwYtLken791z9Y++fevmWE/WJBIelbgJbDtz4mePblBksrcPU/ubVrF65Yuayzs50Vt6/ozuXduel0etdhYIB4PH5RVWXy+WeeWR8aV1PDz+6/56W//PDFxbpELGULgwVEcwSYoWXkKExOuatqGl9b8p3vfb2vt5/b/uoWtyfVe0kqlXqpwDpql1lVlbwhUhr52VNPrQ3PPuccNm16PbXrR3f+9pvm0NV+pWEwhQKIqKHnm57iV9nydc6Smxc1zm5MHvj0AHfecUeuv7f/u509PY8N5wyCReRcYCEw6YknHi9bcfvtl9276r7qG2+6Gdd12bhhQ/rghhe3TdmywT4l2zkhEeIUgJTLZ62RygPbT5/rlv/xvLOmnzE9ns/lWb9uPY8++u9tP/3JPzd9e/nyLLAXeE0ptRlAicgk4BZgfDAGc/DgQb1790fWrT+45Zz58xdMue+++0kkk/5N8RO2iPiZ0BiMCMbz8FyXzq4u7l91L5ub3969Zs2/Np/eMM2rrT21YKQBPgPWKBFZAyQA093drTzPobu7uyPV3XNbR2enam5uZu3atdTW1LDsqqtYeMEipk2b5m8GANd12bVzJ69vfI2n1/6Kjo5OvrVsKefOPZeqqkpJJCtXJ5OJinBpRJLxeOF3bI8FZIAYoEN2SHmeJ6GQ2CiMUipUP2UK199wI59+2sp/rVvP6tVryKRTOE4eAcJ2mFg8wfgJE5nZeA4TJ4yntmYcSimUUsaydMi2wxIKKTXM6n4lIuMCV08m2O52dHSQzfbpvkxvZSqTbkinUnWpVDqUzvTS29dHNpvFcfy6aNsWkUgp0fJyYrEYiUTcSybin8RjiZ2lZeXd0WjUra6uDg2L/z3A6uHBNQNYAEwHqvAXTTl4Kp3O9HhOvk+FGMhmHXHdHGLEE8CytNY6rCKRsPY8VRoOh8tisfIkhFxgIAB2AtuA15VS20ZcTsEgEsBM4DTgFKASiAClQAnBig7EC8/8BoAc0AekgE+B/cAWpVTqSMb/AlY1WXIncMcxAAAAAElFTkSuQmCC\",\"data:image/png;base64,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\"],\"useMarkerImageFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', amount = percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"color\":\"#fe7569\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"showTooltip\":true,\"autocloseTooltip\":true,\"tooltipFunction\":\"var deviceType = dsData[dsIndex]['Type'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}
Energy: ${energy:2} kWt
';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}
Temperature: ${temperature:2} °C
';\\r\\n }\\r\\n}\",\"labelFunction\":\"var deviceType = dsData[dsIndex]['Type'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}, ${energy:2} kWt';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}, ${temperature:2} °C';\\r\\n }\\r\\n}\",\"provider\":\"openstreet-map\",\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\",\"mapPageSize\":16384,\"useTooltipFunction\":false,\"useCustomProvider\":false,\"useDefaultCenterPosition\":false,\"draggableMarker\":false,\"disableScrollZooming\":false,\"disableZoomControl\":false,\"useLabelFunction\":false,\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"showPolygon\":false,\"showCircle\":false,\"useClusterMarkers\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonColorFunction\":false,\"polygonOpacity\":0.2,\"usePolygonStrokeColorFunction\":false,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"showPolygonTooltip\":false},\"title\":\"OpenStreetMap\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('openstreet-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-map-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"openstreet-map\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"gmDefaultMapType\":\"roadmap\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"useCustomProvider\":false,\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"mapProviderHere\":\"HERE.normalDay\",\"credentials\":{\"app_id\":\"AhM6TzD9ThyK78CT3ptx\",\"app_code\":\"p6NPiITB3Vv0GMUFnkLOOg\"},\"mapImageUrl\":\"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==\",\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"tmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#fe7569\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"useClusterMarkers\":false,\"zoomOnClick\":true,\"maxClusterRadius\":80,\"animate\":true,\"spiderfyOnMaxZoom\":false,\"showCoverageOnHover\":true,\"chunkedLoading\":false,\"removeOutsideVisibleBounds\":true},\"title\":\"OpenStreetMap\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" } } ] diff --git a/ui-ngx/extra-webpack.config.js b/ui-ngx/extra-webpack.config.js index 2ea3a032e2..1be975de4f 100644 --- a/ui-ngx/extra-webpack.config.js +++ b/ui-ngx/extra-webpack.config.js @@ -18,6 +18,7 @@ const JavaScriptOptimizerPlugin = require("@angular-devkit/build-angular/src/web const webpack = require("webpack"); const dirTree = require("directory-tree"); const ngWebpack = require('@ngtools/webpack'); +const keysTransformer = require('ts-transformer-keys/transformer').default; var langs = []; @@ -58,9 +59,13 @@ module.exports = (config, options) => { }) ); + const index = config.plugins.findIndex(p => p instanceof ngWebpack.ivy.AngularWebpackPlugin || p instanceof ngWebpack.AngularWebpackPlugin); + const angularWebpackPlugin = config.plugins[index]; + + addTransformerToAngularWebpackPlugin(angularWebpackPlugin, keysTransformer); + if (config.mode === 'production') { - const index = config.plugins.findIndex(p => p instanceof ngWebpack.ivy.AngularWebpackPlugin); - const angularCompilerOptions = config.plugins[index].pluginOptions; + const angularCompilerOptions = angularWebpackPlugin.pluginOptions; angularCompilerOptions.emitClassMetadata = true; angularCompilerOptions.emitNgModuleScope = true; config.plugins.splice(index, 1); @@ -72,3 +77,17 @@ module.exports = (config, options) => { } return config; }; + +function addTransformerToAngularWebpackPlugin(plugin, transformer) { + const originalCreateFileEmitter = plugin.createFileEmitter; // private method + plugin.createFileEmitter = function (program, transformers, getExtraDependencies, onAfterEmit) { + if (!transformers) { + transformers = {}; + } + if (!transformers.before) { + transformers = { before: [] }; + } + transformers.before.push(transformer(program.getProgram())); + return originalCreateFileEmitter.apply(plugin, [program, transformers, getExtraDependencies, onAfterEmit]); + }; +} diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 775454c11a..e54606dc08 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -68,6 +68,7 @@ "ngx-clipboard": "^14.0.2", "ngx-color-picker": "^11.0.0", "ngx-daterangepicker-material": "^5.0.1", + "ngx-drag-drop": "^2.0.0", "ngx-flowchart": "https://github.com/thingsboard/ngx-flowchart.git#release/1.0.0", "ngx-hm-carousel": "^2.0.1", "ngx-markdown": "^12.0.1", @@ -91,6 +92,7 @@ "systemjs": "6.11.0", "tinycolor2": "^1.4.2", "tooltipster": "^4.2.8", + "ts-transformer-keys": "^0.4.3", "tslib": "^2.3.1", "tv4": "^1.3.0", "typeface-roboto": "^1.1.13", diff --git a/ui-ngx/src/app/core/api/entity-data-subscription.ts b/ui-ngx/src/app/core/api/entity-data-subscription.ts index b62bd18b31..9cffe57ae7 100644 --- a/ui-ngx/src/app/core/api/entity-data-subscription.ts +++ b/ui-ngx/src/app/core/api/entity-data-subscription.ts @@ -872,7 +872,11 @@ export class EntityDataSubscription { } } if (tsDataKeys.length) { - this.timeseriesTimer = setTimeout(this.onTimeseriesTick.bind(this, tsDataKeys, true), 0); + if (this.history) { + this.onTimeseriesTick(tsDataKeys, true); + } else { + this.timeseriesTimer = setTimeout(this.onTimeseriesTick.bind(this, tsDataKeys, true), 0); + } } if (latestDataKeys.length) { this.onLatestTick(latestDataKeys, detectChanges); diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index e6f2e20adc..37d2f1f652 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -313,6 +313,10 @@ export function deepClone(target: T, ignoreFields?: string[]): T { return target; } +export function extractType(target: any, keysOfProps: (keyof T)[]): T { + return _.pick(target, keysOfProps); +} + export function isEqual(a: any, b: any): boolean { return _.isEqual(a, b); } diff --git a/ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.html b/ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.html index b1c241c2a0..841ad987df 100644 --- a/ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.html +++ b/ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.html @@ -40,7 +40,7 @@ label="{{importFileLabel | translate}}" [allowedExtensions]="'csv,txt'" [accept]="'.csv,application/csv,text/csv,.txt,text/plain'" - dropLabel="{{'import.drop-file-csv' | translate}}"> + dropLabel="{{'import.drop-file-csv-or' | translate}}"> diff --git a/ui-ngx/src/app/modules/home/components/import-export/import-dialog.component.html b/ui-ngx/src/app/modules/home/components/import-export/import-dialog.component.html index e2dbd7bf0a..d75241d35a 100644 --- a/ui-ngx/src/app/modules/home/components/import-export/import-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/import-export/import-dialog.component.html @@ -35,7 +35,7 @@ formControlName="jsonContent" required label="{{importFileLabel | translate}}" - dropLabel="{{ 'import.drop-file' | translate }}" + dropLabel="{{ 'import.drop-json-file-or' | translate }}" accept=".json,application/json" allowedExtensions="json"> diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html index 1fbd396cce..19cc902098 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html @@ -99,7 +99,6 @@
, + private onDragendListener?, + snappable = false) { this.circleData = this.map.convertToCircleFormat(JSON.parse(data[this.settings.circleKeyName])); const centerPosition = this.createCenterPosition(); const circleFillColor = this.getFillColor(); @@ -50,7 +51,7 @@ export class Circle { fillOpacity: settings.circleFillColorOpacity, opacity: settings.circleStrokeOpacity, pmIgnore: !settings.editableCircle, - snapIgnore: !settings.snappable + snapIgnore: !snappable }).addTo(this.map.map); if (settings.showCircleLabel) { @@ -93,12 +94,14 @@ export class Circle { if (this.settings.showCircleLabel) { if (!this.map.circleLabelText || this.settings.useCircleLabelFunction) { const pattern = this.settings.useCircleLabelFunction ? - safeExecute(this.settings.circleLabelFunction, [this.data, this.dataSources, this.data.dsIndex]) : this.settings.circleLabel; + safeExecute(this.settings.parsedCircleLabelFunction, + [this.data, this.dataSources, this.data.dsIndex]) : this.settings.circleLabel; this.map.circleLabelText = parseWithTranslation.prepareProcessPattern(pattern, true); this.map.replaceInfoTooltipCircle = processDataPattern(this.map.circleLabelText, this.data); } const circleLabelText = fillDataPattern(this.map.circleLabelText, this.map.replaceInfoTooltipCircle, this.data); - this.leafletCircle.bindTooltip(`
${circleLabelText}
`, + const labelColor = this.map.ctx.widgetConfig.color; + this.leafletCircle.bindTooltip(`
${circleLabelText}
`, { className: 'tb-polygon-label', permanent: true, sticky: true, direction: 'center'}) .openTooltip(this.leafletCircle.getLatLng()); } @@ -106,7 +109,7 @@ export class Circle { private updateTooltip() { const pattern = this.settings.useCircleTooltipFunction ? - safeExecute(this.settings.circleTooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : + safeExecute(this.settings.parsedCircleTooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : this.settings.circleTooltipPattern; this.tooltip.setContent(parseWithTranslation.parseTemplate(pattern, this.data, true)); } @@ -151,12 +154,12 @@ export class Circle { } private getFillColor(): string | null { - return functionValueCalculator(this.settings.useCircleFillColorFunction, this.settings.circleFillColorFunction, + return functionValueCalculator(this.settings.useCircleFillColorFunction, this.settings.parsedCircleFillColorFunction, [this.data, this.dataSources, this.data.dsIndex], this.settings.circleFillColor); } private getStrokeColor(): string | null { - return functionValueCalculator(this.settings.useCircleStrokeColorFunction, this.settings.circleStrokeColorFunction, + return functionValueCalculator(this.settings.useCircleStrokeColorFunction, this.settings.parsedCircleStrokeColorFunction, [this.data, this.dataSources, this.data.dsIndex], this.settings.circleStrokeColor); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts index 94f7e70ffb..ca6532c995 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts @@ -76,9 +76,12 @@ export function findAngle(startPoint: FormattedData, endPoint: FormattedData, la } -export function getDefCenterPosition(position) { +export function getDefCenterPosition(position): [number, number] { if (typeof (position) === 'string') { - return position.split(','); + const parts = position.split(','); + if (parts.length === 2) { + return [Number(parts[0]), Number(parts[1])]; + } } if (typeof (position) === 'object') { return position; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts index 6207ddf762..73ff835b39 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts @@ -21,15 +21,11 @@ import { MarkerClusterGroup, MarkerClusterGroupOptions } from 'leaflet.markerclu import '@geoman-io/leaflet-geoman-free'; import { - CircleData, - defaultSettings, - MapSettings, + CircleData, defaultMapSettings, + MarkerClusteringSettings, MarkerIconInfo, MarkerImageInfo, - MarkerSettings, - PolygonSettings, - PolylineSettings, - UnitedMapSettings + WidgetPolygonSettings, WidgetPolylineSettings, WidgetMarkersSettings, WidgetUnitedMapSettings } from './map-models'; import { Marker } from './markers'; import { Observable, of } from 'rxjs'; @@ -66,7 +62,7 @@ export default abstract class LeafletMap { polygons: Map = new Map(); circles: Map = new Map(); map: L.Map; - options: UnitedMapSettings; + options: WidgetUnitedMapSettings; bounds: L.LatLngBounds; datasources: FormattedData[]; markersCluster: MarkerClusterGroup; @@ -86,12 +82,12 @@ export default abstract class LeafletMap { replaceInfoTooltipCircle: Array = []; markerTooltipText: string; drawRoutes: boolean; - showPolygon: boolean; updatePending = false; editPolygons = false; editCircle = false; selectedEntity: FormattedData; ignoreUpdateBounds = false; + initDragModeIgnoreUpdateBoundsSet = false; // tslint:disable-next-line:no-string-literal southWest = new L.LatLng(-Projection.SphericalMercator['MAX_LATITUDE'], -180); // tslint:disable-next-line:no-string-literal @@ -104,37 +100,30 @@ export default abstract class LeafletMap { protected constructor(public ctx: WidgetContext, public $container: HTMLElement, - options: UnitedMapSettings) { + options: WidgetUnitedMapSettings) { this.options = options; + this.options.tinyColor = tinycolor(this.options.color || defaultMapSettings.color); this.editPolygons = options.showPolygon && options.editablePolygon; this.editCircle = options.showCircle && options.editableCircle; L.Icon.Default.imagePath = '/'; this.translateService = this.ctx.$injector.get(TranslateService); + this.initMarkerClusterSettings(); } - public initSettings(options: MapSettings) { - this.options.tinyColor = tinycolor(this.options.color || defaultSettings.color); - const { useClusterMarkers, - zoomOnClick, - showCoverageOnHover, - removeOutsideVisibleBounds, - animate, - chunkedLoading, - spiderfyOnMaxZoom, - maxClusterRadius, - maxZoom }: MapSettings = options; - if (useClusterMarkers) { + private initMarkerClusterSettings() { + const markerClusteringSettings: MarkerClusteringSettings = this.options; + if (markerClusteringSettings.useClusterMarkers) { // disabled marker cluster icon (L as any).MarkerCluster = (L as any).MarkerCluster.extend({ options: { pmIgnore: true, ...L.Icon.prototype.options } }); const clusteringSettings: MarkerClusterGroupOptions = { - spiderfyOnMaxZoom, - zoomToBoundsOnClick: zoomOnClick, - showCoverageOnHover, - removeOutsideVisibleBounds, - animate, - chunkedLoading, + spiderfyOnMaxZoom: markerClusteringSettings.spiderfyOnMaxZoom, + zoomToBoundsOnClick: markerClusteringSettings.zoomOnClick, + showCoverageOnHover: markerClusteringSettings.showCoverageOnHover, + removeOutsideVisibleBounds: markerClusteringSettings.removeOutsideVisibleBounds, + animate: markerClusteringSettings.animate, + chunkedLoading: markerClusteringSettings.chunkedLoading, pmIgnore: true, spiderLegPolylineOptions: { pmIgnore: true @@ -143,11 +132,11 @@ export default abstract class LeafletMap { pmIgnore: true } }; - if (maxClusterRadius && maxClusterRadius > 0) { - clusteringSettings.maxClusterRadius = Math.floor(maxClusterRadius); + if (markerClusteringSettings.maxClusterRadius && markerClusteringSettings.maxClusterRadius > 0) { + clusteringSettings.maxClusterRadius = Math.floor(markerClusteringSettings.maxClusterRadius); } - if (maxZoom && maxZoom >= 0 && maxZoom < 19) { - clusteringSettings.disableClusteringAtZoom = Math.floor(maxZoom); + if (markerClusteringSettings.maxZoom && markerClusteringSettings.maxZoom >= 0 && markerClusteringSettings.maxZoom < 19) { + clusteringSettings.disableClusteringAtZoom = Math.floor(markerClusteringSettings.maxZoom); } this.markersCluster = new MarkerClusterGroup(clusteringSettings); } @@ -355,7 +344,6 @@ export default abstract class LeafletMap { if (this.options.initDragMode) { this.map.pm.enableGlobalDragMode(); - this.ignoreUpdateBounds = true; } this.map.on('pm:globaldrawmodetoggled', (e) => this.ignoreUpdateBounds = e.enabled); @@ -468,7 +456,7 @@ export default abstract class LeafletMap { }); }); if (this.options.useDefaultCenterPosition) { - this.map.panTo(this.options.defaultCenterPosition); + this.map.panTo(this.options.parsedDefaultCenterPosition); this.bounds = map.getBounds(); } else { this.bounds = new L.LatLngBounds(null, null); @@ -488,7 +476,7 @@ export default abstract class LeafletMap { } if (this.updatePending) { this.updatePending = false; - this.updateData(this.drawRoutes, this.showPolygon); + this.updateData(this.drawRoutes); } this.createdControlButtonTooltip(); } @@ -565,7 +553,7 @@ export default abstract class LeafletMap { if (!this.options.fitMapBounds && this.options.defaultZoomLevel) { this.map.setZoom(this.options.defaultZoomLevel, { animate: false }); if (this.options.useDefaultCenterPosition) { - this.map.panTo(this.options.defaultCenterPosition, { animate: false }); + this.map.panTo(this.options.parsedDefaultCenterPosition, { animate: false }); } else { this.map.panTo(this.bounds.getCenter()); @@ -581,7 +569,7 @@ export default abstract class LeafletMap { } }); if (this.options.useDefaultCenterPosition) { - this.bounds = this.bounds.extend(this.options.defaultCenterPosition); + this.bounds = this.bounds.extend(this.options.parsedDefaultCenterPosition); } this.map.fitBounds(this.bounds, { padding: padding || [50, 50], animate: false }); this.map.invalidateSize(); @@ -643,7 +631,7 @@ export default abstract class LeafletMap { }; } - updateData(drawRoutes: boolean, showPolygon: boolean) { + updateData(drawRoutes: boolean) { const data = this.ctx.data; let formattedData = formattedDataFormDatasourceData(data); if (this.ctx.latestData && this.ctx.latestData.length) { @@ -654,18 +642,17 @@ export default abstract class LeafletMap { if (drawRoutes) { polyData = formattedDataArrayFromDatasourceData(data); } - this.updateFromData(drawRoutes, showPolygon, formattedData, polyData); + this.updateFromData(drawRoutes, formattedData, polyData); } - updateFromData(drawRoutes: boolean, showPolygon: boolean, formattedData: FormattedData[], + updateFromData(drawRoutes: boolean, formattedData: FormattedData[], polyData: FormattedData[][], markerClickCallback?: any) { this.drawRoutes = drawRoutes; - this.showPolygon = showPolygon; if (this.map) { if (drawRoutes) { this.updatePolylines(polyData, formattedData, false); } - if (showPolygon) { + if (this.options.showPolygon) { this.updatePolygons(formattedData, false); } if (this.options.showCircle) { @@ -776,7 +763,7 @@ export default abstract class LeafletMap { bounds.extend(polyline.leafletPoly.getBounds()); }); } - if (this.showPolygon) { + if (this.options.showPolygon) { this.polygons.forEach((polygon) => { bounds.extend(polygon.leafletPoly.getBounds()); }); @@ -786,7 +773,7 @@ export default abstract class LeafletMap { bounds.extend(polygon.leafletCircle.getBounds()); }); } - if ((this.options as MarkerSettings).useClusterMarkers && this.markersCluster.getBounds().isValid()) { + if (this.options.useClusterMarkers && this.markersCluster.getBounds().isValid()) { bounds.extend(this.markersCluster.getBounds()); } else { this.markers.forEach((marker) => { @@ -802,6 +789,10 @@ export default abstract class LeafletMap { this.fitBounds(bounds); } } + if (this.options.initDragMode && !this.initDragModeIgnoreUpdateBoundsSet) { + this.initDragModeIgnoreUpdateBoundsSet = true; + this.ignoreUpdateBounds = true; + } } // Markers @@ -815,7 +806,7 @@ export default abstract class LeafletMap { rawMarkers.forEach(data => { if (data.rotationAngle || data.rotationAngle === 0) { const currentImage: MarkerImageInfo = this.options.useMarkerImageFunction ? - safeExecute(this.options.markerImageFunction, + safeExecute(this.options.parsedMarkerImageFunction, [data, this.options.markerImages, markersData, data.dsIndex]) : this.options.currentImage; const style = currentImage ? 'background-image: url(' + currentImage.url + ');' : ''; this.options.icon = { icon: L.divIcon({ @@ -833,7 +824,7 @@ export default abstract class LeafletMap { updatedMarkers.push(m); } } else { - m = this.createMarker(data.entityName, data, markersData, this.options, updateBounds, callback); + m = this.createMarker(data.entityName, data, markersData, this.options, updateBounds, callback, this.options.snappable); if (m) { createdMarkers.push(m); } @@ -867,9 +858,9 @@ export default abstract class LeafletMap { this.saveLocation(data, this.convertToCustomFormat(e.target._latlng)).subscribe(); } - private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: UnitedMapSettings, - updateBounds = true, callback?): Marker { - const newMarker = new Marker(this, this.convertPosition(data), settings, data, dataSources, this.dragMarker); + private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: Partial, + updateBounds = true, callback?, snappable = false): Marker { + const newMarker = new Marker(this, this.convertPosition(data), settings, data, dataSources, this.dragMarker, snappable); if (callback) { newMarker.leafletMarker.on('click', () => { callback(data, true); @@ -885,7 +876,7 @@ export default abstract class LeafletMap { return newMarker; } - private updateMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings): Marker { + private updateMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: Partial): Marker { const marker: Marker = this.markers.get(key); const location = this.convertPosition(data); marker.updateMarkerPosition(location); @@ -933,7 +924,7 @@ export default abstract class LeafletMap { if (!!this.convertPosition(pdata)) { const dsData = pointsData.map(ds => ds[tsIndex]); if (this.options.useColorPointFunction) { - pointColor = safeExecute(this.options.colorPointFunction, [pdata, dsData, pdata.dsIndex]); + pointColor = safeExecute(this.options.parsedColorPointFunction, [pdata, dsData, pdata.dsIndex]); } const point = L.circleMarker(this.convertPosition(pdata), { color: pointColor, @@ -980,7 +971,8 @@ export default abstract class LeafletMap { }); } - createPolyline(data: FormattedData, tsData: FormattedData[], dsData: FormattedData[], settings: PolylineSettings, updateBounds = true) { + createPolyline(data: FormattedData, tsData: FormattedData[], dsData: FormattedData[], + settings: Partial, updateBounds = true) { const poly = new Polyline(this.map, tsData.map(el => this.convertPosition(el)).filter(el => !!el), data, dsData, settings); if (updateBounds) { @@ -990,7 +982,8 @@ export default abstract class LeafletMap { this.polylines.set(data.entityName, poly); } - updatePolyline(data: FormattedData, tsData: FormattedData[], dsData: FormattedData[], settings: PolylineSettings, updateBounds = true) { + updatePolyline(data: FormattedData, tsData: FormattedData[], dsData: FormattedData[], + settings: Partial, updateBounds = true) { const poly = this.polylines.get(data.entityName); const oldBounds = poly.leafletPoly.getBounds(); poly.updatePolyline(tsData.map(el => this.convertPosition(el)).filter(el => !!el), data, dsData, settings); @@ -1031,7 +1024,7 @@ export default abstract class LeafletMap { if (this.polygons.get(data.entityName)) { this.updatePolygon(data, polyData, this.options, updateBounds); } else { - this.createPolygon(data, polyData, this.options, updateBounds); + this.createPolygon(data, polyData, this.options, updateBounds, this.options.snappable); } keys.push(data.entityName); } @@ -1066,8 +1059,9 @@ export default abstract class LeafletMap { this.saveLocation(data, this.convertPolygonToCustomFormat(coordinates)).subscribe(() => {}); } - createPolygon(polyData: FormattedData, dataSources: FormattedData[], settings: UnitedMapSettings, updateBounds = true) { - const polygon = new Polygon(this.map, polyData, dataSources, settings, this.dragPolygonVertex); + createPolygon(polyData: FormattedData, dataSources: FormattedData[], settings: Partial, + updateBounds = true, snappable = false) { + const polygon = new Polygon(this, polyData, dataSources, settings, this.dragPolygonVertex, snappable); if (updateBounds) { const bounds = polygon.leafletPoly.getBounds(); this.fitBounds(bounds); @@ -1075,7 +1069,7 @@ export default abstract class LeafletMap { this.polygons.set(polyData.entityName, polygon); } - updatePolygon(polyData: FormattedData, dataSources: FormattedData[], settings: PolygonSettings, updateBounds = true) { + updatePolygon(polyData: FormattedData, dataSources: FormattedData[], settings: Partial, updateBounds = true) { const poly = this.polygons.get(polyData.entityName); const oldBounds = poly.leafletPoly.getBounds(); poly.updatePolygon(polyData, dataSources, settings); @@ -1167,7 +1161,7 @@ export default abstract class LeafletMap { } createdCircle(data: FormattedData, dataSources: FormattedData[], updateBounds = true) { - const circle = new Circle(this, data, dataSources, this.options, this.dragCircleVertex); + const circle = new Circle(this, data, dataSources, this.options, this.dragCircleVertex, this.options.snappable); if (updateBounds) { const bounds = circle.leafletCircle.getBounds(); this.fitBounds(bounds); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts index 9caf7f2c13..7df76f670d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts @@ -17,138 +17,433 @@ import { Datasource, FormattedData } from '@app/shared/models/widget.models'; import tinycolor from 'tinycolor2'; import { BaseIconOptions, Icon } from 'leaflet'; +import { DeviceProfileType } from '@shared/models/device.models'; export const DEFAULT_MAP_PAGE_SIZE = 16384; export const DEFAULT_ZOOM_LEVEL = 8; +export type MarkerImageInfo = { + url: string; + size: number; + markerOffset?: [number, number]; + tooltipOffset?: [number, number]; +}; + +export type MarkerIconInfo = { + icon: Icon; + size: [number, number]; +}; + +export interface MapImage { + imageUrl: string; + aspect: number; + update?: boolean; +} + +export type actionsHandler = ($event: Event, datasource: Datasource) => void; + +export interface CircleData { + latitude: number; + longitude: number; + radius: number; +} + export type GenericFunction = (data: FormattedData, dsData: FormattedData[], dsIndex: number) => string; export type MarkerImageFunction = (data: FormattedData, dsData: FormattedData[], dsIndex: number) => MarkerImageInfo; export type PosFuncton = (origXPos, origYPos) => { x, y }; export type MarkerIconReadyFunction = (icon: MarkerIconInfo) => void; -export type MapSettings = { - draggableMarker: boolean; - editablePolygon: boolean; - posFunction: PosFuncton; - defaultZoomLevel?: number; - disableScrollZooming?: boolean; - disableZoomControl?: boolean; - minZoomLevel?: number; - useClusterMarkers: boolean; - latKeyName?: string; - lngKeyName?: string; - xPosKeyName?: string; - yPosKeyName?: string; - imageEntityAlias: string; - imageUrlAttribute: string; - mapProvider: MapProviders; - mapProviderHere: string; - mapUrl?: string; - mapImageUrl?: string; - provider?: MapProviders; - credentials?: any; // declare credentials format - gmApiKey?: string; - defaultCenterPosition?: [number, number]; - markerClusteringSetting?; - useDefaultCenterPosition?: boolean; - gmDefaultMapType?: string; - useLabelFunction: boolean; - zoomOnClick: boolean, - maxZoom: number, - showCoverageOnHover: boolean, - animate: boolean, - maxClusterRadius: number, - spiderfyOnMaxZoom: boolean, - chunkedLoading: boolean, - removeOutsideVisibleBounds: boolean, - useCustomProvider: boolean, - customProviderTileUrl: string; - mapPageSize: number; +export enum GoogleMapType { + roadmap = 'roadmap', + satellite = 'satellite', + hybrid = 'hybrid', + terrain = 'terrain' +} + +export const googleMapTypeProviderTranslationMap = new Map( + [ + [GoogleMapType.roadmap, 'widgets.maps.google-map-type-roadmap'], + [GoogleMapType.satellite, 'widgets.maps.google-map-type-satelite'], + [GoogleMapType.hybrid, 'widgets.maps.google-map-type-hybrid'], + [GoogleMapType.terrain, 'widgets.maps.google-map-type-terrain'] + ] +); + +export interface GoogleMapProviderSettings { + gmApiKey: string; + gmDefaultMapType: GoogleMapType; +} + +export const defaultGoogleMapProviderSettings: GoogleMapProviderSettings = { + gmApiKey: 'AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q', + gmDefaultMapType: GoogleMapType.roadmap +}; + +export enum OpenStreetMapProvider { + openStreetMapnik = 'OpenStreetMap.Mapnik', + openStreetHot = 'OpenStreetMap.HOT', + esriWorldStreetMap = 'Esri.WorldStreetMap', + esriWorldTopoMap = 'Esri.WorldTopoMap', + cartoDbPositron = 'CartoDB.Positron', + cartoDbDarkMatter = 'CartoDB.DarkMatter' +} + +export const openStreetMapProviderTranslationMap = new Map( + [ + [OpenStreetMapProvider.openStreetMapnik, 'widgets.maps.openstreet-provider-mapnik'], + [OpenStreetMapProvider.openStreetHot, 'widgets.maps.openstreet-provider-hot'], + [OpenStreetMapProvider.esriWorldStreetMap, 'widgets.maps.openstreet-provider-esri-street'], + [OpenStreetMapProvider.esriWorldTopoMap, 'widgets.maps.openstreet-provider-esri-topo'], + [OpenStreetMapProvider.cartoDbPositron, 'widgets.maps.openstreet-provider-cartodb-positron'], + [OpenStreetMapProvider.cartoDbDarkMatter, 'widgets.maps.openstreet-provider-cartodb-dark-matter'] + ] +); + +export interface OpenStreetMapProviderSettings { + mapProvider: OpenStreetMapProvider; + useCustomProvider: boolean; + customProviderTileUrl?: string; +} + +export const defaultOpenStreetMapProviderSettings: OpenStreetMapProviderSettings = { + mapProvider: OpenStreetMapProvider.openStreetMapnik, + useCustomProvider: false, + customProviderTileUrl: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' +}; + +export enum HereMapProvider { + hereNormalDay = 'HERE.normalDay', + hereNormalNight = 'HERE.normalNight', + hereHybridDay = 'HERE.hybridDay', + hereTerrainDay = 'HERE.terrainDay' +} + +export const hereMapProviderTranslationMap = new Map( + [ + [HereMapProvider.hereNormalDay, 'widgets.maps.here-map-normal-day'], + [HereMapProvider.hereNormalNight, 'widgets.maps.here-map-normal-night'], + [HereMapProvider.hereHybridDay, 'widgets.maps.here-map-hybrid-day'], + [HereMapProvider.hereTerrainDay, 'widgets.maps.here-map-terrain-day'] + ] +); + +export interface HereMapProviderSettings { + mapProviderHere: HereMapProvider; + credentials: { + app_id: string; + app_code: string; + }; +} + +export const defaultHereMapProviderSettings: HereMapProviderSettings = { + mapProviderHere: HereMapProvider.hereNormalDay, + credentials: { + app_id: 'AhM6TzD9ThyK78CT3ptx', + app_code: 'p6NPiITB3Vv0GMUFnkLOOg' + } +}; + +export interface ImageMapProviderSettings { + mapImageUrl?: string; + imageEntityAlias?: string; + imageUrlAttribute?: string; +} + +export const defaultImageMapProviderSettings: ImageMapProviderSettings = { + mapImageUrl: 'data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==', + imageEntityAlias: '', + imageUrlAttribute: '' +}; + +export enum TencentMapType { + roadmap = 'roadmap', + satellite = 'satellite', + hybrid = 'hybrid' +} + +export const tencentMapTypeProviderTranslationMap = new Map( + [ + [TencentMapType.roadmap, 'widgets.maps.tencent-map-type-roadmap'], + [TencentMapType.satellite, 'widgets.maps.tencent-map-type-satelite'], + [TencentMapType.hybrid, 'widgets.maps.tencent-map-type-hybrid'] + ] +); + +export interface TencentMapProviderSettings { + tmApiKey: string; + tmDefaultMapType: TencentMapType; +} + +export const defaultTencentMapProviderSettings: TencentMapProviderSettings = { + tmApiKey: '84d6d83e0e51e481e50454ccbe8986b', + tmDefaultMapType: TencentMapType.roadmap }; export enum MapProviders { - google = 'google-map', - openstreet = 'openstreet-map', - here = 'here', - image = 'image-map', - tencent = 'tencent-map' + google = 'google-map', + openstreet = 'openstreet-map', + here = 'here', + image = 'image-map', + tencent = 'tencent-map' } -export type MarkerImageInfo = { - url: string; - size: number; - markerOffset?: [number, number]; - tooltipOffset?: [number, number]; +export const mapProviderTranslationMap = new Map( + [ + [MapProviders.google, 'widgets.maps.map-provider-google'], + [MapProviders.openstreet, 'widgets.maps.map-provider-openstreet'], + [MapProviders.here, 'widgets.maps.map-provider-here'], + [MapProviders.image, 'widgets.maps.map-provider-image'], + [MapProviders.tencent, 'widgets.maps.map-provider-tencent'] + ] +); + +export interface MapProviderSettings extends GoogleMapProviderSettings, OpenStreetMapProviderSettings, + HereMapProviderSettings, ImageMapProviderSettings, TencentMapProviderSettings { + provider: MapProviders; +} + +export const defaultMapProviderSettings: MapProviderSettings = { + provider: MapProviders.openstreet, + ...defaultGoogleMapProviderSettings, + ...defaultOpenStreetMapProviderSettings, + ...defaultHereMapProviderSettings, + ...defaultImageMapProviderSettings, + ...defaultTencentMapProviderSettings }; -export type MarkerIconInfo = { - icon: Icon; - size: [number, number]; +export interface CommonMapSettings { + latKeyName: string; + lngKeyName: string; + xPosKeyName: string; + yPosKeyName: string; + defaultZoomLevel: number; + defaultCenterPosition?: string; + disableScrollZooming: boolean; + disableZoomControl: boolean; + fitMapBounds: boolean; + useDefaultCenterPosition: boolean; + mapPageSize: number; +} + +export interface WidgetCommonMapSettings extends CommonMapSettings { + parsedDefaultCenterPosition: [number, number]; + minZoomLevel: number; +} + +export interface WidgetToolipSettings { + tooltipAction: { [name: string]: actionsHandler }; +} + +export const defaultCommonMapSettings: CommonMapSettings = { + latKeyName: 'latitude', + lngKeyName: 'longitude', + xPosKeyName: 'xPos', + yPosKeyName: 'yPos', + defaultZoomLevel: null, + defaultCenterPosition: '0,0', + disableScrollZooming: false, + disableZoomControl: false, + fitMapBounds: true, + useDefaultCenterPosition: false, + mapPageSize: DEFAULT_MAP_PAGE_SIZE +}; + +export interface TripAnimationCommonSettings { + normalizationStep: number; + latKeyName: string; + lngKeyName: string; + showTooltip: boolean; + tooltipColor: string; + tooltipFontColor: string; + tooltipOpacity: number; + useTooltipFunction: boolean; + tooltipPattern?: string; + tooltipFunction?: string; + autocloseTooltip: boolean; +} + +export interface WidgetTripAnimationCommonSettings extends TripAnimationCommonSettings { + parsedTooltipFunction: GenericFunction; +} + +export const defaultTripAnimationCommonSettings: TripAnimationCommonSettings = { + normalizationStep: 1000, + latKeyName: 'latitude', + lngKeyName: 'longitude', + showTooltip: true, + tooltipColor: '#fff', + tooltipFontColor: '#000', + tooltipOpacity: 1, + useTooltipFunction: false, + tooltipPattern: '${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}', + tooltipFunction: null, + autocloseTooltip: true }; -export type MarkerSettings = { - tooltipPattern?: any; - tooltipAction: { [name: string]: actionsHandler }; - icon?: MarkerIconInfo; - showLabel?: boolean; - label: string; - labelColor: string; - labelText: string; - useLabelFunction: boolean; - draggableMarker: boolean; - showTooltip?: boolean; - useTooltipFunction: boolean; - useColorFunction: boolean; - color?: string; - tinyColor?: tinycolor.Instance; - autocloseTooltip: boolean; - showTooltipAction: string; - useClusterMarkers: boolean; - currentImage?: MarkerImageInfo; - useMarkerImageFunction?: boolean; - markerImages?: string[]; - markerImageSize: number; - fitMapBounds: boolean; - markerImage: string; - markerClick: { [name: string]: actionsHandler }; - colorFunction: GenericFunction; - tooltipFunction: GenericFunction; - labelFunction: GenericFunction; - markerImageFunction?: MarkerImageFunction; - markerOffsetX: number; - markerOffsetY: number; - tooltipOffsetX: number; - tooltipOffsetY: number; +export enum ShowTooltipAction { + click = 'click', + hover = 'hover' +} + +export const showTooltipActionTranslationMap = new Map( + [ + [ShowTooltipAction.click, 'widgets.maps.show-tooltip-action-click'], + [ShowTooltipAction.hover, 'widgets.maps.show-tooltip-action-hover'] + ] +); + +export interface MarkersSettings { + markerOffsetX: number; + markerOffsetY: number; + posFunction?: string; + draggableMarker: boolean; + showLabel: boolean; + useLabelFunction: boolean; + label?: string; + labelFunction?: string; + showTooltip: boolean; + showTooltipAction: ShowTooltipAction; + autocloseTooltip: boolean; + useTooltipFunction: boolean; + tooltipPattern?: string; + tooltipFunction?: string; + tooltipOffsetX: number; + tooltipOffsetY: number; + color?: string; + useColorFunction: boolean; + colorFunction?: string; + useMarkerImageFunction: boolean; + markerImage?: string; + markerImageSize?: number; + markerImageFunction?: string; + markerImages?: string[]; +} + +export interface WidgetMarkersSettings extends MarkersSettings, WidgetToolipSettings { + parsedLabelFunction: GenericFunction; + parsedTooltipFunction: GenericFunction; + parsedColorFunction: GenericFunction; + parsedMarkerImageFunction: MarkerImageFunction; + markerClick: { [name: string]: actionsHandler }; + currentImage: MarkerImageInfo; + tinyColor: tinycolor.Instance; + icon: MarkerIconInfo; +} + +export const defaultMarkersSettings: MarkersSettings = { + markerOffsetX: 0.5, + markerOffsetY: 1, + posFunction: 'return {x: origXPos, y: origYPos};', + draggableMarker: false, + showLabel: true, + useLabelFunction: false, + label: '${entityName}', + labelFunction: null, + showTooltip: true, + showTooltipAction: ShowTooltipAction.click, + autocloseTooltip: true, + useTooltipFunction: false, + tooltipPattern: '${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}', + tooltipFunction: null, + tooltipOffsetX: 0, + tooltipOffsetY: -1, + color: '#FE7569', + useColorFunction: false, + colorFunction: null, + useMarkerImageFunction: false, + markerImage: null, + markerImageSize: 34, + markerImageFunction: null, + markerImages: [] +}; + +export interface TripAnimationMarkerSettings { + rotationAngle: number; + showLabel: boolean; + useLabelFunction: boolean; + label?: string; + labelFunction?: string; + useMarkerImageFunction: boolean; + markerImage?: string; + markerImageSize?: number; + markerImageFunction?: string; + markerImages?: string[]; +} + +export interface WidgetTripAnimationMarkerSettings extends TripAnimationMarkerSettings { + parsedLabelFunction: GenericFunction; + parsedMarkerImageFunction: MarkerImageFunction; +} + +export const defaultTripAnimationMarkersSettings: TripAnimationMarkerSettings = { + rotationAngle: 0, + showLabel: true, + useLabelFunction: false, + label: '${entityName}', + labelFunction: null, + useMarkerImageFunction: false, + markerImage: null, + markerImageSize: 34, + markerImageFunction: null, + markerImages: [] }; -export type PolygonSettings = { - showPolygon: boolean; - polygonKeyName: string; - polKeyName: string; // deprecated - polygonStrokeOpacity: number; - polygonOpacity: number; - polygonStrokeWeight: number; - polygonStrokeColor: string; - polygonColor: string; - showPolygonLabel?: boolean; - polygonLabel: string; - polygonLabelColor: string; - polygonLabelText: string; - usePolygonLabelFunction: boolean; - showPolygonTooltip: boolean; - autoClosePolygonTooltip: boolean; - showPolygonTooltipAction: string; - tooltipAction: { [name: string]: actionsHandler }; - polygonTooltipPattern: string; - usePolygonTooltipFunction: boolean; - polygonClick: { [name: string]: actionsHandler }; - usePolygonColorFunction: boolean; - usePolygonStrokeColorFunction: boolean; - polygonTooltipFunction: GenericFunction; - polygonColorFunction?: GenericFunction; - polygonStrokeColorFunction?: GenericFunction; - polygonLabelFunction?: GenericFunction; - editablePolygon: boolean; +export interface PolygonSettings { + showPolygon: boolean; + polygonKeyName: string; + editablePolygon: boolean; + showPolygonLabel: boolean; + usePolygonLabelFunction: boolean; + polygonLabel?: string; + polygonLabelFunction?: string; + showPolygonTooltip: boolean; + showPolygonTooltipAction: ShowTooltipAction; + autoClosePolygonTooltip: boolean; + usePolygonTooltipFunction: boolean; + polygonTooltipPattern?: string; + polygonTooltipFunction?: string; + polygonColor?: string; + polygonOpacity?: number; + usePolygonColorFunction: boolean; + polygonColorFunction?: string; + polygonStrokeColor?: string; + polygonStrokeOpacity?: number; + polygonStrokeWeight?: number; + usePolygonStrokeColorFunction: boolean; + polygonStrokeColorFunction?: string; +} + +export interface WidgetPolygonSettings extends PolygonSettings, WidgetToolipSettings { + parsedPolygonLabelFunction: GenericFunction; + parsedPolygonTooltipFunction: GenericFunction; + parsedPolygonColorFunction: GenericFunction; + parsedPolygonStrokeColorFunction: GenericFunction; + polygonClick: { [name: string]: actionsHandler }; +} + +export const defaultPolygonSettings: PolygonSettings = { + showPolygon: false, + polygonKeyName: 'perimeter', + editablePolygon: false, + showPolygonLabel: false, + usePolygonLabelFunction: false, + polygonLabel: '${entityName}', + polygonLabelFunction: null, + showPolygonTooltip: false, + showPolygonTooltipAction: ShowTooltipAction.click, + autoClosePolygonTooltip: true, + usePolygonTooltipFunction: false, + polygonTooltipPattern: '${entityName}

TimeStamp: ${ts:7}', + polygonTooltipFunction: null, + polygonColor: '#3388ff', + polygonOpacity: 0.2, + usePolygonColorFunction: false, + polygonColorFunction: null, + polygonStrokeColor: '#3388ff', + polygonStrokeOpacity: 1, + polygonStrokeWeight: 3, + usePolygonStrokeColorFunction: false, + polygonStrokeColorFunction: null }; export interface CircleSettings { @@ -157,178 +452,221 @@ export interface CircleSettings { editableCircle: boolean; showCircleLabel: boolean; useCircleLabelFunction: boolean; - circleLabel: string; - circleLabelFunction?: GenericFunction; - circleFillColor: string; - useCircleFillColorFunction: boolean; - circleFillColorFunction?: GenericFunction; - circleFillColorOpacity: number; - circleStrokeColor: string; - useCircleStrokeColorFunction: boolean; - circleStrokeColorFunction: GenericFunction; - circleStrokeOpacity: number; - circleStrokeWeight: number; + circleLabel?: string; + circleLabelFunction?: string; showCircleTooltip: boolean; - showCircleTooltipAction: string; + showCircleTooltipAction: ShowTooltipAction; autoCloseCircleTooltip: boolean; useCircleTooltipFunction: boolean; - circleTooltipPattern: string; - circleTooltipFunction?: GenericFunction; - circleClick?: { [name: string]: actionsHandler }; -} - -export type PolylineSettings = { - usePolylineDecorator: any; - autocloseTooltip: boolean; - showTooltipAction: string; - useColorFunction: any; - tooltipAction: { [name: string]: actionsHandler }; - color: string; - useStrokeOpacityFunction: any; - strokeOpacity: number; - useStrokeWeightFunction: any; - strokeWeight: number; - decoratorOffset: string | number; - endDecoratorOffset: string | number; - decoratorRepeat: string | number; - decoratorSymbol: any; - decoratorSymbolSize: any; - useDecoratorCustomColor: any; - decoratorCustomColor: any; - - - colorFunction: GenericFunction; - strokeOpacityFunction: GenericFunction; - strokeWeightFunction: GenericFunction; + circleTooltipPattern?: string; + circleTooltipFunction?: string; + circleFillColor?: string; + circleFillColorOpacity?: number; + useCircleFillColorFunction: boolean; + circleFillColorFunction?: string; + circleStrokeColor?: string; + circleStrokeOpacity?: number; + circleStrokeWeight?: number; + useCircleStrokeColorFunction: boolean; + circleStrokeColorFunction?: string; +} + +export interface WidgetCircleSettings extends CircleSettings, WidgetToolipSettings { + parsedCircleLabelFunction: GenericFunction; + parsedCircleTooltipFunction: GenericFunction; + parsedCircleFillColorFunction: GenericFunction; + parsedCircleStrokeColorFunction: GenericFunction; + circleClick: { [name: string]: actionsHandler }; +} + +export const defaultCircleSettings: CircleSettings = { + showCircle: false, + circleKeyName: 'perimeter', + editableCircle: false, + showCircleLabel: false, + useCircleLabelFunction: false, + circleLabel: '${entityName}', + circleLabelFunction: null, + showCircleTooltip: false, + showCircleTooltipAction: ShowTooltipAction.click, + autoCloseCircleTooltip: true, + useCircleTooltipFunction: false, + circleTooltipPattern: '${entityName}

TimeStamp: ${ts:7}', + circleTooltipFunction: null, + circleFillColor: '#3388ff', + circleFillColorOpacity: 0.2, + useCircleFillColorFunction: false, + circleFillColorFunction: null, + circleStrokeColor: '#3388ff', + circleStrokeOpacity: 1, + circleStrokeWeight: 3, + useCircleStrokeColorFunction: false, + circleStrokeColorFunction: null }; -export interface EditorSettings { - snappable: boolean; - initDragMode: boolean; - hideAllControlButton: boolean; - hideDrawControlButton: boolean; - hideEditControlButton: boolean; - hideRemoveControlButton: boolean; +export enum PolylineDecoratorSymbol { + arrowHead = 'arrowHead', + dash = 'dash' } -export interface HistorySelectSettings { - buttonColor: string; +export const polylineDecoratorSymbolTranslationMap = new Map( + [ + [PolylineDecoratorSymbol.arrowHead, 'widgets.maps.decorator-symbol-arrow-head'], + [PolylineDecoratorSymbol.dash, 'widgets.maps.decorator-symbol-dash'] + ] +); + +export interface PolylineSettings { + useStrokeWeightFunction?: boolean; + strokeWeight: number; + strokeWeightFunction?: string; + useStrokeOpacityFunction?: boolean; + strokeOpacity: number; + strokeOpacityFunction?: string; + useColorFunction?: boolean; + color?: string; + colorFunction?: string; + usePolylineDecorator?: boolean; + decoratorSymbol?: PolylineDecoratorSymbol; + decoratorSymbolSize?: number; + useDecoratorCustomColor?: boolean; + decoratorCustomColor?: string; + decoratorOffset?: string; + endDecoratorOffset?: string; + decoratorRepeat?: string; } -export interface MapImage { - imageUrl: string; - aspect: number; - update?: boolean; -} - -export interface TripAnimationSettings extends PolygonSettings, CircleSettings { - showPoints: boolean; - pointColor: string; - pointSize: number; - pointTooltipOnRightPanel: boolean; - usePointAsAnchor: boolean; - normalizationStep: number; - showPolygon: boolean; - showLabel: boolean; - showTooltip: boolean; - latKeyName: string; - lngKeyName: string; - rotationAngle: number; - label: string; - tooltipPattern: string; - tooltipColor: string; - tooltipOpacity: number; - tooltipFontColor: string; - useTooltipFunction: boolean; - useLabelFunction: boolean; - pointAsAnchorFunction: GenericFunction; - tooltipFunction: GenericFunction; - labelFunction: GenericFunction; - useColorPointFunction: boolean; - colorPointFunction: GenericFunction; +export interface WidgetPolylineSettings extends PolylineSettings { + parsedColorFunction: GenericFunction; + parsedStrokeOpacityFunction: GenericFunction; + parsedStrokeWeightFunction: GenericFunction; } -export type actionsHandler = ($event: Event, datasource: Datasource) => void; +export const defaultRouteMapSettings: PolylineSettings = { + strokeWeight: 2, + strokeOpacity: 1.0 +}; -export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings - & CircleSettings & TripAnimationSettings & EditorSettings; - -export const defaultSettings: Partial = { - xPosKeyName: 'xPos', - yPosKeyName: 'yPos', - markerOffsetX: 0.5, - markerOffsetY: 1, - tooltipOffsetX: 0, - tooltipOffsetY: -1, - latKeyName: 'latitude', - lngKeyName: 'longitude', - polygonKeyName: 'perimeter', - showLabel: false, - label: '${entityName}', - showTooltip: false, - useDefaultCenterPosition: false, - showTooltipAction: 'click', - autocloseTooltip: false, - showPolygon: false, - labelColor: '#000000', - color: '#FE7569', - showPolygonLabel: false, - polygonColor: '#3388ff', - polygonStrokeColor: '#3388ff', - polygonLabelColor: '#000000', - polygonOpacity: 0.2, - polygonStrokeOpacity: 1, - polygonStrokeWeight: 3, - showPolygonTooltipAction: 'click', - autoClosePolygonTooltip: true, - useLabelFunction: false, - markerImages: [], - strokeWeight: 2, - strokeOpacity: 1.0, - disableScrollZooming: false, - minZoomLevel: 16, - credentials: '', - markerClusteringSetting: null, - draggableMarker: false, - editablePolygon: false, - fitMapBounds: true, - mapPageSize: DEFAULT_MAP_PAGE_SIZE, - snappable: false, - initDragMode: false, - hideAllControlButton: false, - hideDrawControlButton: false, - hideEditControlButton: false, - hideRemoveControlButton: false, - showCircle: true, - circleKeyName: 'perimeter', - editableCircle: false, - showCircleLabel: false, - useCircleLabelFunction: false, - circleLabel: '${entityName}', - circleFillColor: '#3388ff', - useCircleFillColorFunction: false, - circleFillColorOpacity: 0.2, - circleStrokeColor: '#3388ff', - useCircleStrokeColorFunction: false, - circleStrokeOpacity: 1, - circleStrokeWeight: 3, - showCircleTooltip: false, - showCircleTooltipAction: 'click', - autoCloseCircleTooltip: true, - useCircleTooltipFunction: false +export const defaultTripAnimationPathSettings: PolylineSettings = { + color: null, + strokeWeight: 2, + strokeOpacity: 1, + useColorFunction: false, + colorFunction: null, + usePolylineDecorator: false, + decoratorSymbol: PolylineDecoratorSymbol.arrowHead, + decoratorSymbolSize: 10, + useDecoratorCustomColor: false, + decoratorCustomColor: '#000', + decoratorOffset: '20px', + endDecoratorOffset: '20px', + decoratorRepeat: '20px' }; -export interface CircleData { - latitude: number; - longitude: number; - radius: number; +export interface PointsSettings { + showPoints?: boolean; + pointColor?: string; + useColorPointFunction?: false; + colorPointFunction?: string; + pointSize?: number; + usePointAsAnchor?: false; + pointAsAnchorFunction?: string; + pointTooltipOnRightPanel?: boolean; +} + +export interface WidgetPointsSettings extends PointsSettings { + parsedColorPointFunction: GenericFunction; + parsedPointAsAnchorFunction: GenericFunction; +} + +export const defaultTripAnimationPointSettings: PointsSettings = { + showPoints: false, + pointColor: null, + useColorPointFunction: false, + colorPointFunction: null, + pointSize: 10, + usePointAsAnchor: false, + pointAsAnchorFunction: null, + pointTooltipOnRightPanel: true +}; + +export interface MarkerClusteringSettings { + useClusterMarkers: boolean; + zoomOnClick: boolean; + maxZoom: number; + maxClusterRadius: number; + animate: boolean; + spiderfyOnMaxZoom: boolean; + showCoverageOnHover: boolean; + chunkedLoading: boolean; + removeOutsideVisibleBounds: boolean; +} + +export const defaultMarkerClusteringSettings: MarkerClusteringSettings = { + useClusterMarkers: false, + zoomOnClick: true, + maxZoom: null, + maxClusterRadius: 80, + animate: true, + spiderfyOnMaxZoom: false, + showCoverageOnHover: true, + chunkedLoading: false, + removeOutsideVisibleBounds: true +}; + +export interface MapEditorSettings { + snappable: boolean; + initDragMode: boolean; + hideAllControlButton: boolean; + hideDrawControlButton: boolean; + hideEditControlButton: boolean; + hideRemoveControlButton: boolean; +} + +export const defaultMapEditorSettings: MapEditorSettings = { + snappable: false, + initDragMode: false, + hideAllControlButton: false, + hideDrawControlButton: false, + hideEditControlButton: false, + hideRemoveControlButton: false +}; + +export type UnitedMapSettings = MapProviderSettings & CommonMapSettings & MarkersSettings & + PolygonSettings & CircleSettings & PolylineSettings & PointsSettings & MarkerClusteringSettings & MapEditorSettings; + +export const defaultMapSettings: UnitedMapSettings = { + ...defaultMapProviderSettings, + ...defaultCommonMapSettings, + ...defaultMarkersSettings, + ...defaultPolygonSettings, + ...defaultCircleSettings, + ...defaultRouteMapSettings, + ...defaultMarkerClusteringSettings, + ...defaultMapEditorSettings +}; + +export type WidgetUnitedMapSettings = UnitedMapSettings & Partial; + +export type UnitedTripAnimationSettings = MapProviderSettings & TripAnimationCommonSettings & TripAnimationMarkerSettings & + PolylineSettings & PointsSettings & PolygonSettings & CircleSettings; + +export const defaultTripAnimationSettings: UnitedTripAnimationSettings = { + ...defaultMapProviderSettings, + ...defaultTripAnimationCommonSettings, + ...defaultTripAnimationMarkersSettings, + ...defaultTripAnimationPathSettings, + ...defaultTripAnimationPointSettings, + ...defaultPolygonSettings, + ...defaultCircleSettings, +}; + +export interface HistorySelectSettings { + buttonColor: string; } -export const circleDataKeys: Array = ['latitude', 'longitude', 'radius']; +export type WidgetUnitedTripAnimationSettings = UnitedTripAnimationSettings & HistorySelectSettings & + Partial; -export const hereProviders = [ - 'HERE.normalDay', - 'HERE.normalNight', - 'HERE.hybridDay', - 'HERE.terrainDay' -]; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts index f2233ce236..d603848018 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts @@ -14,7 +14,12 @@ /// limitations under the License. /// -import { defaultSettings, hereProviders, MapProviders, UnitedMapSettings } from './map-models'; +import { + defaultMapSettings, + MapProviders, + UnitedMapSettings, + WidgetUnitedMapSettings +} from './map-models'; import LeafletMap from './leaflet-map'; import { commonMapSettingsSchema, @@ -96,7 +101,7 @@ export class MapWidgetController implements MapWidgetInterface { provider: MapProviders; schema: JsonSettingsSchema; data: DatasourceData[]; - settings: UnitedMapSettings; + settings: WidgetUnitedMapSettings; pageLink: EntityDataPageLink; public static dataKeySettingsSchema(): object { @@ -190,7 +195,7 @@ export class MapWidgetController implements MapWidgetInterface { if (isDefined(lat) && isDefined(lng)) { const point = lat != null && lng !== null ? L.latLng(lat, lng) : null; markerValue = this.map.convertToCustomFormat(point); - } else if (this.settings.mapProvider !== MapProviders.image) { + } else if (this.settings.provider !== MapProviders.image) { markerValue = { [this.settings.latKeyName]: e[this.settings.latKeyName], [this.settings.lngKeyName]: e[this.settings.lngKeyName], @@ -269,60 +274,55 @@ export class MapWidgetController implements MapWidgetInterface { } } - initSettings(settings: UnitedMapSettings, isEditMap?: boolean): UnitedMapSettings { + initSettings(settings: UnitedMapSettings, isEditMap?: boolean): WidgetUnitedMapSettings { const functionParams = ['data', 'dsData', 'dsIndex']; this.provider = settings.provider || this.mapProvider; - if (this.provider === MapProviders.here && !settings.mapProviderHere) { - if (settings.mapProvider && hereProviders.includes(settings.mapProvider)) { - settings.mapProviderHere = settings.mapProvider; - } else { - settings.mapProviderHere = hereProviders[0]; - } - } - const customOptions: Partial = { + const parsedOptions: Partial = { provider: this.provider, - mapUrl: settings?.mapImageUrl, - labelFunction: parseFunction(settings.labelFunction, functionParams), - tooltipFunction: parseFunction(settings.tooltipFunction, functionParams), - colorFunction: parseFunction(settings.colorFunction, functionParams), - colorPointFunction: parseFunction(settings.colorPointFunction, functionParams), - polygonLabelFunction: parseFunction(settings.polygonLabelFunction, functionParams), - polygonColorFunction: parseFunction(settings.polygonColorFunction, functionParams), - polygonStrokeColorFunction: parseFunction(settings.polygonStrokeColorFunction, functionParams), - polygonTooltipFunction: parseFunction(settings.polygonTooltipFunction, functionParams), - circleLabelFunction: parseFunction(settings.circleLabelFunction, functionParams), - circleStrokeColorFunction: parseFunction(settings.circleStrokeColorFunction, functionParams), - circleFillColorFunction: parseFunction(settings.circleFillColorFunction, functionParams), - circleTooltipFunction: parseFunction(settings.circleTooltipFunction, functionParams), - markerImageFunction: parseFunction(settings.markerImageFunction, ['data', 'images', 'dsData', 'dsIndex']), - labelColor: this.ctx.widgetConfig.color, - polygonLabelColor: this.ctx.widgetConfig.color, - polygonKeyName: settings.polKeyName ? settings.polKeyName : settings.polygonKeyName, + parsedLabelFunction: parseFunction(settings.labelFunction, functionParams), + parsedTooltipFunction: parseFunction(settings.tooltipFunction, functionParams), + parsedColorFunction: parseFunction(settings.colorFunction, functionParams), + parsedColorPointFunction: parseFunction(settings.colorPointFunction, functionParams), + parsedStrokeOpacityFunction: parseFunction(settings.strokeOpacityFunction, functionParams), + parsedStrokeWeightFunction: parseFunction(settings.strokeWeightFunction, functionParams), + parsedPolygonLabelFunction: parseFunction(settings.polygonLabelFunction, functionParams), + parsedPolygonColorFunction: parseFunction(settings.polygonColorFunction, functionParams), + parsedPolygonStrokeColorFunction: parseFunction(settings.polygonStrokeColorFunction, functionParams), + parsedPolygonTooltipFunction: parseFunction(settings.polygonTooltipFunction, functionParams), + parsedCircleLabelFunction: parseFunction(settings.circleLabelFunction, functionParams), + parsedCircleStrokeColorFunction: parseFunction(settings.circleStrokeColorFunction, functionParams), + parsedCircleFillColorFunction: parseFunction(settings.circleFillColorFunction, functionParams), + parsedCircleTooltipFunction: parseFunction(settings.circleTooltipFunction, functionParams), + parsedMarkerImageFunction: parseFunction(settings.markerImageFunction, ['data', 'images', 'dsData', 'dsIndex']), + // labelColor: this.ctx.widgetConfig.color, + // polygonLabelColor: this.ctx.widgetConfig.color, + polygonKeyName: (settings as any).polKeyName ? (settings as any).polKeyName : settings.polygonKeyName, tooltipPattern: settings.tooltipPattern || '${entityName}

Latitude: ${' + settings.latKeyName + ':7}
Longitude: ${' + settings.lngKeyName + ':7}', - defaultCenterPosition: getDefCenterPosition(settings?.defaultCenterPosition), + parsedDefaultCenterPosition: getDefCenterPosition(settings?.defaultCenterPosition), currentImage: (settings.markerImage?.length) ? { url: settings.markerImage, size: settings.markerImageSize || 34 } : null }; if (isEditMap && !settings.hasOwnProperty('draggableMarker')) { - settings.draggableMarker = true; + parsedOptions.draggableMarker = true; } if (isEditMap && !settings.hasOwnProperty('editablePolygon')) { - settings.editablePolygon = true; + parsedOptions.editablePolygon = true; } - return { ...defaultSettings, ...settings, ...customOptions, }; + parsedOptions.minZoomLevel = 16; + return { ...defaultMapSettings, ...settings, ...parsedOptions }; } update() { - this.map.updateData(this.drawRoutes, this.settings.showPolygon); + this.map.updateData(this.drawRoutes); this.map.setLoading(false); } latestDataUpdate() { - this.map.updateData(this.drawRoutes, this.settings.showPolygon); + this.map.updateData(this.drawRoutes); } resize() { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts index 74a76b6625..528172b2ba 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts @@ -15,20 +15,22 @@ /// import L from 'leaflet'; -import { MarkerSettings, PolygonSettings, PolylineSettings } from './map-models'; +import { + ShowTooltipAction, WidgetToolipSettings +} from './map-models'; import { Datasource } from '@app/shared/models/widget.models'; export function createTooltip(target: L.Layer, - settings: MarkerSettings | PolylineSettings | PolygonSettings, + settings: Partial, datasource: Datasource, autoClose = false, - showTooltipAction = 'click', + showTooltipAction = ShowTooltipAction.click, content?: string | HTMLElement ): L.Popup { const popup = L.popup(); popup.setContent(content); target.bindPopup(popup, { autoClose, closeOnClick: false }); - if (showTooltipAction === 'hover') { + if (showTooltipAction === ShowTooltipAction.hover) { target.off('click'); target.on('mouseover', () => { target.openPopup(); @@ -47,7 +49,7 @@ export function createTooltip(target: L.Layer, return popup; } -export function bindPopupActions(popup: L.Popup, settings: MarkerSettings | PolylineSettings | PolygonSettings, +export function bindPopupActions(popup: L.Popup, settings: Partial, datasource: Datasource) { const actions = popup.getElement().getElementsByClassName('tb-custom-action'); Array.from(actions).forEach( diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts index 8d2262813a..09c9f7dad1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts @@ -18,9 +18,7 @@ import L, { LeafletMouseEvent } from 'leaflet'; import { MarkerIconInfo, MarkerIconReadyFunction, - MarkerImageInfo, - MarkerSettings, - UnitedMapSettings + MarkerImageInfo, WidgetMarkersSettings, } from './map-models'; import { bindPopupActions, createTooltip } from './maps-utils'; import { aspectCache, parseWithTranslation } from './common-maps-utils'; @@ -38,15 +36,17 @@ export class Marker { tooltipOffset: L.LatLngTuple; markerOffset: L.LatLngTuple; tooltip: L.Popup; - data: FormattedData; - dataSources: FormattedData[]; - constructor(private map: LeafletMap, private location: L.LatLng, public settings: UnitedMapSettings, - data?: FormattedData, dataSources?, onDragendListener?) { - this.setDataSources(data, dataSources); + constructor(private map: LeafletMap, + private location: L.LatLng, + private settings: Partial, + private data?: FormattedData, + private dataSources?, + private onDragendListener?, + snappable = false) { this.leafletMarker = L.marker(location, { pmIgnore: !settings.draggableMarker, - snapIgnore: !settings.snappable + snapIgnore: !snappable }); this.markerOffset = [ @@ -59,7 +59,7 @@ export class Marker { isDefined(settings.tooltipOffsetY) ? settings.tooltipOffsetY : -1, ]; - this.updateMarkerIcon(settings); + this.updateMarkerIcon(this.settings); if (settings.showTooltip) { this.tooltip = createTooltip(this.leafletMarker, settings, data.$datasource, @@ -100,7 +100,7 @@ export class Marker { updateMarkerTooltip(data: FormattedData) { if (!this.map.markerTooltipText || this.settings.useTooltipFunction) { const pattern = this.settings.useTooltipFunction ? - safeExecute(this.settings.tooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : this.settings.tooltipPattern; + safeExecute(this.settings.parsedTooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : this.settings.tooltipPattern; this.map.markerTooltipText = parseWithTranslation.prepareProcessPattern(pattern, true); this.map.replaceInfoTooltipMarker = processDataPattern(this.map.markerTooltipText, data); } @@ -117,17 +117,18 @@ export class Marker { } } - updateMarkerLabel(settings: MarkerSettings) { + updateMarkerLabel(settings: Partial) { this.leafletMarker.unbindTooltip(); if (settings.showLabel) { if (!this.map.markerLabelText || settings.useLabelFunction) { const pattern = settings.useLabelFunction ? - safeExecute(settings.labelFunction, [this.data, this.dataSources, this.data.dsIndex]) : settings.label; + safeExecute(settings.parsedLabelFunction, [this.data, this.dataSources, this.data.dsIndex]) : settings.label; this.map.markerLabelText = parseWithTranslation.prepareProcessPattern(pattern, true); this.map.replaceInfoLabelMarker = processDataPattern(this.map.markerLabelText, this.data); } - settings.labelText = fillDataPattern(this.map.markerLabelText, this.map.replaceInfoLabelMarker, this.data); - this.leafletMarker.bindTooltip(`
${settings.labelText}
`, + const labelText = fillDataPattern(this.map.markerLabelText, this.map.replaceInfoLabelMarker, this.data); + const labelColor = this.map.ctx.widgetConfig.color; + this.leafletMarker.bindTooltip(`
${labelText}
`, { className: 'tb-marker-label', permanent: true, direction: 'top', offset: this.labelOffset }); } } @@ -138,7 +139,7 @@ export class Marker { }); } - updateMarkerIcon(settings: MarkerSettings) { + updateMarkerIcon(settings: Partial) { this.createMarkerIcon((iconInfo) => { this.leafletMarker.setIcon(iconInfo.icon); const anchor = iconInfo.icon.options.iconAnchor; @@ -157,11 +158,11 @@ export class Marker { return; } const currentImage: MarkerImageInfo = this.settings.useMarkerImageFunction ? - safeExecute(this.settings.markerImageFunction, + safeExecute(this.settings.parsedMarkerImageFunction, [this.data, this.settings.markerImages, this.dataSources, this.data.dsIndex]) : this.settings.currentImage; let currentColor = this.settings.tinyColor; if (this.settings.useColorFunction) { - const functionColor = safeExecute(this.settings.colorFunction, + const functionColor = safeExecute(this.settings.parsedColorFunction, [this.data, this.dataSources, this.data.dsIndex]); if (isDefinedAndNotNull(functionColor)) { currentColor = tinycolor(functionColor); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts index 108e4871c4..45269da5c7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts @@ -20,9 +20,10 @@ import { functionValueCalculator, parseWithTranslation } from './common-maps-utils'; -import { PolygonSettings, UnitedMapSettings } from './map-models'; +import { WidgetPolygonSettings } from './map-models'; import { FormattedData } from '@shared/models/widget.models'; import { fillDataPattern, processDataPattern, safeExecute } from '@core/utils'; +import LeafletMap from '@home/components/widget/lib/maps/leaflet-map'; export class Polygon { @@ -30,13 +31,13 @@ export class Polygon { leafletPoly: L.Polygon; tooltip: L.Popup; - data: FormattedData; - dataSources: FormattedData[]; - constructor(public map, data: FormattedData, dataSources: FormattedData[], private settings: UnitedMapSettings, - private onDragendListener?) { - this.dataSources = dataSources; - this.data = data; + constructor(private map: LeafletMap, + private data: FormattedData, + private dataSources: FormattedData[], + private settings: Partial, + private onDragendListener?, + snappable = false) { const polygonColor = this.getPolygonColor(settings); const polygonStrokeColor = this.getPolygonStrokeColor(settings); const polyData = data[this.settings.polygonKeyName]; @@ -49,8 +50,8 @@ export class Polygon { fillOpacity: settings.polygonOpacity, opacity: settings.polygonStrokeOpacity, pmIgnore: !settings.editablePolygon, - snapIgnore: !settings.snappable - }).addTo(this.map); + snapIgnore: !snappable + }).addTo(this.map.map); if (settings.showPolygonLabel) { this.updateLabel(settings); @@ -91,28 +92,30 @@ export class Polygon { updateTooltip(data: FormattedData) { const pattern = this.settings.usePolygonTooltipFunction ? - safeExecute(this.settings.polygonTooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : + safeExecute(this.settings.parsedPolygonTooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : this.settings.polygonTooltipPattern; this.tooltip.setContent(parseWithTranslation.parseTemplate(pattern, data, true)); } - updateLabel(settings: PolygonSettings) { + updateLabel(settings: Partial) { this.leafletPoly.unbindTooltip(); if (settings.showPolygonLabel) { if (!this.map.polygonLabelText || settings.usePolygonLabelFunction) { const pattern = settings.usePolygonLabelFunction ? - safeExecute(settings.polygonLabelFunction, [this.data, this.dataSources, this.data.dsIndex]) : settings.polygonLabel; + safeExecute(settings.parsedPolygonLabelFunction, + [this.data, this.dataSources, this.data.dsIndex]) : settings.polygonLabel; this.map.polygonLabelText = parseWithTranslation.prepareProcessPattern(pattern, true); this.map.replaceInfoLabelPolygon = processDataPattern(this.map.polygonLabelText, this.data); } const polygonLabelText = fillDataPattern(this.map.polygonLabelText, this.map.replaceInfoLabelPolygon, this.data); - this.leafletPoly.bindTooltip(`
${polygonLabelText}
`, + const labelColor = this.map.ctx.widgetConfig.color; + this.leafletPoly.bindTooltip(`
${polygonLabelText}
`, { className: 'tb-polygon-label', permanent: true, sticky: true, direction: 'center' }) .openTooltip(this.leafletPoly.getBounds().getCenter()); } } - updatePolygon(data: FormattedData, dataSources: FormattedData[], settings: PolygonSettings) { + updatePolygon(data: FormattedData, dataSources: FormattedData[], settings: Partial) { if (this.editing) { return; } @@ -121,7 +124,7 @@ export class Polygon { const polyData = data[this.settings.polygonKeyName]; if (isCutPolygon(polyData) || polyData.length !== 2) { if (this.leafletPoly instanceof L.Rectangle) { - this.map.removeLayer(this.leafletPoly); + this.map.map.removeLayer(this.leafletPoly); const polygonColor = this.getPolygonColor(settings); const polygonStrokeColor = this.getPolygonStrokeColor(settings); this.leafletPoly = L.polygon(polyData, { @@ -132,7 +135,7 @@ export class Polygon { fillOpacity: settings.polygonOpacity, opacity: settings.polygonStrokeOpacity, pmIgnore: !settings.editablePolygon - }).addTo(this.map); + }).addTo(this.map.map); if (settings.showPolygonTooltip) { this.tooltip = createTooltip(this.leafletPoly, settings, data.$datasource, settings.autoClosePolygonTooltip, settings.showPolygonTooltipAction); @@ -156,10 +159,10 @@ export class Polygon { } removePolygon() { - this.map.removeLayer(this.leafletPoly); + this.map.map.removeLayer(this.leafletPoly); } - updatePolygonColor(settings: PolygonSettings) { + updatePolygonColor(settings: Partial) { const polygonColor = this.getPolygonColor(settings); const polygonStrokeColor = this.getPolygonStrokeColor(settings); const style: L.PathOptions = { @@ -178,13 +181,13 @@ export class Polygon { this.leafletPoly.redraw(); } - private getPolygonColor(settings: PolygonSettings): string | null { - return functionValueCalculator(settings.usePolygonColorFunction, settings.polygonColorFunction, + private getPolygonColor(settings: Partial): string | null { + return functionValueCalculator(settings.usePolygonColorFunction, settings.parsedPolygonColorFunction, [this.data, this.dataSources, this.data.dsIndex], settings.polygonColor); } - private getPolygonStrokeColor(settings: PolygonSettings): string | null { - return functionValueCalculator(settings.usePolygonStrokeColorFunction, settings.polygonStrokeColorFunction, + private getPolygonStrokeColor(settings: Partial): string | null { + return functionValueCalculator(settings.usePolygonStrokeColorFunction, settings.parsedPolygonStrokeColorFunction, [this.data, this.dataSources, this.data.dsIndex], settings.polygonStrokeColor); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polyline.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polyline.ts index 8d3496a037..799e1a5002 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polyline.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polyline.ts @@ -18,7 +18,7 @@ import L, { PolylineDecorator, PolylineDecoratorOptions, Symbol } from 'leaflet'; import 'leaflet-polylinedecorator'; -import { PolylineSettings } from './map-models'; +import { WidgetPolylineSettings } from './map-models'; import { functionValueCalculator } from '@home/components/widget/lib/maps/common-maps-utils'; import { FormattedData } from '@shared/models/widget.models'; @@ -26,12 +26,12 @@ export class Polyline { leafletPoly: L.Polyline; polylineDecorator: PolylineDecorator; - dataSources: FormattedData[]; - data: FormattedData; - constructor(private map: L.Map, locations: L.LatLng[], data: FormattedData, dataSources: FormattedData[], settings: PolylineSettings) { - this.dataSources = dataSources; - this.data = data; + constructor(private map: L.Map, + locations: L.LatLng[], + private data: FormattedData, + private dataSources: FormattedData[], + settings: Partial) { this.leafletPoly = L.polyline(locations, this.getPolyStyle(settings) @@ -42,7 +42,7 @@ export class Polyline { } } - getDecoratorSettings(settings: PolylineSettings): PolylineDecoratorOptions { + getDecoratorSettings(settings: Partial): PolylineDecoratorOptions { return { patterns: [ { @@ -62,7 +62,7 @@ export class Polyline { }; } - updatePolyline(locations: L.LatLng[], data: FormattedData, dataSources: FormattedData[], settings: PolylineSettings) { + updatePolyline(locations: L.LatLng[], data: FormattedData, dataSources: FormattedData[], settings: Partial) { this.data = data; this.dataSources = dataSources; this.leafletPoly.setLatLngs(locations); @@ -72,14 +72,14 @@ export class Polyline { } } - getPolyStyle(settings: PolylineSettings): L.PolylineOptions { + getPolyStyle(settings: Partial): L.PolylineOptions { return { interactive: false, - color: functionValueCalculator(settings.useColorFunction, settings.colorFunction, + color: functionValueCalculator(settings.useColorFunction, settings.parsedColorFunction, [this.data, this.dataSources, this.data.dsIndex], settings.color), - opacity: functionValueCalculator(settings.useStrokeOpacityFunction, settings.strokeOpacityFunction, + opacity: functionValueCalculator(settings.useStrokeOpacityFunction, settings.parsedStrokeOpacityFunction, [this.data, this.dataSources, this.data.dsIndex], settings.strokeOpacity), - weight: functionValueCalculator(settings.useStrokeWeightFunction, settings.strokeWeightFunction, + weight: functionValueCalculator(settings.useStrokeWeightFunction, settings.parsedStrokeWeightFunction, [this.data, this.dataSources, this.data.dsIndex], settings.strokeWeight), pmIgnore: true }; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/google-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/google-map.ts index 9ac73d2749..90ce4e78d8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/google-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/google-map.ts @@ -17,7 +17,7 @@ import L from 'leaflet'; import LeafletMap from '../leaflet-map'; -import { DEFAULT_ZOOM_LEVEL, UnitedMapSettings } from '../map-models'; +import { DEFAULT_ZOOM_LEVEL, WidgetUnitedMapSettings } from '../map-models'; import 'leaflet.gridlayer.googlemutant'; import { ResourcesService } from '@core/services/resources.service'; import { WidgetContext } from '@home/models/widget-component.models'; @@ -31,16 +31,15 @@ interface GmGlobal { export class GoogleMap extends LeafletMap { private resource: ResourcesService; - constructor(ctx: WidgetContext, $container, options: UnitedMapSettings) { + constructor(ctx: WidgetContext, $container, options: WidgetUnitedMapSettings) { super(ctx, $container, options); this.resource = ctx.$injector.get(ResourcesService); - super.initSettings(options); this.loadGoogle(() => { const map = L.map($container, { attributionControl: false, zoomControl: !this.options.disableZoomControl, tap: L.Browser.safari && L.Browser.mobile - }).setView(options?.defaultCenterPosition, options?.defaultZoomLevel || DEFAULT_ZOOM_LEVEL); + }).setView(options?.parsedDefaultCenterPosition, options?.defaultZoomLevel || DEFAULT_ZOOM_LEVEL); (L.gridLayer as any).googleMutant({ type: options?.gmDefaultMapType || 'roadmap' }).addTo(map); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/here-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/here-map.ts index bc261b5c63..584c4ea255 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/here-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/here-map.ts @@ -16,19 +16,18 @@ import L from 'leaflet'; import LeafletMap from '../leaflet-map'; -import { DEFAULT_ZOOM_LEVEL, UnitedMapSettings } from '../map-models'; +import { DEFAULT_ZOOM_LEVEL, WidgetUnitedMapSettings } from '../map-models'; import { WidgetContext } from '@home/models/widget-component.models'; export class HEREMap extends LeafletMap { - constructor(ctx: WidgetContext, $container, options: UnitedMapSettings) { + constructor(ctx: WidgetContext, $container, options: WidgetUnitedMapSettings) { super(ctx, $container, options); const map = L.map($container, { tap: L.Browser.safari && L.Browser.mobile, zoomControl: !this.options.disableZoomControl - }).setView(options?.defaultCenterPosition, options?.defaultZoomLevel || DEFAULT_ZOOM_LEVEL); + }).setView(options?.parsedDefaultCenterPosition, options?.defaultZoomLevel || DEFAULT_ZOOM_LEVEL); const tileLayer = (L.tileLayer as any).provider(options.mapProviderHere || 'HERE.normalDay', options.credentials); tileLayer.addTo(map); - super.initSettings(options); super.setMap(map); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts index d0678dea8b..411258d3af 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts @@ -16,7 +16,7 @@ import L, { LatLngBounds, LatLngLiteral, LatLngTuple } from 'leaflet'; import LeafletMap from '../leaflet-map'; -import { CircleData, MapImage, PosFuncton, UnitedMapSettings } from '../map-models'; +import { CircleData, MapImage, PosFuncton, WidgetUnitedMapSettings } from '../map-models'; import { Observable, ReplaySubject } from 'rxjs'; import { map, mergeMap } from 'rxjs/operators'; import { @@ -41,7 +41,7 @@ export class ImageMap extends LeafletMap { imageUrl: string; posFunction: PosFuncton; - constructor(ctx: WidgetContext, $container: HTMLElement, options: UnitedMapSettings) { + constructor(ctx: WidgetContext, $container: HTMLElement, options: WidgetUnitedMapSettings) { super(ctx, $container, options); this.posFunction = parseFunction(options.posFunction, ['origXPos', 'origYPos']) as PosFuncton; this.mapImage(options).subscribe((mapImage) => { @@ -51,21 +51,20 @@ export class ImageMap extends LeafletMap { this.onResize(true); } else { this.onResize(); - super.initSettings(options); super.setMap(this.map); } }); } - private mapImage(options: UnitedMapSettings): Observable { + private mapImage(options: WidgetUnitedMapSettings): Observable { const imageEntityAlias = options.imageEntityAlias; const imageUrlAttribute = options.imageUrlAttribute; if (!imageEntityAlias || !imageUrlAttribute) { - return this.imageFromUrl(options.mapUrl); + return this.imageFromUrl(options.mapImageUrl); } const entityAliasId = this.ctx.aliasController.getEntityAliasId(imageEntityAlias); if (!entityAliasId) { - return this.imageFromUrl(options.mapUrl); + return this.imageFromUrl(options.mapImageUrl); } const datasources = [ { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/openstreet-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/openstreet-map.ts index 1a27f26931..96988e40c5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/openstreet-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/openstreet-map.ts @@ -16,16 +16,16 @@ import L from 'leaflet'; import LeafletMap from '../leaflet-map'; -import { DEFAULT_ZOOM_LEVEL, UnitedMapSettings } from '../map-models'; +import { DEFAULT_ZOOM_LEVEL, WidgetUnitedMapSettings } from '../map-models'; import { WidgetContext } from '@home/models/widget-component.models'; export class OpenStreetMap extends LeafletMap { - constructor(ctx: WidgetContext, $container, options: UnitedMapSettings) { + constructor(ctx: WidgetContext, $container, options: WidgetUnitedMapSettings) { super(ctx, $container, options); const map = L.map($container, { zoomControl: !this.options.disableZoomControl, tap: L.Browser.safari && L.Browser.mobile - }).setView(options?.defaultCenterPosition, options?.defaultZoomLevel || DEFAULT_ZOOM_LEVEL); + }).setView(options?.parsedDefaultCenterPosition, options?.defaultZoomLevel || DEFAULT_ZOOM_LEVEL); let tileLayer; if (options.useCustomProvider) { tileLayer = L.tileLayer(options.customProviderTileUrl); @@ -33,7 +33,6 @@ export class OpenStreetMap extends LeafletMap { tileLayer = (L.tileLayer as any).provider(options.mapProvider || 'OpenStreetMap.Mapnik'); } tileLayer.addTo(map); - super.initSettings(options); super.setMap(map); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/tencent-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/tencent-map.ts index fb53f15b3e..9cf1801056 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/tencent-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/tencent-map.ts @@ -17,24 +17,23 @@ import L from 'leaflet'; import LeafletMap from '../leaflet-map'; -import { DEFAULT_ZOOM_LEVEL, UnitedMapSettings } from '../map-models'; +import { DEFAULT_ZOOM_LEVEL, WidgetUnitedMapSettings } from '../map-models'; import { WidgetContext } from '@home/models/widget-component.models'; export class TencentMap extends LeafletMap { - constructor(ctx: WidgetContext, $container, options: UnitedMapSettings) { + constructor(ctx: WidgetContext, $container, options: WidgetUnitedMapSettings) { super(ctx, $container, options); const txUrl = 'http://rt{s}.map.gtimg.com/realtimerender?z={z}&x={x}&y={y}&type=vector&style=0'; const map = L.map($container, { zoomControl: !this.options.disableZoomControl, tap: L.Browser.safari && L.Browser.mobile - }).setView(options?.defaultCenterPosition, options?.defaultZoomLevel || DEFAULT_ZOOM_LEVEL); + }).setView(options?.parsedDefaultCenterPosition, options?.defaultZoomLevel || DEFAULT_ZOOM_LEVEL); const txLayer = L.tileLayer(txUrl, { subdomains: '0123', tms: true, attribution: '©2021 Tencent - GS(2020)2236号- Data© NavInfo' }).addTo(map); txLayer.addTo(map); - super.initSettings(options); super.setMap(map); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/circle-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/circle-settings.component.html new file mode 100644 index 0000000000..3f480cc347 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/circle-settings.component.html @@ -0,0 +1,199 @@ + +
+
+ widgets.maps.circle-settings + + + + + {{ 'widgets.maps.show-circle' | translate }} + + + + widget-config.advanced-settings + + + + + + + {{ 'widgets.maps.enable-circle-edit' | translate }} + +
+ widgets.maps.circle-label + + + + + {{ 'widgets.maps.show-circle-label' | translate }} + + + + widget-config.advanced-settings + + + + + {{ 'widgets.maps.use-circle-label-function' | translate }} + + + + + + + +
+
+ widgets.maps.circle-tooltip + + + + + {{ 'widgets.maps.show-circle-tooltip' | translate }} + + + + widget-config.advanced-settings + + + + + widgets.maps.show-tooltip-action + + + {{showTooltipActionTranslations.get(action) | translate}} + + + + + {{ 'widgets.maps.auto-close-circle-tooltips' | translate }} + + + {{ 'widgets.maps.use-circle-tooltip-function' | translate }} + + + + + + + +
+
+ widgets.maps.circle-fill-color +
+ + + + widgets.maps.circle-fill-color-opacity + + +
+ + + + + {{ 'widgets.maps.use-circle-fill-color-function' | translate }} + + + + widget-config.advanced-settings + + + + + + + +
+
+ widgets.maps.circle-stroke +
+ + + + widgets.maps.stroke-opacity + + + + widgets.maps.stroke-weight + + +
+ + + + + {{ 'widgets.maps.use-circle-stroke-color-function' | translate }} + + + + widget-config.advanced-settings + + + + + + + +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/circle-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/circle-settings.component.ts new file mode 100644 index 0000000000..10ee46ce12 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/circle-settings.component.ts @@ -0,0 +1,243 @@ +/// +/// Copyright © 2016-2022 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 { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { + CircleSettings, + ShowTooltipAction, + showTooltipActionTranslationMap +} from '@home/components/widget/lib/maps/map-models'; +import { WidgetService } from '@core/http/widget.service'; +import { Widget } from '@shared/models/widget.models'; + +@Component({ + selector: 'tb-circle-settings', + templateUrl: './circle-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CircleSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CircleSettingsComponent), + multi: true + } + ] +}) +export class CircleSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { + + @Input() + disabled: boolean; + + @Input() + widget: Widget; + + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + + private modelValue: CircleSettings; + + private propagateChange = null; + + public circleSettingsFormGroup: FormGroup; + + showTooltipActions = Object.values(ShowTooltipAction); + + showTooltipActionTranslations = showTooltipActionTranslationMap; + + constructor(protected store: Store, + private translate: TranslateService, + private widgetService: WidgetService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.circleSettingsFormGroup = this.fb.group({ + showCircle: [null, []], + circleKeyName: [null, [Validators.required]], + editableCircle: [null, []], + showCircleLabel: [null, []], + useCircleLabelFunction: [null, []], + circleLabel: [null, []], + circleLabelFunction: [null, []], + showCircleTooltip: [null, []], + showCircleTooltipAction: [null, []], + autoCloseCircleTooltip: [null, []], + useCircleTooltipFunction: [null, []], + circleTooltipPattern: [null, []], + circleTooltipFunction: [null, []], + circleFillColor: [null, []], + circleFillColorOpacity: [null, [Validators.min(0), Validators.max(1)]], + useCircleFillColorFunction: [null, []], + circleFillColorFunction: [null, []], + circleStrokeColor: [null, []], + circleStrokeOpacity: [null, [Validators.min(0), Validators.max(1)]], + circleStrokeWeight: [null, [Validators.min(0)]], + useCircleStrokeColorFunction: [null, []], + circleStrokeColorFunction: [null, []] + }); + this.circleSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + this.circleSettingsFormGroup.get('showCircle').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.circleSettingsFormGroup.get('showCircleLabel').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.circleSettingsFormGroup.get('useCircleLabelFunction').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.circleSettingsFormGroup.get('showCircleTooltip').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.circleSettingsFormGroup.get('useCircleTooltipFunction').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.circleSettingsFormGroup.get('useCircleFillColorFunction').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.circleSettingsFormGroup.get('useCircleStrokeColorFunction').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.updateValidators(false); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.circleSettingsFormGroup.disable({emitEvent: false}); + } else { + this.circleSettingsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: CircleSettings): void { + this.modelValue = value; + this.circleSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + this.updateValidators(false); + } + + public validate(c: FormControl) { + return this.circleSettingsFormGroup.valid ? null : { + circleSettings: { + valid: false, + }, + }; + } + + private updateModel() { + const value: CircleSettings = this.circleSettingsFormGroup.value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } + + private updateValidators(emitEvent?: boolean): void { + const showCircle: boolean = this.circleSettingsFormGroup.get('showCircle').value; + const showCircleLabel: boolean = this.circleSettingsFormGroup.get('showCircleLabel').value; + const useCircleLabelFunction: boolean = this.circleSettingsFormGroup.get('useCircleLabelFunction').value; + const showCircleTooltip: boolean = this.circleSettingsFormGroup.get('showCircleTooltip').value; + const useCircleTooltipFunction: boolean = this.circleSettingsFormGroup.get('useCircleTooltipFunction').value; + const useCircleFillColorFunction: boolean = this.circleSettingsFormGroup.get('useCircleFillColorFunction').value; + const useCircleStrokeColorFunction: boolean = this.circleSettingsFormGroup.get('useCircleStrokeColorFunction').value; + + this.circleSettingsFormGroup.disable({emitEvent: false}); + this.circleSettingsFormGroup.get('showCircle').enable({emitEvent: false}); + + if (showCircle) { + this.circleSettingsFormGroup.get('circleKeyName').enable({emitEvent: false}); + this.circleSettingsFormGroup.get('editableCircle').enable({emitEvent: false}); + this.circleSettingsFormGroup.get('showCircleLabel').enable({emitEvent: false}); + this.circleSettingsFormGroup.get('showCircleTooltip').enable({emitEvent: false}); + this.circleSettingsFormGroup.get('circleFillColor').enable({emitEvent: false}); + this.circleSettingsFormGroup.get('circleFillColorOpacity').enable({emitEvent: false}); + this.circleSettingsFormGroup.get('useCircleFillColorFunction').enable({emitEvent: false}); + this.circleSettingsFormGroup.get('circleStrokeColor').enable({emitEvent: false}); + this.circleSettingsFormGroup.get('circleStrokeOpacity').enable({emitEvent: false}); + this.circleSettingsFormGroup.get('circleStrokeWeight').enable({emitEvent: false}); + this.circleSettingsFormGroup.get('useCircleStrokeColorFunction').enable({emitEvent: false}); + if (showCircleLabel) { + this.circleSettingsFormGroup.get('useCircleLabelFunction').enable({emitEvent: false}); + if (useCircleLabelFunction) { + this.circleSettingsFormGroup.get('circleLabelFunction').enable({emitEvent}); + this.circleSettingsFormGroup.get('circleLabel').disable({emitEvent}); + } else { + this.circleSettingsFormGroup.get('circleLabelFunction').disable({emitEvent}); + this.circleSettingsFormGroup.get('circleLabel').enable({emitEvent}); + } + } else { + this.circleSettingsFormGroup.get('useCircleLabelFunction').disable({emitEvent: false}); + this.circleSettingsFormGroup.get('circleLabelFunction').disable({emitEvent}); + this.circleSettingsFormGroup.get('circleLabel').disable({emitEvent}); + } + if (showCircleTooltip) { + this.circleSettingsFormGroup.get('showCircleTooltipAction').enable({emitEvent}); + this.circleSettingsFormGroup.get('autoCloseCircleTooltip').enable({emitEvent}); + this.circleSettingsFormGroup.get('useCircleTooltipFunction').enable({emitEvent: false}); + if (useCircleTooltipFunction) { + this.circleSettingsFormGroup.get('circleTooltipFunction').enable({emitEvent}); + this.circleSettingsFormGroup.get('circleTooltipPattern').disable({emitEvent}); + } else { + this.circleSettingsFormGroup.get('circleTooltipFunction').disable({emitEvent}); + this.circleSettingsFormGroup.get('circleTooltipPattern').enable({emitEvent}); + } + } else { + this.circleSettingsFormGroup.get('showCircleTooltipAction').disable({emitEvent}); + this.circleSettingsFormGroup.get('autoCloseCircleTooltip').disable({emitEvent}); + this.circleSettingsFormGroup.get('useCircleTooltipFunction').disable({emitEvent: false}); + this.circleSettingsFormGroup.get('circleTooltipFunction').disable({emitEvent}); + this.circleSettingsFormGroup.get('circleTooltipPattern').disable({emitEvent}); + } + if (useCircleFillColorFunction) { + this.circleSettingsFormGroup.get('circleFillColorFunction').enable({emitEvent}); + } else { + this.circleSettingsFormGroup.get('circleFillColorFunction').disable({emitEvent}); + } + if (useCircleStrokeColorFunction) { + this.circleSettingsFormGroup.get('circleStrokeColorFunction').enable({emitEvent}); + } else { + this.circleSettingsFormGroup.get('circleStrokeColorFunction').disable({emitEvent}); + } + } + this.circleSettingsFormGroup.updateValueAndValidity({emitEvent: false}); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/common-map-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/common-map-settings.component.html new file mode 100644 index 0000000000..8f199654e0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/common-map-settings.component.html @@ -0,0 +1,93 @@ + +
+
+ widgets.maps.common-map-settings +
+ + + + + + + + +
+ + + + widget-config.advanced-settings + + + +
+ + widgets.maps.default-map-zoom-level + + + + widgets.maps.default-map-center-position + + +
+
+
+ + {{ 'widgets.maps.disable-scroll-zooming' | translate }} + + + {{ 'widgets.maps.disable-zoom-control-buttons' | translate }} + + + {{ 'widgets.maps.fit-map-bounds' | translate }} + +
+
+ + {{ 'widgets.maps.use-default-map-center-position' | translate }} + +
+
+ + widgets.maps.entities-limit + + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/common-map-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/common-map-settings.component.ts new file mode 100644 index 0000000000..12a2e8cef7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/common-map-settings.component.ts @@ -0,0 +1,178 @@ +/// +/// Copyright © 2016-2022 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 { Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { CommonMapSettings, MapProviders } from '@home/components/widget/lib/maps/map-models'; +import { Widget } from '@shared/models/widget.models'; + +@Component({ + selector: 'tb-common-map-settings', + templateUrl: './common-map-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CommonMapSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CommonMapSettingsComponent), + multi: true + } + ] +}) +export class CommonMapSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator, OnChanges { + + @Input() + disabled: boolean; + + @Input() + provider: MapProviders; + + @Input() + widget: Widget; + + mapProvider = MapProviders; + + private modelValue: CommonMapSettings; + + private propagateChange = null; + + public commonMapSettingsFormGroup: FormGroup; + + constructor(protected store: Store, + private translate: TranslateService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.commonMapSettingsFormGroup = this.fb.group({ + latKeyName: [null, [Validators.required]], + lngKeyName: [null, [Validators.required]], + xPosKeyName: [null, [Validators.required]], + yPosKeyName: [null, [Validators.required]], + defaultZoomLevel: [null, [Validators.min(0), Validators.max(20)]], + defaultCenterPosition: [null, []], + disableScrollZooming: [null, []], + disableZoomControl: [null, []], + fitMapBounds: [null, []], + useDefaultCenterPosition: [null, []], + mapPageSize: [null, [Validators.min(1), Validators.required]] + }); + this.commonMapSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + this.updateValidators(false); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'provider') { + this.updateValidators(false); + } + } + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.commonMapSettingsFormGroup.disable({emitEvent: false}); + } else { + this.commonMapSettingsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: CommonMapSettings): void { + this.modelValue = value; + this.commonMapSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + this.updateValidators(false); + } + + public validate(c: FormControl) { + return this.commonMapSettingsFormGroup.valid ? null : { + commonMapSettings: { + valid: false, + }, + }; + } + + private updateModel() { + const value: CommonMapSettings = this.commonMapSettingsFormGroup.value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } + + private updateValidators(emitEvent?: boolean): void { + if (this.provider === MapProviders.image) { + this.commonMapSettingsFormGroup.get('latKeyName').disable({emitEvent}); + this.commonMapSettingsFormGroup.get('lngKeyName').disable({emitEvent}); + this.commonMapSettingsFormGroup.get('defaultZoomLevel').disable({emitEvent}); + this.commonMapSettingsFormGroup.get('useDefaultCenterPosition').disable({emitEvent}); + this.commonMapSettingsFormGroup.get('defaultCenterPosition').disable({emitEvent}); + this.commonMapSettingsFormGroup.get('fitMapBounds').disable({emitEvent}); + + this.commonMapSettingsFormGroup.get('xPosKeyName').enable({emitEvent}); + this.commonMapSettingsFormGroup.get('yPosKeyName').enable({emitEvent}); + } else { + this.commonMapSettingsFormGroup.get('latKeyName').enable({emitEvent}); + this.commonMapSettingsFormGroup.get('lngKeyName').enable({emitEvent}); + this.commonMapSettingsFormGroup.get('defaultZoomLevel').enable({emitEvent}); + this.commonMapSettingsFormGroup.get('useDefaultCenterPosition').enable({emitEvent}); + this.commonMapSettingsFormGroup.get('defaultCenterPosition').enable({emitEvent}); + this.commonMapSettingsFormGroup.get('fitMapBounds').enable({emitEvent}); + + + this.commonMapSettingsFormGroup.get('xPosKeyName').disable({emitEvent}); + this.commonMapSettingsFormGroup.get('yPosKeyName').disable({emitEvent}); + } + this.commonMapSettingsFormGroup.get('latKeyName').updateValueAndValidity({emitEvent: false}); + this.commonMapSettingsFormGroup.get('lngKeyName').updateValueAndValidity({emitEvent: false}); + this.commonMapSettingsFormGroup.get('xPosKeyName').updateValueAndValidity({emitEvent: false}); + this.commonMapSettingsFormGroup.get('yPosKeyName').updateValueAndValidity({emitEvent: false}); + this.commonMapSettingsFormGroup.get('defaultZoomLevel').updateValueAndValidity({emitEvent: false}); + this.commonMapSettingsFormGroup.get('useDefaultCenterPosition').updateValueAndValidity({emitEvent: false}); + this.commonMapSettingsFormGroup.get('defaultCenterPosition').updateValueAndValidity({emitEvent: false}); + this.commonMapSettingsFormGroup.get('fitMapBounds').updateValueAndValidity({emitEvent: false}); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/datasources-key-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/datasources-key-autocomplete.component.html new file mode 100644 index 0000000000..4f7623f2f7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/datasources-key-autocomplete.component.html @@ -0,0 +1,39 @@ + + + {{ label | translate }} + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/datasources-key-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/datasources-key-autocomplete.component.ts new file mode 100644 index 0000000000..5c31d495d2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/datasources-key-autocomplete.component.ts @@ -0,0 +1,152 @@ +/// +/// Copyright © 2016-2022 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 { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { Observable, of } from 'rxjs'; +import { map, mergeMap } from 'rxjs/operators'; +import { Datasource } from '@shared/models/widget.models'; +import { EntityService } from '@core/http/entity.service'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; + +@Component({ + selector: 'tb-datasources-key-autocomplete', + templateUrl: './datasources-key-autocomplete.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DatasourcesKeyAutocompleteComponent), + multi: true + } + ] +}) +export class DatasourcesKeyAutocompleteComponent extends PageComponent implements OnInit, ControlValueAccessor { + + @ViewChild('keyInput') keyInput: ElementRef; + + @Input() + disabled: boolean; + + @Input() + datasources: Array; + + @Input() + label = 'entity.key-name'; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + private modelValue: string; + + private propagateChange = null; + + public keyFormGroup: FormGroup; + + filteredKeys: Observable>; + keySearchText = ''; + + constructor(protected store: Store, + private translate: TranslateService, + private entityService: EntityService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.keyFormGroup = this.fb.group({ + key: [null, this.required ? [Validators.required] : []] + }); + this.keyFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + + this.filteredKeys = this.keyFormGroup.get('key').valueChanges + .pipe( + map(value => value ? value : ''), + mergeMap(name => this.fetchKeys(name) ) + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.keyFormGroup.disable({emitEvent: false}); + } else { + this.keyFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: string): void { + this.modelValue = value; + this.keyFormGroup.patchValue( + {key: value}, {emitEvent: false} + ); + } + + clearKey() { + this.keyFormGroup.get('key').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.keyInput.nativeElement.blur(); + this.keyInput.nativeElement.focus(); + }, 0); + } + + onFocus() { + this.keyFormGroup.get('key').updateValueAndValidity({onlySelf: true, emitEvent: true}); + } + + private updateModel() { + this.modelValue = this.keyFormGroup.get('key').value; + this.propagateChange(this.modelValue); + } + + private fetchKeys(searchText?: string): Observable> { + this.keySearchText = searchText; + const dataKeyFilter = this.createKeyFilter(this.keySearchText); + return of(this.allKeys()).pipe( + map(name => name.filter(dataKeyFilter)) + ); + } + + private createKeyFilter(query: string): (key: string) => boolean { + const lowercaseQuery = query.toLowerCase(); + return key => key.toLowerCase().startsWith(lowercaseQuery); + } + + private allKeys(): string[] { + const allDataKeys = (this.datasources || []).map(ds => ds.dataKeys) + .reduce((accumulator, value) => accumulator.concat(value), []).map(dataKey => dataKey.label); + return [...new Set(allDataKeys)].sort((a, b) => a.localeCompare(b)); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/google-map-provider-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/google-map-provider-settings.component.html new file mode 100644 index 0000000000..7d718d4dcd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/google-map-provider-settings.component.html @@ -0,0 +1,31 @@ + +
+ + widgets.maps.google-maps-api-key + + + + widgets.maps.default-map-type + + + {{googleMapTypeTranslations.get(type) | translate}} + + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/google-map-provider-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/google-map-provider-settings.component.ts new file mode 100644 index 0000000000..d48c10bbd9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/google-map-provider-settings.component.ts @@ -0,0 +1,122 @@ +/// +/// Copyright © 2016-2022 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 { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { + GoogleMapProviderSettings, + GoogleMapType, + googleMapTypeProviderTranslationMap +} from '@home/components/widget/lib/maps/map-models'; + +@Component({ + selector: 'tb-google-map-provider-settings', + templateUrl: './google-map-provider-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => GoogleMapProviderSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => GoogleMapProviderSettingsComponent), + multi: true + } + ] +}) +export class GoogleMapProviderSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { + + @Input() + disabled: boolean; + + private modelValue: GoogleMapProviderSettings; + + private propagateChange = null; + + public providerSettingsFormGroup: FormGroup; + + googleMapTypes = Object.values(GoogleMapType); + + googleMapTypeTranslations = googleMapTypeProviderTranslationMap; + + constructor(protected store: Store, + private translate: TranslateService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.providerSettingsFormGroup = this.fb.group({ + gmApiKey: [null, [Validators.required]], + gmDefaultMapType: [null, [Validators.required]] + }); + this.providerSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.providerSettingsFormGroup.disable({emitEvent: false}); + } else { + this.providerSettingsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: GoogleMapProviderSettings): void { + this.modelValue = value; + this.providerSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + } + + public validate(c: FormControl) { + return this.providerSettingsFormGroup.valid ? null : { + googleMapProviderSettings: { + valid: false, + }, + }; + } + + private updateModel() { + const value: GoogleMapProviderSettings = this.providerSettingsFormGroup.value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/here-map-provider-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/here-map-provider-settings.component.html new file mode 100644 index 0000000000..530a3b615e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/here-map-provider-settings.component.html @@ -0,0 +1,40 @@ + +
+ + widgets.maps.map-layer + + + {{hereMapProviderTranslations.get(provider) | translate}} + + + +
+
+ widgets.maps.credentials + + widgets.maps.here-app-id + + + + widgets.maps.here-app-code + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/here-map-provider-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/here-map-provider-settings.component.ts new file mode 100644 index 0000000000..6bf6c32d10 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/here-map-provider-settings.component.ts @@ -0,0 +1,125 @@ +/// +/// Copyright © 2016-2022 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 { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { + HereMapProvider, + HereMapProviderSettings, + hereMapProviderTranslationMap +} from '@home/components/widget/lib/maps/map-models'; + +@Component({ + selector: 'tb-here-map-provider-settings', + templateUrl: './here-map-provider-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => HereMapProviderSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => HereMapProviderSettingsComponent), + multi: true + } + ] +}) +export class HereMapProviderSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { + + @Input() + disabled: boolean; + + private modelValue: HereMapProviderSettings; + + private propagateChange = null; + + public providerSettingsFormGroup: FormGroup; + + hereMapProviders = Object.values(HereMapProvider); + + hereMapProviderTranslations = hereMapProviderTranslationMap; + + constructor(protected store: Store, + private translate: TranslateService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.providerSettingsFormGroup = this.fb.group({ + mapProviderHere: [null, [Validators.required]], + credentials: this.fb.group({ + app_id: [null, [Validators.required]], + app_code: [null, [Validators.required]] + }) + }); + this.providerSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.providerSettingsFormGroup.disable({emitEvent: false}); + } else { + this.providerSettingsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: HereMapProviderSettings): void { + this.modelValue = value; + this.providerSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + } + + public validate(c: FormControl) { + return this.providerSettingsFormGroup.valid ? null : { + hereMapProviderSettings: { + valid: false, + }, + }; + } + + private updateModel() { + const value: HereMapProviderSettings = this.providerSettingsFormGroup.value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/image-map-provider-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/image-map-provider-settings.component.html new file mode 100644 index 0000000000..a8c5318142 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/image-map-provider-settings.component.html @@ -0,0 +1,71 @@ + +
+ + +
+ widgets.maps.image-map-background-from-entity-attribute +
+ + widgets.maps.image-url-source-entity-alias + + + + + + + + + + widgets.maps.image-url-source-entity-attribute + + + + + + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/image-map-provider-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/image-map-provider-settings.component.ts new file mode 100644 index 0000000000..c9a6947b43 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/image-map-provider-settings.component.ts @@ -0,0 +1,253 @@ +/// +/// Copyright © 2016-2022 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 { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { ImageMapProviderSettings } from '@home/components/widget/lib/maps/map-models'; +import { IAliasController } from '@core/api/widget-api.models'; +import { Observable, of } from 'rxjs'; +import { catchError, map, mergeMap, publishReplay, refCount, startWith, tap } from 'rxjs/operators'; +import { DataKey } from '@shared/models/widget.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { EntityService } from '@core/http/entity.service'; + +@Component({ + selector: 'tb-image-map-provider-settings', + templateUrl: './image-map-provider-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ImageMapProviderSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ImageMapProviderSettingsComponent), + multi: true + } + ] +}) +export class ImageMapProviderSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { + + @ViewChild('entityAliasInput') entityAliasInput: ElementRef; + + @ViewChild('keyInput') keyInput: ElementRef; + + @Input() + disabled: boolean; + + @Input() + aliasController: IAliasController; + + private modelValue: ImageMapProviderSettings; + + private propagateChange = null; + + public providerSettingsFormGroup: FormGroup; + + filteredEntityAliases: Observable>; + aliasSearchText = ''; + + filteredKeys: Observable>; + keySearchText = ''; + + private latestKeySearchResult: Array = null; + private keysFetchObservable$: Observable> = null; + + private entityAliasList: Array = []; + + constructor(protected store: Store, + private translate: TranslateService, + private entityService: EntityService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.providerSettingsFormGroup = this.fb.group({ + mapImageUrl: [null, []], + imageEntityAlias: [null, []], + imageUrlAttribute: [null, []] + }); + this.providerSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + + this.filteredEntityAliases = this.providerSettingsFormGroup.get('imageEntityAlias').valueChanges + .pipe( + tap((value) => { + if (this.modelValue?.imageEntityAlias !== value) { + this.latestKeySearchResult = null; + this.keysFetchObservable$ = null; + this.providerSettingsFormGroup.get('imageUrlAttribute').setValue(this.providerSettingsFormGroup.get('imageUrlAttribute').value); + } + }), + map(value => value ? value : ''), + mergeMap(name => this.fetchEntityAliases(name) ) + ); + + this.filteredKeys = this.providerSettingsFormGroup.get('imageUrlAttribute').valueChanges + .pipe( + map(value => value ? value : ''), + mergeMap(name => this.fetchKeys(name) ) + ); + + if (this.aliasController) { + const entityAliases = this.aliasController.getEntityAliases(); + for (const aliasId of Object.keys(entityAliases)) { + this.entityAliasList.push(entityAliases[aliasId].alias); + } + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.providerSettingsFormGroup.disable({emitEvent: false}); + } else { + this.providerSettingsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: ImageMapProviderSettings): void { + this.modelValue = value; + this.providerSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + } + + public validate(c: FormControl) { + return this.providerSettingsFormGroup.valid ? null : { + imageMapProviderSettings: { + valid: false, + }, + }; + } + + private updateModel() { + const value: ImageMapProviderSettings = this.providerSettingsFormGroup.value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } + + clearEntityAlias() { + this.providerSettingsFormGroup.get('imageEntityAlias').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.entityAliasInput.nativeElement.blur(); + this.entityAliasInput.nativeElement.focus(); + }, 0); + } + + onEntityAliasFocus() { + this.providerSettingsFormGroup.get('imageEntityAlias').updateValueAndValidity({onlySelf: true, emitEvent: true}); + } + + clearKey() { + this.providerSettingsFormGroup.get('imageUrlAttribute').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.keyInput.nativeElement.blur(); + this.keyInput.nativeElement.focus(); + }, 0); + } + + onKeyFocus() { + this.providerSettingsFormGroup.get('imageUrlAttribute').updateValueAndValidity({onlySelf: true, emitEvent: true}); + } + + private fetchEntityAliases(searchText?: string): Observable> { + this.aliasSearchText = searchText; + let result = this.entityAliasList; + if (searchText && searchText.length) { + result = this.entityAliasList.filter((entityAlias) => entityAlias.toLowerCase().includes(searchText.toLowerCase())); + } + return of(result); + } + + private fetchKeys(searchText?: string): Observable> { + if (this.keySearchText !== searchText || this.latestKeySearchResult === null) { + this.keySearchText = searchText; + const dataKeyFilter = this.createKeyFilter(this.keySearchText); + return this.getKeys().pipe( + map(name => name.filter(dataKeyFilter)), + tap(res => this.latestKeySearchResult = res) + ); + } + return of(this.latestKeySearchResult); + } + + private getKeys() { + if (this.keysFetchObservable$ === null) { + let fetchObservable: Observable>; + let entityAliasId: string; + const entityAlias: string = this.providerSettingsFormGroup.get('imageEntityAlias').value; + if (entityAlias && this.aliasController) { + entityAliasId = this.aliasController.getEntityAliasId(entityAlias); + } + if (entityAliasId) { + const dataKeyTypes = [DataKeyType.attribute]; + fetchObservable = this.fetchEntityKeys(entityAliasId, dataKeyTypes); + } else { + fetchObservable = of([]); + } + this.keysFetchObservable$ = fetchObservable.pipe( + map((dataKeys) => dataKeys.map((dataKey) => dataKey.name)), + publishReplay(1), + refCount() + ); + } + return this.keysFetchObservable$; + } + + private fetchEntityKeys(entityAliasId: string, dataKeyTypes: Array): Observable> { + return this.aliasController.getAliasInfo(entityAliasId).pipe( + mergeMap((aliasInfo) => { + return this.entityService.getEntityKeysByEntityFilter( + aliasInfo.entityFilter, + dataKeyTypes, + {ignoreLoading: true, ignoreErrors: true} + ).pipe( + catchError(() => of([])) + ); + }), + catchError(() => of([] as Array)) + ); + } + + private createKeyFilter(query: string): (key: string) => boolean { + const lowercaseQuery = query.toLowerCase(); + return key => key.toLowerCase().startsWith(lowercaseQuery); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-editor-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-editor-settings.component.html new file mode 100644 index 0000000000..9dfa9f9db8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-editor-settings.component.html @@ -0,0 +1,52 @@ + +
+
+ widgets.maps.editor-settings + + {{ 'widgets.maps.enable-snapping' | translate }} + + + {{ 'widgets.maps.init-draggable-mode' | translate }} + + + + + + {{ 'widgets.maps.hide-all-edit-buttons' | translate }} + + + + widget-config.advanced-settings + + + + + {{ 'widgets.maps.hide-draw-buttons' | translate }} + + + {{ 'widgets.maps.hide-edit-buttons' | translate }} + + + {{ 'widgets.maps.hide-remove-button' | translate }} + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-editor-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-editor-settings.component.ts new file mode 100644 index 0000000000..02d47a686d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-editor-settings.component.ts @@ -0,0 +1,141 @@ +/// +/// Copyright © 2016-2022 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 { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { MapEditorSettings } from '@home/components/widget/lib/maps/map-models'; +import { WidgetService } from '@core/http/widget.service'; + +@Component({ + selector: 'tb-map-editor-settings', + templateUrl: './map-editor-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapEditorSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MapEditorSettingsComponent), + multi: true + } + ] +}) +export class MapEditorSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { + + @Input() + disabled: boolean; + + private modelValue: MapEditorSettings; + + private propagateChange = null; + + public mapEditorSettingsFormGroup: FormGroup; + + constructor(protected store: Store, + private translate: TranslateService, + private widgetService: WidgetService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.mapEditorSettingsFormGroup = this.fb.group({ + snappable: [null, []], + initDragMode: [null, []], + hideAllControlButton: [null, []], + hideDrawControlButton: [null, []], + hideEditControlButton: [null, []], + hideRemoveControlButton: [null, []], + }); + this.mapEditorSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + this.mapEditorSettingsFormGroup.get('hideAllControlButton').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.updateValidators(false); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.mapEditorSettingsFormGroup.disable({emitEvent: false}); + } else { + this.mapEditorSettingsFormGroup.enable({emitEvent: false}); + this.updateValidators(false); + } + } + + writeValue(value: MapEditorSettings): void { + this.modelValue = value; + this.mapEditorSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + this.updateValidators(false); + } + + public validate(c: FormControl) { + return this.mapEditorSettingsFormGroup.valid ? null : { + mapEditorSettings: { + valid: false, + }, + }; + } + + private updateModel() { + const value: MapEditorSettings = this.mapEditorSettingsFormGroup.value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } + + private updateValidators(emitEvent?: boolean): void { + const hideAllControlButton: boolean = this.mapEditorSettingsFormGroup.get('hideAllControlButton').value; + if (hideAllControlButton) { + this.mapEditorSettingsFormGroup.get('hideDrawControlButton').disable({emitEvent}); + this.mapEditorSettingsFormGroup.get('hideEditControlButton').disable({emitEvent}); + this.mapEditorSettingsFormGroup.get('hideRemoveControlButton').disable({emitEvent}); + } else { + this.mapEditorSettingsFormGroup.get('hideDrawControlButton').enable({emitEvent}); + this.mapEditorSettingsFormGroup.get('hideEditControlButton').enable({emitEvent}); + this.mapEditorSettingsFormGroup.get('hideRemoveControlButton').enable({emitEvent}); + } + this.mapEditorSettingsFormGroup.get('hideDrawControlButton').updateValueAndValidity({emitEvent: false}); + this.mapEditorSettingsFormGroup.get('hideEditControlButton').updateValueAndValidity({emitEvent: false}); + this.mapEditorSettingsFormGroup.get('hideRemoveControlButton').updateValueAndValidity({emitEvent: false}); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-provider-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-provider-settings.component.html new file mode 100644 index 0000000000..9eef35e732 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-provider-settings.component.html @@ -0,0 +1,51 @@ + +
+
+ widgets.maps.map-provider-settings + + widgets.maps.map-provider + + + {{mapProviderTranslations.get(provider) | translate}} + + + + + + + + + + + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-provider-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-provider-settings.component.ts new file mode 100644 index 0000000000..51e95ec42b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-provider-settings.component.ts @@ -0,0 +1,204 @@ +/// +/// Copyright © 2016-2022 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 { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { + GoogleMapProviderSettings, + HereMapProviderSettings, + ImageMapProviderSettings, + MapProviders, + MapProviderSettings, + mapProviderTranslationMap, + OpenStreetMapProviderSettings, + TencentMapProviderSettings +} from '@home/components/widget/lib/maps/map-models'; +import { extractType } from '@core/utils'; +import { keys } from 'ts-transformer-keys'; +import { IAliasController } from '@core/api/widget-api.models'; + +@Component({ + selector: 'tb-map-provider-settings', + templateUrl: './map-provider-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapProviderSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MapProviderSettingsComponent), + multi: true + } + ] +}) +export class MapProviderSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { + + @Input() + aliasController: IAliasController; + + @Input() + disabled: boolean; + + @Input() + ignoreImageMap = false; + + private modelValue: MapProviderSettings; + + private propagateChange = null; + + public providerSettingsFormGroup: FormGroup; + + mapProviders = Object.values(MapProviders); + + mapProvider = MapProviders; + + mapProviderTranslations = mapProviderTranslationMap; + + constructor(protected store: Store, + private translate: TranslateService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + if (this.ignoreImageMap) { + this.mapProviders = this.mapProviders.filter((provider) => provider !== MapProviders.image); + } + this.providerSettingsFormGroup = this.fb.group({ + provider: [null, [Validators.required]], + googleProviderSettings: [null, []], + openstreetProviderSettings: [null, []], + hereProviderSettings: [null, []], + imageMapProviderSettings: [null, []], + tencentMapProviderSettings: [null, []] + }); + this.providerSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + this.providerSettingsFormGroup.get('provider').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.updateValidators(false); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.providerSettingsFormGroup.disable({emitEvent: false}); + } else { + this.providerSettingsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: MapProviderSettings): void { + this.modelValue = value; + const provider = value?.provider; + const googleProviderSettings = extractType(value, keys()); + const openstreetProviderSettings = extractType(value, keys()); + const hereProviderSettings = extractType(value, keys()); + const imageMapProviderSettings = extractType(value, keys()); + const tencentMapProviderSettings = extractType(value, keys()); + this.providerSettingsFormGroup.patchValue( + { + provider, + googleProviderSettings, + openstreetProviderSettings, + hereProviderSettings, + imageMapProviderSettings, + tencentMapProviderSettings + }, {emitEvent: false} + ); + this.updateValidators(false); + } + + public validate(c: FormControl) { + return this.providerSettingsFormGroup.valid ? null : { + mapProviderSettings: { + valid: false, + }, + }; + } + + private updateModel() { + const value: { + provider: MapProviders, + googleProviderSettings: GoogleMapProviderSettings, + openstreetProviderSettings: OpenStreetMapProviderSettings, + hereProviderSettings: HereMapProviderSettings, + imageMapProviderSettings: ImageMapProviderSettings, + tencentMapProviderSettings: TencentMapProviderSettings + } = this.providerSettingsFormGroup.value; + this.modelValue = { + provider: value.provider, + ...value.googleProviderSettings, + ...value.openstreetProviderSettings, + ...value.hereProviderSettings, + ...value.imageMapProviderSettings, + ...value.tencentMapProviderSettings + }; + this.propagateChange(this.modelValue); + } + + private updateValidators(emitEvent?: boolean): void { + const provider: MapProviders = this.providerSettingsFormGroup.get('provider').value; + this.providerSettingsFormGroup.disable({emitEvent: false}); + this.providerSettingsFormGroup.get('provider').enable({emitEvent: false}); + switch (provider) { + case MapProviders.google: + this.providerSettingsFormGroup.get('googleProviderSettings').enable({emitEvent}); + this.providerSettingsFormGroup.get('googleProviderSettings').updateValueAndValidity({emitEvent: false}); + break; + case MapProviders.openstreet: + this.providerSettingsFormGroup.get('openstreetProviderSettings').enable({emitEvent}); + this.providerSettingsFormGroup.get('openstreetProviderSettings').updateValueAndValidity({emitEvent: false}); + break; + case MapProviders.here: + this.providerSettingsFormGroup.get('hereProviderSettings').enable({emitEvent}); + this.providerSettingsFormGroup.get('hereProviderSettings').updateValueAndValidity({emitEvent: false}); + break; + case MapProviders.image: + this.providerSettingsFormGroup.get('imageMapProviderSettings').enable({emitEvent}); + this.providerSettingsFormGroup.get('imageMapProviderSettings').updateValueAndValidity({emitEvent: false}); + break; + case MapProviders.tencent: + this.providerSettingsFormGroup.get('tencentMapProviderSettings').enable({emitEvent}); + this.providerSettingsFormGroup.get('tencentMapProviderSettings').updateValueAndValidity({emitEvent: false}); + break; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-settings.component.html new file mode 100644 index 0000000000..74a96d9dc7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-settings.component.html @@ -0,0 +1,53 @@ + +
+ + + + + + + + + + + + + + + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-settings.component.ts new file mode 100644 index 0000000000..b2652d9c69 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-settings.component.ts @@ -0,0 +1,217 @@ +/// +/// Copyright © 2016-2022 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 { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { + CircleSettings, + CommonMapSettings, + MapEditorSettings, + MapProviders, + MapProviderSettings, + MarkerClusteringSettings, + MarkersSettings, + PolygonSettings, + PolylineSettings, + UnitedMapSettings +} from '@home/components/widget/lib/maps/map-models'; +import { extractType } from '@core/utils'; +import { keys } from 'ts-transformer-keys'; +import { IAliasController } from '@core/api/widget-api.models'; +import { Widget } from '@shared/models/widget.models'; + +@Component({ + selector: 'tb-map-settings', + templateUrl: './map-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MapSettingsComponent), + multi: true + } + ] +}) +export class MapSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { + + @Input() + disabled: boolean; + + @Input() + aliasController: IAliasController; + + @Input() + widget: Widget; + + @Input() + routeMap = false; + + mapProvider = MapProviders; + + private modelValue: UnitedMapSettings; + + private propagateChange = null; + + public mapSettingsFormGroup: FormGroup; + + constructor(protected store: Store, + private translate: TranslateService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.mapSettingsFormGroup = this.fb.group({ + mapProviderSettings: [null, []], + commonMapSettings: [null, []], + markersSettings: [null, []], + polygonSettings: [null, []], + circleSettings: [null, []], + }); + if (this.routeMap) { + this.mapSettingsFormGroup.addControl('routeMapSettings', this.fb.control(null, [])); + } else { + this.mapSettingsFormGroup.addControl('markerClusteringSettings', this.fb.control(null, [])); + } + this.mapSettingsFormGroup.addControl('mapEditorSettings', this.fb.control(null, [])); + this.mapSettingsFormGroup.get('mapProviderSettings').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.mapSettingsFormGroup.get('markersSettings').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.mapSettingsFormGroup.get('polygonSettings').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.mapSettingsFormGroup.get('circleSettings').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.mapSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + this.updateValidators(false); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.mapSettingsFormGroup.disable({emitEvent: false}); + } else { + this.mapSettingsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: UnitedMapSettings): void { + this.modelValue = value; + const mapProviderSettings = extractType(value, keys()); + const commonMapSettings = extractType(value, keys()); + const markersSettings = extractType(value, keys()); + const polygonSettings = extractType(value, keys()); + const circleSettings = extractType(value, keys()); + const mapEditorSettings = extractType(value, keys()); + const formValue = { + mapProviderSettings, + commonMapSettings, + markersSettings, + polygonSettings, + circleSettings, + mapEditorSettings + } as any; + if (this.routeMap) { + formValue.routeMapSettings = extractType(value, ['strokeWeight', 'strokeOpacity']); + } else { + formValue.markerClusteringSettings = extractType(value, keys()); + } + this.mapSettingsFormGroup.patchValue( formValue, {emitEvent: false} ); + this.updateValidators(false); + } + + public validate(c: FormControl) { + return this.mapSettingsFormGroup.valid ? null : { + mapSettings: { + valid: false, + }, + }; + } + + private updateModel() { + const value = this.mapSettingsFormGroup.value; + this.modelValue = { + ...value.mapProviderSettings, + ...value.commonMapSettings, + ...value.markersSettings, + ...value.polygonSettings, + ...value.circleSettings, + ...value.mapEditorSettings + }; + if (this.routeMap) { + this.modelValue = {...this.modelValue, ...value.routeMapSettings}; + } else { + this.modelValue = {...this.modelValue, ...value.markerClusteringSettings}; + } + this.propagateChange(this.modelValue); + } + + displayEditorSettings(): boolean { + const markersSettings: MarkersSettings = this.mapSettingsFormGroup.get('markersSettings').value; + const polygonSettings: PolygonSettings = this.mapSettingsFormGroup.get('polygonSettings').value; + const circleSettings: CircleSettings = this.mapSettingsFormGroup.get('circleSettings').value; + return markersSettings?.draggableMarker || polygonSettings?.editablePolygon || circleSettings?.editableCircle; + } + + private updateValidators(emitEvent?: boolean): void { + const displayEditorSettings = this.displayEditorSettings(); + if (displayEditorSettings) { + this.mapSettingsFormGroup.get('mapEditorSettings').enable({emitEvent}); + } else { + this.mapSettingsFormGroup.get('mapEditorSettings').disable({emitEvent}); + } + if (!this.routeMap) { + const mapProviderSettings: MapProviderSettings = this.mapSettingsFormGroup.get('mapProviderSettings').value; + if (mapProviderSettings?.provider === MapProviders.image) { + this.mapSettingsFormGroup.get('markerClusteringSettings').disable({emitEvent}); + } else { + this.mapSettingsFormGroup.get('markerClusteringSettings').enable({emitEvent}); + } + this.mapSettingsFormGroup.get('markerClusteringSettings').updateValueAndValidity({emitEvent: false}); + } + this.mapSettingsFormGroup.get('mapEditorSettings').updateValueAndValidity({emitEvent: false}); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.html new file mode 100644 index 0000000000..fa4807ea63 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.html @@ -0,0 +1,24 @@ + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.ts new file mode 100644 index 0000000000..01d4287bb9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.ts @@ -0,0 +1,63 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { defaultMapSettings } from 'src/app/modules/home/components/widget/lib/maps/map-models'; + +@Component({ + selector: 'tb-map-widget-settings', + templateUrl: './map-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class MapWidgetSettingsComponent extends WidgetSettingsComponent { + + mapWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.mapWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + ...defaultMapSettings + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.mapWidgetSettingsForm = this.fb.group({ + mapSettings: [settings.mapSettings, []] + }); + } + + protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { + return { + mapSettings: settings + }; + } + + protected prepareOutputSettings(settings: any): WidgetSettings { + return settings.mapSettings; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.html new file mode 100644 index 0000000000..9ceb172090 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.html @@ -0,0 +1,70 @@ + +
+
+ widgets.maps.markers-clustering-settings + + + + + {{ 'widgets.maps.use-map-markers-clustering' | translate }} + + + + widget-config.advanced-settings + + + + + {{ 'widgets.maps.zoom-on-cluster-click' | translate }} + +
+ + widgets.maps.max-cluster-zoom + + + + widgets.maps.max-cluster-radius-pixels + + +
+
+ + {{ 'widgets.maps.cluster-zoom-animation' | translate }} + + + {{ 'widgets.maps.show-markers-bounds-on-cluster-mouse-over' | translate }} + +
+ + {{ 'widgets.maps.spiderfy-max-zoom-level' | translate }} + +
+ widgets.maps.load-optimization + + {{ 'widgets.maps.cluster-chunked-loading' | translate }} + + + {{ 'widgets.maps.cluster-markers-lazy-load' | translate }} + +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.ts new file mode 100644 index 0000000000..f1ca781bed --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.ts @@ -0,0 +1,141 @@ +/// +/// Copyright © 2016-2022 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 { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { MarkerClusteringSettings } from '@home/components/widget/lib/maps/map-models'; +import { WidgetService } from '@core/http/widget.service'; + +@Component({ + selector: 'tb-marker-clustering-settings', + templateUrl: './marker-clustering-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MarkerClusteringSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MarkerClusteringSettingsComponent), + multi: true + } + ] +}) +export class MarkerClusteringSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { + + @Input() + disabled: boolean; + + private modelValue: MarkerClusteringSettings; + + private propagateChange = null; + + public markerClusteringSettingsFormGroup: FormGroup; + + constructor(protected store: Store, + private translate: TranslateService, + private widgetService: WidgetService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.markerClusteringSettingsFormGroup = this.fb.group({ + useClusterMarkers: [null, []], + zoomOnClick: [null, []], + maxZoom: [null, [Validators.min(0), Validators.max(18)]], + maxClusterRadius: [null, [Validators.min(0)]], + animate: [null, []], + spiderfyOnMaxZoom: [null, []], + showCoverageOnHover: [null, []], + chunkedLoading: [null, []], + removeOutsideVisibleBounds: [null, []] + }); + this.markerClusteringSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + this.markerClusteringSettingsFormGroup.get('useClusterMarkers').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.updateValidators(false); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.markerClusteringSettingsFormGroup.disable({emitEvent: false}); + } else { + this.markerClusteringSettingsFormGroup.enable({emitEvent: false}); + this.updateValidators(false); + } + } + + writeValue(value: MarkerClusteringSettings): void { + this.modelValue = value; + this.markerClusteringSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + this.updateValidators(false); + } + + public validate(c: FormControl) { + return this.markerClusteringSettingsFormGroup.valid ? null : { + markerClusteringSettings: { + valid: false, + }, + }; + } + + private updateModel() { + const value: MarkerClusteringSettings = this.markerClusteringSettingsFormGroup.value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } + + private updateValidators(emitEvent?: boolean): void { + const useClusterMarkers: boolean = this.markerClusteringSettingsFormGroup.get('useClusterMarkers').value; + + this.markerClusteringSettingsFormGroup.disable({emitEvent: false}); + this.markerClusteringSettingsFormGroup.get('useClusterMarkers').enable({emitEvent: false}); + + if (useClusterMarkers) { + this.markerClusteringSettingsFormGroup.enable({emitEvent: false}); + } + this.markerClusteringSettingsFormGroup.updateValueAndValidity({emitEvent: false}); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/markers-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/markers-settings.component.html new file mode 100644 index 0000000000..ea955f5f7f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/markers-settings.component.html @@ -0,0 +1,197 @@ + +
+
+ widgets.maps.markers-settings +
+ + widgets.maps.marker-offset-x + + + + widgets.maps.marker-offset-y + + +
+ + + + {{ 'widgets.maps.draggable-marker' | translate }} + +
+ widgets.maps.label + + + + + {{ 'widgets.maps.show-label' | translate }} + + + + widget-config.advanced-settings + + + + + {{ 'widgets.maps.use-label-function' | translate }} + + + + + + + +
+
+ widgets.maps.tooltip + + + + + {{ 'widgets.maps.show-tooltip' | translate }} + + + + widget-config.advanced-settings + + + + + widgets.maps.show-tooltip-action + + + {{showTooltipActionTranslations.get(action) | translate}} + + + + + {{ 'widgets.maps.auto-close-tooltips' | translate }} + + + {{ 'widgets.maps.use-tooltip-function' | translate }} + + + + + +
+ + widgets.maps.tooltip-offset-x + + + + widgets.maps.tooltip-offset-y + + +
+
+
+
+
+ widgets.maps.color + + + + + + + {{ 'widgets.maps.use-color-function' | translate }} + + + + widget-config.advanced-settings + + + + + + + +
+
+ widgets.maps.marker-image + + + + + {{ 'widgets.maps.use-marker-image-function' | translate }} + + + + widget-config.advanced-settings + + + + + + + widgets.maps.custom-marker-image-size + + + + + + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/markers-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/markers-settings.component.ts new file mode 100644 index 0000000000..f877372c40 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/markers-settings.component.ts @@ -0,0 +1,264 @@ +/// +/// Copyright © 2016-2022 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 { Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { + MapProviders, + MarkersSettings, ShowTooltipAction, showTooltipActionTranslationMap +} from '@home/components/widget/lib/maps/map-models'; +import { WidgetService } from '@core/http/widget.service'; + +@Component({ + selector: 'tb-markers-settings', + templateUrl: './markers-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MarkersSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MarkersSettingsComponent), + multi: true + } + ] +}) +export class MarkersSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator, OnChanges { + + @Input() + disabled: boolean; + + @Input() + provider: MapProviders; + + mapProvider = MapProviders; + + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + + private modelValue: MarkersSettings; + + private propagateChange = null; + + public markersSettingsFormGroup: FormGroup; + + showTooltipActions = Object.values(ShowTooltipAction); + + showTooltipActionTranslations = showTooltipActionTranslationMap; + + constructor(protected store: Store, + private translate: TranslateService, + private widgetService: WidgetService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.markersSettingsFormGroup = this.fb.group({ + markerOffsetX: [null, []], + markerOffsetY: [null, []], + posFunction: [null, []], + draggableMarker: [null, []], + showLabel: [null, []], + useLabelFunction: [null, []], + label: [null, []], + labelFunction: [null, []], + showTooltip: [null, []], + showTooltipAction: [null, []], + autocloseTooltip: [null, []], + useTooltipFunction: [null, []], + tooltipPattern: [null, []], + tooltipFunction: [null, []], + tooltipOffsetX: [null, []], + tooltipOffsetY: [null, []], + color: [null, []], + useColorFunction: [null, []], + colorFunction: [null, []], + useMarkerImageFunction: [null, []], + markerImage: [null, []], + markerImageSize: [null, [Validators.min(1)]], + markerImageFunction: [null, []], + markerImages: [null, []] + }); + this.markersSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + this.markersSettingsFormGroup.get('showLabel').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.markersSettingsFormGroup.get('useLabelFunction').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.markersSettingsFormGroup.get('showTooltip').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.markersSettingsFormGroup.get('useTooltipFunction').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.markersSettingsFormGroup.get('useColorFunction').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.markersSettingsFormGroup.get('useMarkerImageFunction').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.updateValidators(false); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'provider') { + this.updateValidators(false); + } + } + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.markersSettingsFormGroup.disable({emitEvent: false}); + } else { + this.markersSettingsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: MarkersSettings): void { + this.modelValue = value; + this.markersSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + this.updateValidators(false); + } + + public validate(c: FormControl) { + return this.markersSettingsFormGroup.valid ? null : { + markersSettings: { + valid: false, + }, + }; + } + + private updateModel() { + const value: MarkersSettings = this.markersSettingsFormGroup.value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } + + private updateValidators(emitEvent?: boolean): void { + const showLabel: boolean = this.markersSettingsFormGroup.get('showLabel').value; + const useLabelFunction: boolean = this.markersSettingsFormGroup.get('useLabelFunction').value; + const showTooltip: boolean = this.markersSettingsFormGroup.get('showTooltip').value; + const useTooltipFunction: boolean = this.markersSettingsFormGroup.get('useTooltipFunction').value; + const useColorFunction: boolean = this.markersSettingsFormGroup.get('useColorFunction').value; + const useMarkerImageFunction: boolean = this.markersSettingsFormGroup.get('useMarkerImageFunction').value; + if (this.provider === MapProviders.image) { + this.markersSettingsFormGroup.get('posFunction').enable({emitEvent}); + } else { + this.markersSettingsFormGroup.get('posFunction').disable({emitEvent}); + } + if (showLabel) { + this.markersSettingsFormGroup.get('useLabelFunction').enable({emitEvent: false}); + if (useLabelFunction) { + this.markersSettingsFormGroup.get('labelFunction').enable({emitEvent}); + this.markersSettingsFormGroup.get('label').disable({emitEvent}); + } else { + this.markersSettingsFormGroup.get('labelFunction').disable({emitEvent}); + this.markersSettingsFormGroup.get('label').enable({emitEvent}); + } + } else { + this.markersSettingsFormGroup.get('useLabelFunction').disable({emitEvent: false}); + this.markersSettingsFormGroup.get('labelFunction').disable({emitEvent}); + this.markersSettingsFormGroup.get('label').disable({emitEvent}); + } + if (showTooltip) { + this.markersSettingsFormGroup.get('showTooltipAction').enable({emitEvent}); + this.markersSettingsFormGroup.get('autocloseTooltip').enable({emitEvent}); + this.markersSettingsFormGroup.get('useTooltipFunction').enable({emitEvent: false}); + this.markersSettingsFormGroup.get('tooltipOffsetX').enable({emitEvent}); + this.markersSettingsFormGroup.get('tooltipOffsetY').enable({emitEvent}); + if (useTooltipFunction) { + this.markersSettingsFormGroup.get('tooltipFunction').enable({emitEvent}); + this.markersSettingsFormGroup.get('tooltipPattern').disable({emitEvent}); + } else { + this.markersSettingsFormGroup.get('tooltipFunction').disable({emitEvent}); + this.markersSettingsFormGroup.get('tooltipPattern').enable({emitEvent}); + } + } else { + this.markersSettingsFormGroup.get('showTooltipAction').disable({emitEvent}); + this.markersSettingsFormGroup.get('autocloseTooltip').disable({emitEvent}); + this.markersSettingsFormGroup.get('useTooltipFunction').disable({emitEvent: false}); + this.markersSettingsFormGroup.get('tooltipFunction').disable({emitEvent}); + this.markersSettingsFormGroup.get('tooltipPattern').disable({emitEvent}); + this.markersSettingsFormGroup.get('tooltipOffsetX').disable({emitEvent}); + this.markersSettingsFormGroup.get('tooltipOffsetY').disable({emitEvent}); + } + if (useColorFunction) { + this.markersSettingsFormGroup.get('colorFunction').enable({emitEvent}); + } else { + this.markersSettingsFormGroup.get('colorFunction').disable({emitEvent}); + } + if (useMarkerImageFunction) { + this.markersSettingsFormGroup.get('markerImageFunction').enable({emitEvent}); + this.markersSettingsFormGroup.get('markerImages').enable({emitEvent}); + this.markersSettingsFormGroup.get('markerImage').disable({emitEvent}); + this.markersSettingsFormGroup.get('markerImageSize').disable({emitEvent}); + } else { + this.markersSettingsFormGroup.get('markerImageFunction').disable({emitEvent}); + this.markersSettingsFormGroup.get('markerImages').disable({emitEvent}); + this.markersSettingsFormGroup.get('markerImage').enable({emitEvent}); + this.markersSettingsFormGroup.get('markerImageSize').enable({emitEvent}); + } + this.markersSettingsFormGroup.get('posFunction').updateValueAndValidity({emitEvent: false}); + this.markersSettingsFormGroup.get('useLabelFunction').updateValueAndValidity({emitEvent: false}); + this.markersSettingsFormGroup.get('labelFunction').updateValueAndValidity({emitEvent: false}); + this.markersSettingsFormGroup.get('label').updateValueAndValidity({emitEvent: false}); + this.markersSettingsFormGroup.get('showTooltipAction').updateValueAndValidity({emitEvent: false}); + this.markersSettingsFormGroup.get('autocloseTooltip').updateValueAndValidity({emitEvent: false}); + this.markersSettingsFormGroup.get('useTooltipFunction').updateValueAndValidity({emitEvent: false}); + this.markersSettingsFormGroup.get('tooltipFunction').updateValueAndValidity({emitEvent: false}); + this.markersSettingsFormGroup.get('tooltipPattern').updateValueAndValidity({emitEvent: false}); + this.markersSettingsFormGroup.get('tooltipOffsetX').updateValueAndValidity({emitEvent: false}); + this.markersSettingsFormGroup.get('tooltipOffsetY').updateValueAndValidity({emitEvent: false}); + this.markersSettingsFormGroup.get('colorFunction').updateValueAndValidity({emitEvent: false}); + this.markersSettingsFormGroup.get('markerImageFunction').updateValueAndValidity({emitEvent: false}); + this.markersSettingsFormGroup.get('markerImages').updateValueAndValidity({emitEvent: false}); + this.markersSettingsFormGroup.get('markerImage').updateValueAndValidity({emitEvent: false}); + this.markersSettingsFormGroup.get('markerImageSize').updateValueAndValidity({emitEvent: false}); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/openstreet-map-provider-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/openstreet-map-provider-settings.component.html new file mode 100644 index 0000000000..a53db66706 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/openstreet-map-provider-settings.component.html @@ -0,0 +1,46 @@ + +
+ + widgets.maps.openstreet-provider + + + {{openStreetMapProviderTranslations.get(provider) | translate}} + + + + + + + + {{ 'widgets.maps.use-custom-provider' | translate }} + + + + widget-config.advanced-settings + + + + + widgets.maps.custom-provider-tile-url + + + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/openstreet-map-provider-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/openstreet-map-provider-settings.component.ts new file mode 100644 index 0000000000..6d7ba24af1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/openstreet-map-provider-settings.component.ts @@ -0,0 +1,139 @@ +/// +/// Copyright © 2016-2022 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 { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { + OpenStreetMapProvider, + OpenStreetMapProviderSettings, + openStreetMapProviderTranslationMap +} from '@home/components/widget/lib/maps/map-models'; + +@Component({ + selector: 'tb-openstreet-map-provider-settings', + templateUrl: './openstreet-map-provider-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => OpenStreetMapProviderSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => OpenStreetMapProviderSettingsComponent), + multi: true + } + ] +}) +export class OpenStreetMapProviderSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { + + @Input() + disabled: boolean; + + private modelValue: OpenStreetMapProviderSettings; + + private propagateChange = null; + + public providerSettingsFormGroup: FormGroup; + + mapProviders = Object.values(OpenStreetMapProvider); + + openStreetMapProviderTranslations = openStreetMapProviderTranslationMap; + + constructor(protected store: Store, + private translate: TranslateService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.providerSettingsFormGroup = this.fb.group({ + mapProvider: [null, [Validators.required]], + useCustomProvider: [null, []], + customProviderTileUrl: [null, [Validators.required]] + }); + this.providerSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + this.providerSettingsFormGroup.get('useCustomProvider').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.updateValidators(false); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.providerSettingsFormGroup.disable({emitEvent: false}); + } else { + this.providerSettingsFormGroup.enable({emitEvent: false}); + this.updateValidators(false); + } + } + + writeValue(value: OpenStreetMapProviderSettings): void { + this.modelValue = value; + this.providerSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + this.updateValidators(false); + } + + public validate(c: FormControl) { + return this.providerSettingsFormGroup.valid ? null : { + openStreetProviderSettings: { + valid: false, + }, + }; + } + + private updateModel() { + const value: OpenStreetMapProviderSettings = this.providerSettingsFormGroup.value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } + + private updateValidators(emitEvent?: boolean): void { + const useCustomProvider: boolean = this.providerSettingsFormGroup.get('useCustomProvider').value; + if (useCustomProvider) { + this.providerSettingsFormGroup.get('customProviderTileUrl').enable({emitEvent}); + } else { + this.providerSettingsFormGroup.get('customProviderTileUrl').disable({emitEvent}); + } + this.providerSettingsFormGroup.get('customProviderTileUrl').updateValueAndValidity({emitEvent: false}); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/polygon-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/polygon-settings.component.html new file mode 100644 index 0000000000..8f3f74b1ff --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/polygon-settings.component.html @@ -0,0 +1,199 @@ + +
+
+ widgets.maps.polygon-settings + + + + + {{ 'widgets.maps.show-polygon' | translate }} + + + + widget-config.advanced-settings + + + + + + + {{ 'widgets.maps.enable-polygon-edit' | translate }} + +
+ widgets.maps.polygon-label + + + + + {{ 'widgets.maps.show-polygon-label' | translate }} + + + + widget-config.advanced-settings + + + + + {{ 'widgets.maps.use-polygon-label-function' | translate }} + + + + + + + +
+
+ widgets.maps.polygon-tooltip + + + + + {{ 'widgets.maps.show-polygon-tooltip' | translate }} + + + + widget-config.advanced-settings + + + + + widgets.maps.show-tooltip-action + + + {{showTooltipActionTranslations.get(action) | translate}} + + + + + {{ 'widgets.maps.auto-close-polygon-tooltips' | translate }} + + + {{ 'widgets.maps.use-polygon-tooltip-function' | translate }} + + + + + + + +
+
+ widgets.maps.polygon-color +
+ + + + widgets.maps.polygon-opacity + + +
+ + + + + {{ 'widgets.maps.use-polygon-color-function' | translate }} + + + + widget-config.advanced-settings + + + + + + + +
+
+ widgets.maps.polygon-stroke +
+ + + + widgets.maps.stroke-opacity + + + + widgets.maps.stroke-weight + + +
+ + + + + {{ 'widgets.maps.use-polygon-stroke-color-function' | translate }} + + + + widget-config.advanced-settings + + + + + + + +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/polygon-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/polygon-settings.component.ts new file mode 100644 index 0000000000..9c375350c7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/polygon-settings.component.ts @@ -0,0 +1,243 @@ +/// +/// Copyright © 2016-2022 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 { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { + PolygonSettings, + ShowTooltipAction, + showTooltipActionTranslationMap +} from '@home/components/widget/lib/maps/map-models'; +import { WidgetService } from '@core/http/widget.service'; +import { Widget } from '@shared/models/widget.models'; + +@Component({ + selector: 'tb-polygon-settings', + templateUrl: './polygon-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => PolygonSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => PolygonSettingsComponent), + multi: true + } + ] +}) +export class PolygonSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { + + @Input() + disabled: boolean; + + @Input() + widget: Widget; + + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + + private modelValue: PolygonSettings; + + private propagateChange = null; + + public polygonSettingsFormGroup: FormGroup; + + showTooltipActions = Object.values(ShowTooltipAction); + + showTooltipActionTranslations = showTooltipActionTranslationMap; + + constructor(protected store: Store, + private translate: TranslateService, + private widgetService: WidgetService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.polygonSettingsFormGroup = this.fb.group({ + showPolygon: [null, []], + polygonKeyName: [null, [Validators.required]], + editablePolygon: [null, []], + showPolygonLabel: [null, []], + usePolygonLabelFunction: [null, []], + polygonLabel: [null, []], + polygonLabelFunction: [null, []], + showPolygonTooltip: [null, []], + showPolygonTooltipAction: [null, []], + autoClosePolygonTooltip: [null, []], + usePolygonTooltipFunction: [null, []], + polygonTooltipPattern: [null, []], + polygonTooltipFunction: [null, []], + polygonColor: [null, []], + polygonOpacity: [null, [Validators.min(0), Validators.max(1)]], + usePolygonColorFunction: [null, []], + polygonColorFunction: [null, []], + polygonStrokeColor: [null, []], + polygonStrokeOpacity: [null, [Validators.min(0), Validators.max(1)]], + polygonStrokeWeight: [null, [Validators.min(0)]], + usePolygonStrokeColorFunction: [null, []], + polygonStrokeColorFunction: [null, []] + }); + this.polygonSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + this.polygonSettingsFormGroup.get('showPolygon').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.polygonSettingsFormGroup.get('showPolygonLabel').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.polygonSettingsFormGroup.get('usePolygonLabelFunction').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.polygonSettingsFormGroup.get('showPolygonTooltip').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.polygonSettingsFormGroup.get('usePolygonTooltipFunction').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.polygonSettingsFormGroup.get('usePolygonColorFunction').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.polygonSettingsFormGroup.get('usePolygonStrokeColorFunction').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.updateValidators(false); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.polygonSettingsFormGroup.disable({emitEvent: false}); + } else { + this.polygonSettingsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: PolygonSettings): void { + this.modelValue = value; + this.polygonSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + this.updateValidators(false); + } + + public validate(c: FormControl) { + return this.polygonSettingsFormGroup.valid ? null : { + polygonSettings: { + valid: false, + }, + }; + } + + private updateModel() { + const value: PolygonSettings = this.polygonSettingsFormGroup.value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } + + private updateValidators(emitEvent?: boolean): void { + const showPolygon: boolean = this.polygonSettingsFormGroup.get('showPolygon').value; + const showPolygonLabel: boolean = this.polygonSettingsFormGroup.get('showPolygonLabel').value; + const usePolygonLabelFunction: boolean = this.polygonSettingsFormGroup.get('usePolygonLabelFunction').value; + const showPolygonTooltip: boolean = this.polygonSettingsFormGroup.get('showPolygonTooltip').value; + const usePolygonTooltipFunction: boolean = this.polygonSettingsFormGroup.get('usePolygonTooltipFunction').value; + const usePolygonColorFunction: boolean = this.polygonSettingsFormGroup.get('usePolygonColorFunction').value; + const usePolygonStrokeColorFunction: boolean = this.polygonSettingsFormGroup.get('usePolygonStrokeColorFunction').value; + + this.polygonSettingsFormGroup.disable({emitEvent: false}); + this.polygonSettingsFormGroup.get('showPolygon').enable({emitEvent: false}); + + if (showPolygon) { + this.polygonSettingsFormGroup.get('polygonKeyName').enable({emitEvent: false}); + this.polygonSettingsFormGroup.get('editablePolygon').enable({emitEvent: false}); + this.polygonSettingsFormGroup.get('showPolygonLabel').enable({emitEvent: false}); + this.polygonSettingsFormGroup.get('showPolygonTooltip').enable({emitEvent: false}); + this.polygonSettingsFormGroup.get('polygonColor').enable({emitEvent: false}); + this.polygonSettingsFormGroup.get('polygonOpacity').enable({emitEvent: false}); + this.polygonSettingsFormGroup.get('usePolygonColorFunction').enable({emitEvent: false}); + this.polygonSettingsFormGroup.get('polygonStrokeColor').enable({emitEvent: false}); + this.polygonSettingsFormGroup.get('polygonStrokeOpacity').enable({emitEvent: false}); + this.polygonSettingsFormGroup.get('polygonStrokeWeight').enable({emitEvent: false}); + this.polygonSettingsFormGroup.get('usePolygonStrokeColorFunction').enable({emitEvent: false}); + if (showPolygonLabel) { + this.polygonSettingsFormGroup.get('usePolygonLabelFunction').enable({emitEvent: false}); + if (usePolygonLabelFunction) { + this.polygonSettingsFormGroup.get('polygonLabelFunction').enable({emitEvent}); + this.polygonSettingsFormGroup.get('polygonLabel').disable({emitEvent}); + } else { + this.polygonSettingsFormGroup.get('polygonLabelFunction').disable({emitEvent}); + this.polygonSettingsFormGroup.get('polygonLabel').enable({emitEvent}); + } + } else { + this.polygonSettingsFormGroup.get('usePolygonLabelFunction').disable({emitEvent: false}); + this.polygonSettingsFormGroup.get('polygonLabelFunction').disable({emitEvent}); + this.polygonSettingsFormGroup.get('polygonLabel').disable({emitEvent}); + } + if (showPolygonTooltip) { + this.polygonSettingsFormGroup.get('showPolygonTooltipAction').enable({emitEvent}); + this.polygonSettingsFormGroup.get('autoClosePolygonTooltip').enable({emitEvent}); + this.polygonSettingsFormGroup.get('usePolygonTooltipFunction').enable({emitEvent: false}); + if (usePolygonTooltipFunction) { + this.polygonSettingsFormGroup.get('polygonTooltipFunction').enable({emitEvent}); + this.polygonSettingsFormGroup.get('polygonTooltipPattern').disable({emitEvent}); + } else { + this.polygonSettingsFormGroup.get('polygonTooltipFunction').disable({emitEvent}); + this.polygonSettingsFormGroup.get('polygonTooltipPattern').enable({emitEvent}); + } + } else { + this.polygonSettingsFormGroup.get('showPolygonTooltipAction').disable({emitEvent}); + this.polygonSettingsFormGroup.get('autoClosePolygonTooltip').disable({emitEvent}); + this.polygonSettingsFormGroup.get('usePolygonTooltipFunction').disable({emitEvent: false}); + this.polygonSettingsFormGroup.get('polygonTooltipFunction').disable({emitEvent}); + this.polygonSettingsFormGroup.get('polygonTooltipPattern').disable({emitEvent}); + } + if (usePolygonColorFunction) { + this.polygonSettingsFormGroup.get('polygonColorFunction').enable({emitEvent}); + } else { + this.polygonSettingsFormGroup.get('polygonColorFunction').disable({emitEvent}); + } + if (usePolygonStrokeColorFunction) { + this.polygonSettingsFormGroup.get('polygonStrokeColorFunction').enable({emitEvent}); + } else { + this.polygonSettingsFormGroup.get('polygonStrokeColorFunction').disable({emitEvent}); + } + } + this.polygonSettingsFormGroup.updateValueAndValidity({emitEvent: false}); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-settings.component.html new file mode 100644 index 0000000000..eb52ea2d8d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-settings.component.html @@ -0,0 +1,32 @@ + +
+
+ widgets.maps.route-map-settings +
+ + widgets.maps.stroke-weight + + + + widgets.maps.stroke-opacity + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-settings.component.ts new file mode 100644 index 0000000000..5d9ed7774a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-settings.component.ts @@ -0,0 +1,115 @@ +/// +/// Copyright © 2016-2022 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 { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { PolylineSettings } from '@home/components/widget/lib/maps/map-models'; +import { WidgetService } from '@core/http/widget.service'; + +@Component({ + selector: 'tb-route-map-settings', + templateUrl: './route-map-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RouteMapSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => RouteMapSettingsComponent), + multi: true + } + ] +}) +export class RouteMapSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { + + @Input() + disabled: boolean; + + private modelValue: PolylineSettings; + + private propagateChange = null; + + public routeMapSettingsFormGroup: FormGroup; + + constructor(protected store: Store, + private translate: TranslateService, + private widgetService: WidgetService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.routeMapSettingsFormGroup = this.fb.group({ + strokeWeight: [null, [Validators.min(0)]], + strokeOpacity: [null, [Validators.min(0), Validators.max(1)]] + }); + this.routeMapSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.routeMapSettingsFormGroup.disable({emitEvent: false}); + } else { + this.routeMapSettingsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: PolylineSettings): void { + this.modelValue = value; + this.routeMapSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + } + + public validate(c: FormControl) { + return this.routeMapSettingsFormGroup.valid ? null : { + routeMapSettings: { + valid: false, + }, + }; + } + + private updateModel() { + const value: PolylineSettings = this.routeMapSettingsFormGroup.value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-widget-settings.component.html new file mode 100644 index 0000000000..e453c69407 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-widget-settings.component.html @@ -0,0 +1,24 @@ + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-widget-settings.component.ts new file mode 100644 index 0000000000..0b45405dcf --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/route-map-widget-settings.component.ts @@ -0,0 +1,63 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { defaultMapSettings } from 'src/app/modules/home/components/widget/lib/maps/map-models'; + +@Component({ + selector: 'tb-route-map-widget-settings', + templateUrl: './route-map-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class RouteMapWidgetSettingsComponent extends WidgetSettingsComponent { + + routeMapWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.routeMapWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + ...defaultMapSettings + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.routeMapWidgetSettingsForm = this.fb.group({ + mapSettings: [settings.mapSettings, []] + }); + } + + protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { + return { + mapSettings: settings + }; + } + + protected prepareOutputSettings(settings: any): WidgetSettings { + return settings.mapSettings; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/tencent-map-provider-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/tencent-map-provider-settings.component.html new file mode 100644 index 0000000000..2132ea69cb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/tencent-map-provider-settings.component.html @@ -0,0 +1,31 @@ + +
+ + widgets.maps.tencent-maps-api-key + + + + widgets.maps.default-map-type + + + {{tencentMapTypeTranslations.get(type) | translate}} + + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/tencent-map-provider-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/tencent-map-provider-settings.component.ts new file mode 100644 index 0000000000..b52bce6ff2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/tencent-map-provider-settings.component.ts @@ -0,0 +1,122 @@ +/// +/// Copyright © 2016-2022 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 { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { + TencentMapProviderSettings, + TencentMapType, + tencentMapTypeProviderTranslationMap +} from '@home/components/widget/lib/maps/map-models'; + +@Component({ + selector: 'tb-tencent-map-provider-settings', + templateUrl: './tencent-map-provider-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TencentMapProviderSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TencentMapProviderSettingsComponent), + multi: true + } + ] +}) +export class TencentMapProviderSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { + + @Input() + disabled: boolean; + + private modelValue: TencentMapProviderSettings; + + private propagateChange = null; + + public providerSettingsFormGroup: FormGroup; + + tencentMapTypes = Object.values(TencentMapType); + + tencentMapTypeTranslations = tencentMapTypeProviderTranslationMap; + + constructor(protected store: Store, + private translate: TranslateService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.providerSettingsFormGroup = this.fb.group({ + tmApiKey: [null, [Validators.required]], + tmDefaultMapType: [null, [Validators.required]] + }); + this.providerSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.providerSettingsFormGroup.disable({emitEvent: false}); + } else { + this.providerSettingsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: TencentMapProviderSettings): void { + this.modelValue = value; + this.providerSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + } + + public validate(c: FormControl) { + return this.providerSettingsFormGroup.valid ? null : { + tencentMapProviderSettings: { + valid: false, + }, + }; + } + + private updateModel() { + const value: TencentMapProviderSettings = this.providerSettingsFormGroup.value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-common-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-common-settings.component.html new file mode 100644 index 0000000000..7173ab31c5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-common-settings.component.html @@ -0,0 +1,94 @@ + +
+
+ widgets.maps.trip-animation-settings + + widgets.maps.normalization-step + + +
+ + + + +
+
+ widgets.maps.tooltip + + + + + {{ 'widgets.maps.show-tooltip' | translate }} + + + + widget-config.advanced-settings + + + +
+ + + + + + widgets.maps.tooltip-opacity + + +
+ + {{ 'widgets.maps.auto-close-tooltip' | translate }} + + + {{ 'widgets.maps.use-tooltip-function' | translate }} + + + + + +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-common-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-common-settings.component.ts new file mode 100644 index 0000000000..d144b36b0b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-common-settings.component.ts @@ -0,0 +1,173 @@ +/// +/// Copyright © 2016-2022 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 { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { TripAnimationCommonSettings } from '@home/components/widget/lib/maps/map-models'; +import { Widget } from '@shared/models/widget.models'; +import { WidgetService } from '@core/http/widget.service'; + +@Component({ + selector: 'tb-trip-animation-common-settings', + templateUrl: './trip-animation-common-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TripAnimationCommonSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TripAnimationCommonSettingsComponent), + multi: true + } + ] +}) +export class TripAnimationCommonSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { + + @Input() + disabled: boolean; + + @Input() + widget: Widget; + + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + + private modelValue: TripAnimationCommonSettings; + + private propagateChange = null; + + public tripAnimationCommonSettingsFormGroup: FormGroup; + + constructor(protected store: Store, + private translate: TranslateService, + private widgetService: WidgetService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.tripAnimationCommonSettingsFormGroup = this.fb.group({ + normalizationStep: [null, [Validators.min(1)]], + latKeyName: [null, [Validators.required]], + lngKeyName: [null, [Validators.required]], + showTooltip: [null, []], + tooltipColor: [null, []], + tooltipFontColor: [null, []], + tooltipOpacity: [null, [Validators.min(0), Validators.max(1)]], + autocloseTooltip: [null, []], + useTooltipFunction: [null, []], + tooltipPattern: [null, []], + tooltipFunction: [null, []], + }); + this.tripAnimationCommonSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + this.tripAnimationCommonSettingsFormGroup.get('showTooltip').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.tripAnimationCommonSettingsFormGroup.get('useTooltipFunction').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.updateValidators(false); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.tripAnimationCommonSettingsFormGroup.disable({emitEvent: false}); + } else { + this.tripAnimationCommonSettingsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: TripAnimationCommonSettings): void { + this.modelValue = value; + this.tripAnimationCommonSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + this.updateValidators(false); + } + + public validate(c: FormControl) { + return this.tripAnimationCommonSettingsFormGroup.valid ? null : { + tripAnimationCommonSettings: { + valid: false, + }, + }; + } + + private updateModel() { + const value: TripAnimationCommonSettings = this.tripAnimationCommonSettingsFormGroup.value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } + + private updateValidators(emitEvent?: boolean): void { + const showTooltip: boolean = this.tripAnimationCommonSettingsFormGroup.get('showTooltip').value; + const useTooltipFunction: boolean = this.tripAnimationCommonSettingsFormGroup.get('useTooltipFunction').value; + if (showTooltip) { + this.tripAnimationCommonSettingsFormGroup.get('tooltipColor').enable({emitEvent}); + this.tripAnimationCommonSettingsFormGroup.get('tooltipFontColor').enable({emitEvent}); + this.tripAnimationCommonSettingsFormGroup.get('tooltipOpacity').enable({emitEvent}); + this.tripAnimationCommonSettingsFormGroup.get('autocloseTooltip').enable({emitEvent}); + this.tripAnimationCommonSettingsFormGroup.get('useTooltipFunction').enable({emitEvent: false}); + if (useTooltipFunction) { + this.tripAnimationCommonSettingsFormGroup.get('tooltipFunction').enable({emitEvent}); + this.tripAnimationCommonSettingsFormGroup.get('tooltipPattern').disable({emitEvent}); + } else { + this.tripAnimationCommonSettingsFormGroup.get('tooltipFunction').disable({emitEvent}); + this.tripAnimationCommonSettingsFormGroup.get('tooltipPattern').enable({emitEvent}); + } + } else { + this.tripAnimationCommonSettingsFormGroup.get('tooltipColor').disable({emitEvent}); + this.tripAnimationCommonSettingsFormGroup.get('tooltipFontColor').disable({emitEvent}); + this.tripAnimationCommonSettingsFormGroup.get('tooltipOpacity').disable({emitEvent}); + this.tripAnimationCommonSettingsFormGroup.get('autocloseTooltip').disable({emitEvent}); + this.tripAnimationCommonSettingsFormGroup.get('useTooltipFunction').disable({emitEvent: false}); + this.tripAnimationCommonSettingsFormGroup.get('tooltipFunction').disable({emitEvent}); + this.tripAnimationCommonSettingsFormGroup.get('tooltipPattern').disable({emitEvent}); + } + this.tripAnimationCommonSettingsFormGroup.get('tooltipColor').updateValueAndValidity({emitEvent: false}); + this.tripAnimationCommonSettingsFormGroup.get('tooltipFontColor').updateValueAndValidity({emitEvent: false}); + this.tripAnimationCommonSettingsFormGroup.get('tooltipOpacity').updateValueAndValidity({emitEvent: false}); + this.tripAnimationCommonSettingsFormGroup.get('autocloseTooltip').updateValueAndValidity({emitEvent: false}); + this.tripAnimationCommonSettingsFormGroup.get('useTooltipFunction').updateValueAndValidity({emitEvent: false}); + this.tripAnimationCommonSettingsFormGroup.get('tooltipFunction').updateValueAndValidity({emitEvent: false}); + this.tripAnimationCommonSettingsFormGroup.get('tooltipPattern').updateValueAndValidity({emitEvent: false}); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-marker-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-marker-settings.component.html new file mode 100644 index 0000000000..6cbeeb2b8b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-marker-settings.component.html @@ -0,0 +1,97 @@ + +
+
+ widgets.maps.markers-settings + + widgets.maps.rotation-angle + + +
+ widgets.maps.label + + + + + {{ 'widgets.maps.show-label' | translate }} + + + + widget-config.advanced-settings + + + + + {{ 'widgets.maps.use-label-function' | translate }} + + + + + + + +
+
+ widgets.maps.marker-image + + + + + {{ 'widgets.maps.use-marker-image-function' | translate }} + + + + widget-config.advanced-settings + + + + + + + widgets.maps.custom-marker-image-size + + + + + + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-marker-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-marker-settings.component.ts new file mode 100644 index 0000000000..5f959c4e5c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-marker-settings.component.ts @@ -0,0 +1,175 @@ +/// +/// Copyright © 2016-2022 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 { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { TripAnimationMarkerSettings } from '@home/components/widget/lib/maps/map-models'; +import { WidgetService } from '@core/http/widget.service'; + +@Component({ + selector: 'tb-trip-animation-marker-settings', + templateUrl: './trip-animation-marker-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TripAnimationMarkerSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TripAnimationMarkerSettingsComponent), + multi: true + } + ] +}) +export class TripAnimationMarkerSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { + + @Input() + disabled: boolean; + + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + + private modelValue: TripAnimationMarkerSettings; + + private propagateChange = null; + + public tripAnimationMarkerSettingsFormGroup: FormGroup; + + constructor(protected store: Store, + private translate: TranslateService, + private widgetService: WidgetService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.tripAnimationMarkerSettingsFormGroup = this.fb.group({ + rotationAngle: [null, [Validators.min(0), Validators.max(360)]], + showLabel: [null, []], + useLabelFunction: [null, []], + label: [null, []], + labelFunction: [null, []], + useMarkerImageFunction: [null, []], + markerImage: [null, []], + markerImageSize: [null, [Validators.min(1)]], + markerImageFunction: [null, []], + markerImages: [null, []] + }); + this.tripAnimationMarkerSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + this.tripAnimationMarkerSettingsFormGroup.get('showLabel').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.tripAnimationMarkerSettingsFormGroup.get('useLabelFunction').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.tripAnimationMarkerSettingsFormGroup.get('useMarkerImageFunction').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.updateValidators(false); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.tripAnimationMarkerSettingsFormGroup.disable({emitEvent: false}); + } else { + this.tripAnimationMarkerSettingsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: TripAnimationMarkerSettings): void { + this.modelValue = value; + this.tripAnimationMarkerSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + this.updateValidators(false); + } + + public validate(c: FormControl) { + return this.tripAnimationMarkerSettingsFormGroup.valid ? null : { + tripAnimationMarkerSettings: { + valid: false, + }, + }; + } + + private updateModel() { + const value: TripAnimationMarkerSettings = this.tripAnimationMarkerSettingsFormGroup.value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } + + private updateValidators(emitEvent?: boolean): void { + const showLabel: boolean = this.tripAnimationMarkerSettingsFormGroup.get('showLabel').value; + const useLabelFunction: boolean = this.tripAnimationMarkerSettingsFormGroup.get('useLabelFunction').value; + const useMarkerImageFunction: boolean = this.tripAnimationMarkerSettingsFormGroup.get('useMarkerImageFunction').value; + if (showLabel) { + this.tripAnimationMarkerSettingsFormGroup.get('useLabelFunction').enable({emitEvent: false}); + if (useLabelFunction) { + this.tripAnimationMarkerSettingsFormGroup.get('labelFunction').enable({emitEvent}); + this.tripAnimationMarkerSettingsFormGroup.get('label').disable({emitEvent}); + } else { + this.tripAnimationMarkerSettingsFormGroup.get('labelFunction').disable({emitEvent}); + this.tripAnimationMarkerSettingsFormGroup.get('label').enable({emitEvent}); + } + } else { + this.tripAnimationMarkerSettingsFormGroup.get('useLabelFunction').disable({emitEvent: false}); + this.tripAnimationMarkerSettingsFormGroup.get('labelFunction').disable({emitEvent}); + this.tripAnimationMarkerSettingsFormGroup.get('label').disable({emitEvent}); + } + if (useMarkerImageFunction) { + this.tripAnimationMarkerSettingsFormGroup.get('markerImageFunction').enable({emitEvent}); + this.tripAnimationMarkerSettingsFormGroup.get('markerImages').enable({emitEvent}); + this.tripAnimationMarkerSettingsFormGroup.get('markerImage').disable({emitEvent}); + this.tripAnimationMarkerSettingsFormGroup.get('markerImageSize').disable({emitEvent}); + } else { + this.tripAnimationMarkerSettingsFormGroup.get('markerImageFunction').disable({emitEvent}); + this.tripAnimationMarkerSettingsFormGroup.get('markerImages').disable({emitEvent}); + this.tripAnimationMarkerSettingsFormGroup.get('markerImage').enable({emitEvent}); + this.tripAnimationMarkerSettingsFormGroup.get('markerImageSize').enable({emitEvent}); + } + this.tripAnimationMarkerSettingsFormGroup.get('useLabelFunction').updateValueAndValidity({emitEvent: false}); + this.tripAnimationMarkerSettingsFormGroup.get('labelFunction').updateValueAndValidity({emitEvent: false}); + this.tripAnimationMarkerSettingsFormGroup.get('label').updateValueAndValidity({emitEvent: false}); + this.tripAnimationMarkerSettingsFormGroup.get('markerImageFunction').updateValueAndValidity({emitEvent: false}); + this.tripAnimationMarkerSettingsFormGroup.get('markerImages').updateValueAndValidity({emitEvent: false}); + this.tripAnimationMarkerSettingsFormGroup.get('markerImage').updateValueAndValidity({emitEvent: false}); + this.tripAnimationMarkerSettingsFormGroup.get('markerImageSize').updateValueAndValidity({emitEvent: false}); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-path-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-path-settings.component.html new file mode 100644 index 0000000000..c2bc5ed7ce --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-path-settings.component.html @@ -0,0 +1,116 @@ + +
+
+ widgets.maps.path-settings +
+ + + + widgets.maps.stroke-weight + + + + widgets.maps.stroke-opacity + + +
+ + + + + {{ 'widgets.maps.use-path-color-function' | translate }} + + + + widget-config.advanced-settings + + + + + + + +
+ widgets.maps.path-decorator + + + + + {{ 'widgets.maps.use-path-decorator' | translate }} + + + + widget-config.advanced-settings + + + +
+ + widgets.maps.decorator-symbol + + + {{polylineDecoratorSymbolTranslations.get(symbol) | translate}} + + + + + widgets.maps.decorator-symbol-size + + +
+
+ + {{ 'widgets.maps.use-path-decorator-custom-color' | translate }} + + + +
+
+ + widgets.maps.decorator-offset + + + + widgets.maps.end-decorator-offset + + + + widgets.maps.decorator-repeat + + +
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-path-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-path-settings.component.ts new file mode 100644 index 0000000000..8dc90b0882 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-path-settings.component.ts @@ -0,0 +1,187 @@ +/// +/// Copyright © 2016-2022 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 { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { + PolylineDecoratorSymbol, + polylineDecoratorSymbolTranslationMap, + PolylineSettings +} from '@home/components/widget/lib/maps/map-models'; +import { WidgetService } from '@core/http/widget.service'; + +@Component({ + selector: 'tb-trip-animation-path-settings', + templateUrl: './trip-animation-path-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TripAnimationPathSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TripAnimationPathSettingsComponent), + multi: true + } + ] +}) +export class TripAnimationPathSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { + + @Input() + disabled: boolean; + + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + + private modelValue: PolylineSettings; + + private propagateChange = null; + + public tripAnimationPathSettingsFormGroup: FormGroup; + + polylineDecoratorSymbols = Object.values(PolylineDecoratorSymbol); + + polylineDecoratorSymbolTranslations = polylineDecoratorSymbolTranslationMap; + + constructor(protected store: Store, + private translate: TranslateService, + private widgetService: WidgetService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.tripAnimationPathSettingsFormGroup = this.fb.group({ + color: [null, []], + strokeWeight: [null, [Validators.min(0)]], + strokeOpacity: [null, [Validators.min(0), Validators.max(1)]], + useColorFunction: [null, []], + colorFunction: [null, []], + usePolylineDecorator: [null, []], + decoratorSymbol: [null, []], + decoratorSymbolSize: [null, [Validators.min(1)]], + useDecoratorCustomColor: [null, []], + decoratorCustomColor: [null, []], + decoratorOffset: [null, []], + endDecoratorOffset: [null, []], + decoratorRepeat: [null, []], + }); + this.tripAnimationPathSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + this.tripAnimationPathSettingsFormGroup.get('useColorFunction').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.tripAnimationPathSettingsFormGroup.get('usePolylineDecorator').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.tripAnimationPathSettingsFormGroup.get('useDecoratorCustomColor').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.updateValidators(false); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.tripAnimationPathSettingsFormGroup.disable({emitEvent: false}); + } else { + this.tripAnimationPathSettingsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: PolylineSettings): void { + this.modelValue = value; + this.tripAnimationPathSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + this.updateValidators(false); + } + + public validate(c: FormControl) { + return this.tripAnimationPathSettingsFormGroup.valid ? null : { + tripAnimationPathSettings: { + valid: false, + }, + }; + } + + private updateModel() { + const value: PolylineSettings = this.tripAnimationPathSettingsFormGroup.value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } + + private updateValidators(emitEvent?: boolean): void { + const useColorFunction: boolean = this.tripAnimationPathSettingsFormGroup.get('useColorFunction').value; + const usePolylineDecorator: boolean = this.tripAnimationPathSettingsFormGroup.get('usePolylineDecorator').value; + const useDecoratorCustomColor: boolean = this.tripAnimationPathSettingsFormGroup.get('useDecoratorCustomColor').value; + if (useColorFunction) { + this.tripAnimationPathSettingsFormGroup.get('colorFunction').enable({emitEvent}); + } else { + this.tripAnimationPathSettingsFormGroup.get('colorFunction').disable({emitEvent}); + } + if (usePolylineDecorator) { + this.tripAnimationPathSettingsFormGroup.get('decoratorSymbol').enable({emitEvent}); + this.tripAnimationPathSettingsFormGroup.get('decoratorSymbolSize').enable({emitEvent}); + this.tripAnimationPathSettingsFormGroup.get('useDecoratorCustomColor').enable({emitEvent: false}); + if (useDecoratorCustomColor) { + this.tripAnimationPathSettingsFormGroup.get('decoratorCustomColor').enable({emitEvent}); + } else { + this.tripAnimationPathSettingsFormGroup.get('decoratorCustomColor').disable({emitEvent}); + } + this.tripAnimationPathSettingsFormGroup.get('decoratorOffset').enable({emitEvent}); + this.tripAnimationPathSettingsFormGroup.get('endDecoratorOffset').enable({emitEvent}); + this.tripAnimationPathSettingsFormGroup.get('decoratorRepeat').enable({emitEvent}); + } else { + this.tripAnimationPathSettingsFormGroup.get('decoratorSymbol').disable({emitEvent}); + this.tripAnimationPathSettingsFormGroup.get('decoratorSymbolSize').disable({emitEvent}); + this.tripAnimationPathSettingsFormGroup.get('useDecoratorCustomColor').disable({emitEvent: false}); + this.tripAnimationPathSettingsFormGroup.get('decoratorCustomColor').disable({emitEvent}); + this.tripAnimationPathSettingsFormGroup.get('decoratorOffset').disable({emitEvent}); + this.tripAnimationPathSettingsFormGroup.get('endDecoratorOffset').disable({emitEvent}); + this.tripAnimationPathSettingsFormGroup.get('decoratorRepeat').disable({emitEvent}); + } + this.tripAnimationPathSettingsFormGroup.get('colorFunction').updateValueAndValidity({emitEvent: false}); + this.tripAnimationPathSettingsFormGroup.get('decoratorSymbol').updateValueAndValidity({emitEvent: false}); + this.tripAnimationPathSettingsFormGroup.get('decoratorSymbolSize').updateValueAndValidity({emitEvent: false}); + this.tripAnimationPathSettingsFormGroup.get('useDecoratorCustomColor').updateValueAndValidity({emitEvent: false}); + this.tripAnimationPathSettingsFormGroup.get('decoratorCustomColor').updateValueAndValidity({emitEvent: false}); + this.tripAnimationPathSettingsFormGroup.get('decoratorOffset').updateValueAndValidity({emitEvent: false}); + this.tripAnimationPathSettingsFormGroup.get('endDecoratorOffset').updateValueAndValidity({emitEvent: false}); + this.tripAnimationPathSettingsFormGroup.get('decoratorRepeat').updateValueAndValidity({emitEvent: false}); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-point-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-point-settings.component.html new file mode 100644 index 0000000000..e48c690823 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-point-settings.component.html @@ -0,0 +1,94 @@ + +
+
+ widgets.maps.points-settings + + + + + {{ 'widgets.maps.show-points' | translate }} + + + + widget-config.advanced-settings + + + +
+ + + + widgets.maps.point-size + + +
+ + + + + {{ 'widgets.maps.use-point-color-function' | translate }} + + + + widget-config.advanced-settings + + + + + + + + + + + + {{ 'widgets.maps.use-point-as-anchor' | translate }} + + + + widget-config.advanced-settings + + + + + + + + + {{ 'widgets.maps.independent-point-tooltip' | translate }} + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-point-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-point-settings.component.ts new file mode 100644 index 0000000000..571c20cfca --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-point-settings.component.ts @@ -0,0 +1,163 @@ +/// +/// Copyright © 2016-2022 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 { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { + PointsSettings, + PolylineDecoratorSymbol, + polylineDecoratorSymbolTranslationMap, + PolylineSettings +} from '@home/components/widget/lib/maps/map-models'; +import { WidgetService } from '@core/http/widget.service'; + +@Component({ + selector: 'tb-trip-animation-point-settings', + templateUrl: './trip-animation-point-settings.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TripAnimationPointSettingsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TripAnimationPointSettingsComponent), + multi: true + } + ] +}) +export class TripAnimationPointSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { + + @Input() + disabled: boolean; + + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + + private modelValue: PointsSettings; + + private propagateChange = null; + + public tripAnimationPointSettingsFormGroup: FormGroup; + + constructor(protected store: Store, + private translate: TranslateService, + private widgetService: WidgetService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.tripAnimationPointSettingsFormGroup = this.fb.group({ + showPoints: [null, []], + pointColor: [null, []], + pointSize: [null, [Validators.min(1)]], + useColorPointFunction: [null, []], + colorPointFunction: [null, []], + usePointAsAnchor: [null, []], + pointAsAnchorFunction: [null, []], + pointTooltipOnRightPanel: [null, []] + }); + this.tripAnimationPointSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + this.tripAnimationPointSettingsFormGroup.get('showPoints').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.tripAnimationPointSettingsFormGroup.get('useColorPointFunction').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.tripAnimationPointSettingsFormGroup.get('usePointAsAnchor').valueChanges.subscribe(() => { + this.updateValidators(true); + }); + this.updateValidators(false); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.tripAnimationPointSettingsFormGroup.disable({emitEvent: false}); + } else { + this.tripAnimationPointSettingsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: PointsSettings): void { + this.modelValue = value; + this.tripAnimationPointSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + this.updateValidators(false); + } + + public validate(c: FormControl) { + return this.tripAnimationPointSettingsFormGroup.valid ? null : { + tripAnimationPointSettings: { + valid: false, + }, + }; + } + + private updateModel() { + const value: PointsSettings = this.tripAnimationPointSettingsFormGroup.value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } + + private updateValidators(emitEvent?: boolean): void { + const showPoints: boolean = this.tripAnimationPointSettingsFormGroup.get('showPoints').value; + const useColorPointFunction: boolean = this.tripAnimationPointSettingsFormGroup.get('useColorPointFunction').value; + const usePointAsAnchor: boolean = this.tripAnimationPointSettingsFormGroup.get('usePointAsAnchor').value; + + this.tripAnimationPointSettingsFormGroup.disable({emitEvent: false}); + this.tripAnimationPointSettingsFormGroup.get('showPoints').enable({emitEvent: false}); + + if (showPoints) { + this.tripAnimationPointSettingsFormGroup.get('pointColor').enable({emitEvent: false}); + this.tripAnimationPointSettingsFormGroup.get('pointSize').enable({emitEvent: false}); + this.tripAnimationPointSettingsFormGroup.get('useColorPointFunction').enable({emitEvent: false}); + if (useColorPointFunction) { + this.tripAnimationPointSettingsFormGroup.get('colorPointFunction').enable({emitEvent: false}); + } + this.tripAnimationPointSettingsFormGroup.get('usePointAsAnchor').enable({emitEvent: false}); + if (usePointAsAnchor) { + this.tripAnimationPointSettingsFormGroup.get('pointAsAnchorFunction').enable({emitEvent: false}); + } + this.tripAnimationPointSettingsFormGroup.get('pointTooltipOnRightPanel').enable({emitEvent: false}); + } + this.tripAnimationPointSettingsFormGroup.updateValueAndValidity({emitEvent: false}); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-widget-settings.component.html new file mode 100644 index 0000000000..6a5e0d66e0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-widget-settings.component.html @@ -0,0 +1,45 @@ + +
+ + + + + + + + + + + + + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-widget-settings.component.ts new file mode 100644 index 0000000000..5e28fcd1ca --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-widget-settings.component.ts @@ -0,0 +1,101 @@ +/// +/// Copyright © 2016-2022 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 { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + CircleSettings, + defaultTripAnimationSettings, + MapProviderSettings, + PointsSettings, + PolygonSettings, + PolylineSettings, + TripAnimationCommonSettings, + TripAnimationMarkerSettings +} from 'src/app/modules/home/components/widget/lib/maps/map-models'; +import { extractType } from '@core/utils'; +import { keys } from 'ts-transformer-keys'; + +@Component({ + selector: 'tb-trip-animation-widget-settings', + templateUrl: './trip-animation-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class TripAnimationWidgetSettingsComponent extends WidgetSettingsComponent { + + tripAnimationWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.tripAnimationWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + ...defaultTripAnimationSettings + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.tripAnimationWidgetSettingsForm = this.fb.group({ + mapProviderSettings: [settings.mapProviderSettings, []], + commonMapSettings: [settings.commonMapSettings, []], + markersSettings: [settings.markersSettings, []], + pathSettings: [settings.pathSettings, []], + pointSettings: [settings.pointSettings, []], + polygonSettings: [settings.polygonSettings, []], + circleSettings: [settings.circleSettings, []] + }); + } + + protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { + const mapProviderSettings = extractType(settings, keys()); + const commonMapSettings = extractType(settings, keys()); + const markersSettings = extractType(settings, keys()); + const pathSettings = extractType(settings, keys()); + const pointSettings = extractType(settings, keys()); + const polygonSettings = extractType(settings, keys()); + const circleSettings = extractType(settings, keys()); + return { + mapProviderSettings, + commonMapSettings, + markersSettings, + pathSettings, + pointSettings, + polygonSettings, + circleSettings + }; + } + + protected prepareOutputSettings(settings: any): WidgetSettings { + return { + ...settings.mapProviderSettings, + ...settings.commonMapSettings, + ...settings.markersSettings, + ...settings.pathSettings, + ...settings.pointSettings, + ...settings.polygonSettings, + ...settings.circleSettings, + }; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index d982dba872..17875262c0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -211,6 +211,54 @@ import { import { UpdateMultipleAttributesKeySettingsComponent } from '@home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component'; +import { + OpenStreetMapProviderSettingsComponent +} from '@home/components/widget/lib/settings/map/openstreet-map-provider-settings.component'; +import { MapProviderSettingsComponent } from '@home/components/widget/lib/settings/map/map-provider-settings.component'; +import { MapSettingsComponent } from '@home/components/widget/lib/settings/map/map-settings.component'; +import { MapWidgetSettingsComponent } from '@home/components/widget/lib/settings/map/map-widget-settings.component'; +import { + GoogleMapProviderSettingsComponent +} from '@home/components/widget/lib/settings/map/google-map-provider-settings.component'; +import { + HereMapProviderSettingsComponent +} from '@home/components/widget/lib/settings/map/here-map-provider-settings.component'; +import { + TencentMapProviderSettingsComponent +} from '@home/components/widget/lib/settings/map/tencent-map-provider-settings.component'; +import { + ImageMapProviderSettingsComponent +} from '@home/components/widget/lib/settings/map/image-map-provider-settings.component'; +import { + DatasourcesKeyAutocompleteComponent +} from '@home/components/widget/lib/settings/map/datasources-key-autocomplete.component'; +import { CommonMapSettingsComponent } from '@home/components/widget/lib/settings/map/common-map-settings.component'; +import { MarkersSettingsComponent } from '@home/components/widget/lib/settings/map/markers-settings.component'; +import { PolygonSettingsComponent } from '@home/components/widget/lib/settings/map/polygon-settings.component'; +import { CircleSettingsComponent } from '@home/components/widget/lib/settings/map/circle-settings.component'; +import { + MarkerClusteringSettingsComponent +} from '@home/components/widget/lib/settings/map/marker-clustering-settings.component'; +import { MapEditorSettingsComponent } from '@home/components/widget/lib/settings/map/map-editor-settings.component'; +import { RouteMapSettingsComponent } from '@home/components/widget/lib/settings/map/route-map-settings.component'; +import { + RouteMapWidgetSettingsComponent +} from '@home/components/widget/lib/settings/map/route-map-widget-settings.component'; +import { + TripAnimationWidgetSettingsComponent +} from '@home/components/widget/lib/settings/map/trip-animation-widget-settings.component'; +import { + TripAnimationCommonSettingsComponent +} from '@home/components/widget/lib/settings/map/trip-animation-common-settings.component'; +import { + TripAnimationMarkerSettingsComponent +} from '@home/components/widget/lib/settings/map/trip-animation-marker-settings.component'; +import { + TripAnimationPathSettingsComponent +} from '@home/components/widget/lib/settings/map/trip-animation-path-settings.component'; +import { + TripAnimationPointSettingsComponent +} from '@home/components/widget/lib/settings/map/trip-animation-point-settings.component'; @NgModule({ declarations: [ @@ -287,7 +335,29 @@ import { PhotoCameraInputWidgetSettingsComponent, UpdateMultipleAttributesWidgetSettingsComponent, DataKeySelectOptionComponent, - UpdateMultipleAttributesKeySettingsComponent + UpdateMultipleAttributesKeySettingsComponent, + GoogleMapProviderSettingsComponent, + OpenStreetMapProviderSettingsComponent, + HereMapProviderSettingsComponent, + ImageMapProviderSettingsComponent, + TencentMapProviderSettingsComponent, + MapProviderSettingsComponent, + DatasourcesKeyAutocompleteComponent, + CommonMapSettingsComponent, + MarkersSettingsComponent, + PolygonSettingsComponent, + CircleSettingsComponent, + MarkerClusteringSettingsComponent, + MapEditorSettingsComponent, + RouteMapSettingsComponent, + MapSettingsComponent, + TripAnimationCommonSettingsComponent, + TripAnimationMarkerSettingsComponent, + TripAnimationPathSettingsComponent, + TripAnimationPointSettingsComponent, + MapWidgetSettingsComponent, + RouteMapWidgetSettingsComponent, + TripAnimationWidgetSettingsComponent ], imports: [ CommonModule, @@ -368,7 +438,29 @@ import { PhotoCameraInputWidgetSettingsComponent, UpdateMultipleAttributesWidgetSettingsComponent, DataKeySelectOptionComponent, - UpdateMultipleAttributesKeySettingsComponent + UpdateMultipleAttributesKeySettingsComponent, + GoogleMapProviderSettingsComponent, + OpenStreetMapProviderSettingsComponent, + HereMapProviderSettingsComponent, + ImageMapProviderSettingsComponent, + TencentMapProviderSettingsComponent, + MapProviderSettingsComponent, + DatasourcesKeyAutocompleteComponent, + CommonMapSettingsComponent, + MarkersSettingsComponent, + PolygonSettingsComponent, + CircleSettingsComponent, + MarkerClusteringSettingsComponent, + MapEditorSettingsComponent, + RouteMapSettingsComponent, + MapSettingsComponent, + TripAnimationCommonSettingsComponent, + TripAnimationMarkerSettingsComponent, + TripAnimationPathSettingsComponent, + TripAnimationPointSettingsComponent, + MapWidgetSettingsComponent, + RouteMapWidgetSettingsComponent, + TripAnimationWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -432,5 +524,8 @@ export const widgetSettingsComponentsMap: {[key: string]: Type .tb-settings { margin-top: 0; padding: 0 16px 8px; } diff --git a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts b/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts index 7db38e2416..524e7f8ca5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts @@ -27,7 +27,11 @@ import { SecurityContext, ViewChild } from '@angular/core'; -import { MapProviders, TripAnimationSettings } from '@home/components/widget/lib/maps/map-models'; +import { + defaultTripAnimationSettings, + MapProviders, + WidgetUnitedTripAnimationSettings +} from '@home/components/widget/lib/maps/map-models'; import { addCondition, addGroupInfo, addToSchema, initSchema } from '@app/core/schema-utils'; import { mapCircleSchema, @@ -86,7 +90,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy formattedCurrentPosition: FormattedData[] = []; formattedLatestData: FormattedData[] = []; widgetConfig: WidgetConfig; - settings: TripAnimationSettings; + settings: WidgetUnitedTripAnimationSettings; mainTooltips = []; visibleTooltip = false; activeTrip: FormattedData; @@ -116,19 +120,16 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy ngOnInit(): void { this.widgetConfig = this.ctx.widgetConfig; - const settings = { - normalizationStep: 1000, - showLabel: false, + this.settings = { buttonColor: tinycolor(this.widgetConfig.color).setAlpha(0.54).toRgbString(), - disabledButtonColor: tinycolor(this.widgetConfig.color).setAlpha(0.3).toRgbString(), - rotationAngle: 0 + ...defaultTripAnimationSettings, + ...this.ctx.settings }; - this.settings = { ...settings, ...this.ctx.settings }; this.useAnchors = this.settings.showPoints && this.settings.usePointAsAnchor; - this.settings.pointAsAnchorFunction = parseFunction(this.settings.pointAsAnchorFunction, ['data', 'dsData', 'dsIndex']); - this.settings.tooltipFunction = parseFunction(this.settings.tooltipFunction, ['data', 'dsData', 'dsIndex']); - this.settings.labelFunction = parseFunction(this.settings.labelFunction, ['data', 'dsData', 'dsIndex']); - this.settings.colorPointFunction = parseFunction(this.settings.colorPointFunction, ['data', 'dsData', 'dsIndex']); + this.settings.parsedPointAsAnchorFunction = parseFunction(this.settings.pointAsAnchorFunction, ['data', 'dsData', 'dsIndex']); + this.settings.parsedTooltipFunction = parseFunction(this.settings.tooltipFunction, ['data', 'dsData', 'dsIndex']); + this.settings.parsedLabelFunction = parseFunction(this.settings.labelFunction, ['data', 'dsData', 'dsIndex']); + this.settings.parsedColorPointFunction = parseFunction(this.settings.colorPointFunction, ['data', 'dsData', 'dsIndex']); this.normalizationStep = this.settings.normalizationStep; const subscription = this.ctx.defaultSubscription; subscription.callbacks.onDataUpdated = () => { @@ -212,7 +213,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy this.calcLabel(currentPosition); this.calcMainTooltip(currentPosition); if (this.mapWidget && this.mapWidget.map && this.mapWidget.map.map) { - this.mapWidget.map.updateFromData(true, this.settings.showPolygon, currentPosition, this.formattedInterpolatedTimeData, (trip) => { + this.mapWidget.map.updateFromData(true, currentPosition, this.formattedInterpolatedTimeData, (trip) => { this.activeTrip = trip; this.timeUpdated(this.currentTime); this.cd.markForCheck(); @@ -262,7 +263,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy if (this.useAnchors) { const anchorDate = Object.entries(_.union(this.interpolatedTimeData)[0]); this.anchors = anchorDate - .filter((data: [string, FormattedData], tsIndex) => safeExecute(this.settings.pointAsAnchorFunction, [data[1], + .filter((data: [string, FormattedData], tsIndex) => safeExecute(this.settings.parsedPointAsAnchorFunction, [data[1], this.formattedInterpolatedTimeData.map(ds => ds[tsIndex]), data[1].dsIndex])) .map(data => parseInt(data[0], 10)); } @@ -271,7 +272,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy calcTooltip = (point: FormattedData, points: FormattedData[]): string => { const data = point ? point : this.activeTrip; const tooltipPattern: string = this.settings.useTooltipFunction ? - safeExecute(this.settings.tooltipFunction, + safeExecute(this.settings.parsedTooltipFunction, [data, points, point.dsIndex]) : this.settings.tooltipPattern; return parseWithTranslation.parseTemplate(tooltipPattern, data, true); } @@ -287,7 +288,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy calcLabel(points: FormattedData[]) { const data = points[this.activeTrip.dsIndex]; const labelText: string = this.settings.useLabelFunction ? - safeExecute(this.settings.labelFunction, [data, points, data.dsIndex]) : this.settings.label; + safeExecute(this.settings.parsedLabelFunction, [data, points, data.dsIndex]) : this.settings.label; this.label = this.sanitizer.bypassSecurityTrustHtml(parseWithTranslation.parseTemplate(labelText, data, true)); } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index 3b764f0cf0..b3df4de3be 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -499,7 +499,6 @@
{ this.updateModel(settings); @@ -187,6 +196,7 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On if (this.definedSettingsComponentRef) { this.definedSettingsComponentRef.destroy(); this.definedSettingsComponentRef = null; + this.definedSettingsComponent = null; } if (this.settingsDirective && this.settingsDirective.length) { const componentType = widgetSettingsComponentsMap[this.settingsDirective]; diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html index 9ad88f4ff3..ca75fa996c 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html @@ -74,7 +74,7 @@ [contentConvertFunction]="convertToBase64File" [accept]="getAcceptType()" [multipleFile]="entityForm.get('resourceType').value === resourceType.LWM2M_MODEL" - dropLabel="{{'resource.drop-file' | translate}}" + dropLabel="{{'resource.drop-resource-file-or' | translate}}" [existingFileName]="entityForm.get('fileName')?.value" (fileNameChanged)="entityForm?.get('fileName').patchValue($event)"> diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html index bae03691f6..4bb5e24bbb 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html @@ -122,7 +122,7 @@ formControlName="file" workFromFileObj="true" [required]="!entityForm.get('isURL').value" - dropLabel="{{'ota-update.drop-file' | translate}}"> + dropLabel="{{'ota-update.drop-package-file-or' | translate}}"> {{ 'ota-update.auto-generate-checksum' | translate }} diff --git a/ui-ngx/src/app/shared/components/color-input.component.html b/ui-ngx/src/app/shared/components/color-input.component.html index 765178022b..115f1a3dd4 100644 --- a/ui-ngx/src/app/shared/components/color-input.component.html +++ b/ui-ngx/src/app/shared/components/color-input.component.html @@ -24,7 +24,7 @@
- + +
diff --git a/ui-ngx/src/app/shared/components/file-input.component.scss b/ui-ngx/src/app/shared/components/file-input.component.scss index 9f6eff32fb..1156f46470 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.scss +++ b/ui-ngx/src/app/shared/components/file-input.component.scss @@ -61,19 +61,38 @@ $previewSize: 100px !default; position: relative; height: $previewSize; overflow: hidden; - border: dashed 2px; + border: 2px dashed rgba(0, 0, 0, 0.2); + border-radius: 4px; + box-sizing: border-box; - label { - display: flex; - flex-direction: column; - justify-content: center; + .upload-label { width: 100%; height: 100%; + padding: 0 16px; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; font-size: 16px; + color: rgba(0, 0, 0, 0.54); text-align: center; + .mat-icon { + margin-right: 17px; + } + } + } +} - @media #{$mat-gt-sm} { - font-size: 24px; +:host ::ng-deep { + button.browse-file { + padding: 0; + font-size: 16px; + span.mat-button-wrapper { + display: block; + label { + display: block; + cursor: pointer; + padding: 0 16px; } } } diff --git a/ui-ngx/src/app/shared/components/html.component.html b/ui-ngx/src/app/shared/components/html.component.html index 17d913b89d..48187ac2b9 100644 --- a/ui-ngx/src/app/shared/components/html.component.html +++ b/ui-ngx/src/app/shared/components/html.component.html @@ -19,7 +19,7 @@ tb-fullscreen [fullscreen]="fullscreen" fxLayout="column">
- +
-
+
diff --git a/ui-ngx/src/app/shared/components/html.component.scss b/ui-ngx/src/app/shared/components/html.component.scss index 585dca667a..50ebbcf867 100644 --- a/ui-ngx/src/app/shared/components/html.component.scss +++ b/ui-ngx/src/app/shared/components/html.component.scss @@ -32,13 +32,13 @@ width: 100%; min-width: 200px; height: 100%; - - &:not(.fill-height) { - min-height: 200px; - } } } + &:not(.tb-fullscreen) { + padding-bottom: 15px; + } + .tb-html-toolbar { & > * { &:not(:last-child) { diff --git a/ui-ngx/src/app/shared/components/html.component.ts b/ui-ngx/src/app/shared/components/html.component.ts index 696873507c..7378cca7ea 100644 --- a/ui-ngx/src/app/shared/components/html.component.ts +++ b/ui-ngx/src/app/shared/components/html.component.ts @@ -71,6 +71,8 @@ export class HtmlComponent implements OnInit, OnDestroy, ControlValueAccessor, V @Input() fillHeight: boolean; + @Input() minHeight = '200px'; + private requiredValue: boolean; get required(): boolean { return this.requiredValue; diff --git a/ui-ngx/src/app/shared/components/image-input.component.html b/ui-ngx/src/app/shared/components/image-input.component.html index 09904a2849..d9059eb3c2 100644 --- a/ui-ngx/src/app/shared/components/image-input.component.html +++ b/ui-ngx/src/app/shared/components/image-input.component.html @@ -16,30 +16,49 @@ -->
- +
-
-
{{ (disabled ? 'dashboard.empty-image' : 'dashboard.no-image') | translate }}
- +
+
+
+
{{ (disabled ? 'dashboard.empty-image' : 'dashboard.no-image') | translate }}
+ +
+
+ +
+
-
+
+
+ cloud_upload + image-input.drop-image-or + + +
+
+
-
- - -
dashboard.maximum-upload-file-size
diff --git a/ui-ngx/src/app/shared/components/image-input.component.scss b/ui-ngx/src/app/shared/components/image-input.component.scss index 858d38c8a5..7caa4c620c 100644 --- a/ui-ngx/src/app/shared/components/image-input.component.scss +++ b/ui-ngx/src/app/shared/components/image-input.component.scss @@ -15,7 +15,9 @@ */ @import "../../../scss/constants"; -$previewSize: 100px !default; +$containerHeight: 120px !default; +$previewContainerWidth: 168px !default; +$previewSize: 96px !default; :host { @@ -30,14 +32,35 @@ $previewSize: 100px !default; .tb-image-select-container { position: relative; width: 100%; + height: $containerHeight; + } + + .image-container { + position: relative; + float: left; + height: $containerHeight; + padding: 12px; + margin-right: 8px; + background: rgba(0, 0, 0, 0.03); + border-radius: 4px; + } + + .image-content-container { + background: #FFFFFF; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 4px; + padding-left: 8px; height: $previewSize; + &.no-padding { + padding-left: 0px; + } } .tb-image-preview { width: auto; - max-width: $previewSize - 2; + max-width: $previewSize - 2px; height: auto; - max-height: $previewSize - 2; + max-height: $previewSize - 2px; } .tb-image-preview-container { @@ -45,9 +68,9 @@ $previewSize: 100px !default; float: left; width: $previewSize; height: $previewSize; - margin-right: 12px; - vertical-align: top; - border: solid 1px; + margin-top: -1px; + margin-bottom: -1px; + border: 1px solid rgba(0, 0, 0, 0.54); div { width: 100%; @@ -67,14 +90,12 @@ $previewSize: 100px !default; .tb-image-clear-container { position: relative; float: right; - width: 48px; height: $previewSize; - } - - .tb-image-clear-btn { - position: absolute !important; - top: 50%; - transform: translate(0%, -50%) !important; + display: flex; + align-items: center; + &.full-height { + height: $containerHeight; + } } .file-input { @@ -83,21 +104,29 @@ $previewSize: 100px !default; .tb-flow-drop { position: relative; - height: $previewSize; + height: $containerHeight; overflow: hidden; - border: dashed 2px; + border: 2px dashed rgba(0, 0, 0, 0.2); + border-radius: 4px; + box-sizing: border-box; - label { - display: flex; - flex-direction: column; - justify-content: center; + &.float-left { + float: left; + } + + .upload-label { width: 100%; height: 100%; + padding: 0 16px; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; font-size: 16px; + color: rgba(0, 0, 0, 0.54); text-align: center; - - @media #{$mat-gt-sm} { - font-size: 24px; + .mat-icon { + margin-right: 17px; } } } @@ -106,3 +135,18 @@ $previewSize: 100px !default; margin-top: 8px; } } + +:host ::ng-deep { + button.browse-file { + padding: 0; + font-size: 16px; + span.mat-button-wrapper { + display: block; + label { + display: block; + cursor: pointer; + padding: 0 16px; + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/image-input.component.ts b/ui-ngx/src/app/shared/components/image-input.component.ts index 8a0bc96c3e..6e36f545df 100644 --- a/ui-ngx/src/app/shared/components/image-input.component.ts +++ b/ui-ngx/src/app/shared/components/image-input.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { AfterViewInit, Component, forwardRef, Input, OnDestroy, ViewChild } from '@angular/core'; +import { AfterViewInit, ChangeDetectorRef, Component, forwardRef, Input, OnDestroy, ViewChild } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -89,7 +89,8 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit, private sanitizer: DomSanitizer, private dialog: DialogService, private translate: TranslateService, - private fileSize: FileSizePipe) { + private fileSize: FileSizePipe, + private cd: ChangeDetectorRef) { super(store); } @@ -144,6 +145,7 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit, } private updateModel() { + this.cd.markForCheck(); this.propagateChange(this.imageUrl); } diff --git a/ui-ngx/src/app/shared/components/js-func.component.html b/ui-ngx/src/app/shared/components/js-func.component.html index 093bce582d..dc79290181 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.html +++ b/ui-ngx/src/app/shared/components/js-func.component.html @@ -20,9 +20,9 @@ [fullscreen]="fullscreen" fxLayout="column">
+ [ngClass]="{'tb-error': !disabled && (hasErrors || !functionValid || required && !modelValue), 'tb-required': !disabled && required}">{{functionTitle + ': f(' + functionArgsString + ')' }} + [ngClass]="{'tb-error': !disabled && (hasErrors || !functionValid || required && !modelValue), 'tb-required': !disabled && required}">{{'function ' + (functionName ? functionName : '') + '(' + functionArgsString + ') {'}}
-
+
diff --git a/ui-ngx/src/app/shared/components/js-func.component.scss b/ui-ngx/src/app/shared/components/js-func.component.scss index 6b11058333..0df86ce5fa 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.scss +++ b/ui-ngx/src/app/shared/components/js-func.component.scss @@ -44,10 +44,6 @@ width: 100%; min-width: 200px; height: 100%; - - &:not(.fill-height) { - min-height: 200px; - } } } diff --git a/ui-ngx/src/app/shared/components/js-func.component.ts b/ui-ngx/src/app/shared/components/js-func.component.ts index ce995c1f1c..740273ed87 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.ts +++ b/ui-ngx/src/app/shared/components/js-func.component.ts @@ -84,6 +84,8 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, @Input() fillHeight: boolean; + @Input() minHeight = '200px'; + @Input() editorCompleter: TbEditorCompleter; @Input() globalVariables: Array; diff --git a/ui-ngx/src/app/shared/components/json-content.component.html b/ui-ngx/src/app/shared/components/json-content.component.html index 8ca404f9da..1654785510 100644 --- a/ui-ngx/src/app/shared/components/json-content.component.html +++ b/ui-ngx/src/app/shared/components/json-content.component.html @@ -19,7 +19,7 @@ tb-fullscreen [fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column">
- + +
+
+
+
{{ 'image-input.no-images' | translate }}
+
+
+
+ cloud_upload + image-input.drop-images-or + + +
+
+
+ +
dashboard.maximum-upload-file-size
+ diff --git a/ui-ngx/src/app/shared/components/multiple-image-input.component.scss b/ui-ngx/src/app/shared/components/multiple-image-input.component.scss new file mode 100644 index 0000000000..a1594a69a4 --- /dev/null +++ b/ui-ngx/src/app/shared/components/multiple-image-input.component.scss @@ -0,0 +1,169 @@ +/** + * Copyright © 2016-2022 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 "../../../scss/constants"; + +$imagesContainerHeight: 106px !default; +$containerHeight: 120px !default; +$previewSize: 64px !default; + +.image-card { + margin-bottom: 8px; + &.image-dnd-placeholder { + height: 82px; + width: 146px; + border: 2px dashed rgba(0, 0, 0, 0.2); + background: rgba(0, 0, 0, 0.1); + border-radius: 4px; + } + &.image-dragging { + display: none !important; + } +} + + +.image-title { + font-size: 11px; + font-weight: 400; + line-height: 14px; + color: rgba(0, 0, 0, 0.6); + padding-bottom: 4px; +} + +.image-content-container { + background: #FFFFFF; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 4px; + height: $previewSize; +} + +.tb-image-preview { + width: auto; + max-width: $previewSize - 2px; + height: auto; + max-height: $previewSize - 2px; +} + +.tb-image-preview-container { + position: relative; + width: $previewSize; + height: $previewSize; + margin-top: -1px; + margin-bottom: -1px; + border: 1px solid rgba(0, 0, 0, 0.54); + + .tb-image-preview { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } +} + +.tb-image-action-container { + position: relative; + height: $previewSize - 2px; + display: flex; + align-items: center; + justify-content: center; + min-width: 40px; +} + +:host { + + .tb-container { + margin-top: 0; + label.tb-title { + display: block; + padding-bottom: 8px; + } + } + + .tb-image-select-container { + position: relative; + width: 100%; + } + + .images-container { + padding: 12px 12px 4px; + background: rgba(0, 0, 0, 0.03); + border-radius: 4px; + flex-wrap: wrap; + margin-bottom: 8px; + &.no-images { + height: $imagesContainerHeight; + padding-bottom: 12px; + align-items: center; + justify-content: center; + } + } + + .no-images-prompt { + font-size: 18px; + color: rgba(0, 0, 0, 0.54); + } + + .file-input { + display: none; + } + + .tb-flow-drop { + position: relative; + height: $containerHeight; + overflow: hidden; + border: 2px dashed rgba(0, 0, 0, 0.2); + border-radius: 4px; + box-sizing: border-box; + + &.float-left { + float: left; + } + + .upload-label { + width: 100%; + height: 100%; + padding: 0 16px; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + font-size: 16px; + color: rgba(0, 0, 0, 0.54); + text-align: center; + .mat-icon { + margin-right: 17px; + } + } + } + + .tb-hint{ + margin-top: 8px; + } +} + +:host ::ng-deep { + button.browse-file { + padding: 0; + font-size: 16px; + span.mat-button-wrapper { + display: block; + label { + display: block; + cursor: pointer; + padding: 0 16px; + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/multiple-image-input.component.ts b/ui-ngx/src/app/shared/components/multiple-image-input.component.ts new file mode 100644 index 0000000000..ae372a86fd --- /dev/null +++ b/ui-ngx/src/app/shared/components/multiple-image-input.component.ts @@ -0,0 +1,210 @@ +/// +/// Copyright © 2016-2022 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 { AfterViewInit, ChangeDetectorRef, Component, forwardRef, Input, OnDestroy, ViewChild } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ControlValueAccessor, FormArray, NG_VALUE_ACCESSOR, } from '@angular/forms'; +import { Subscription } from 'rxjs'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { DropDirective, FlowDirective } from '@flowjs/ngx-flow'; +import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; +import { UtilsService } from '@core/services/utils.service'; +import { DialogService } from '@core/services/dialog.service'; +import { TranslateService } from '@ngx-translate/core'; +import { FileSizePipe } from '@shared/pipe/file-size.pipe'; +import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; +import { DndDropEvent } from 'ngx-drag-drop'; +import { isUndefined } from '@core/utils'; + +@Component({ + selector: 'tb-multiple-image-input', + templateUrl: './multiple-image-input.component.html', + styleUrls: ['./multiple-image-input.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MultipleImageInputComponent), + multi: true + } + ] +}) +export class MultipleImageInputComponent extends PageComponent implements AfterViewInit, OnDestroy, ControlValueAccessor { + + @Input() + label: string; + + @Input() + maxSizeByte: number; + + private requiredValue: boolean; + + get required(): boolean { + return this.requiredValue; + } + + @Input() + set required(value: boolean) { + const newVal = coerceBooleanProperty(value); + if (this.requiredValue !== newVal) { + this.requiredValue = newVal; + } + } + + @Input() + disabled: boolean; + + @Input() + inputId = this.utils.guid(); + + imageUrls: string[]; + safeImageUrls: SafeUrl[]; + + dragIndex: number; + + @ViewChild('flow', {static: true}) + flow: FlowDirective; + + @ViewChild('flowDrop', {static: true}) + flowDrop: DropDirective; + + autoUploadSubscription: Subscription; + + private propagateChange = null; + + private viewInited = false; + + constructor(protected store: Store, + private utils: UtilsService, + private sanitizer: DomSanitizer, + private dialog: DialogService, + private translate: TranslateService, + private fileSize: FileSizePipe, + private cd: ChangeDetectorRef) { + super(store); + } + + ngAfterViewInit() { + this.autoUploadSubscription = this.flow.events$.subscribe(event => { + if (event.type === 'filesAdded') { + const readers = []; + (event.event[0] as flowjs.FlowFile[]).forEach(file => { + readers.push(this.readImageUrl(file)); + }); + if (readers.length) { + Promise.all(readers).then((files) => { + files = files.filter(file => file.imageUrl != null || file.safeImageUrl != null); + this.imageUrls = this.imageUrls.concat(files.map(content => content.imageUrl)); + this.safeImageUrls = this.safeImageUrls.concat(files.map(content => content.safeImageUrl)); + this.updateModel(); + }); + } + } + }); + if (this.disabled) { + this.flowDrop.disable(); + } else { + this.flowDrop.enable(); + } + this.viewInited = true; + } + + private readImageUrl(file: flowjs.FlowFile): Promise { + return new Promise((resolve) => { + if (this.maxSizeByte && this.maxSizeByte < file.size) { + resolve({imageUrl: null, safeImageUrl: null}); + } + const reader = new FileReader(); + reader.onload = () => { + let imageUrl = null; + let safeImageUrl = null; + if (typeof reader.result === 'string' && reader.result.startsWith('data:image/')) { + imageUrl = reader.result; + if (imageUrl && imageUrl.length > 0) { + safeImageUrl = this.sanitizer.bypassSecurityTrustUrl(imageUrl); + } + } + resolve({imageUrl, safeImageUrl}); + }; + reader.onerror = () => { + resolve({imageUrl: null, safeImageUrl: null}); + }; + reader.readAsDataURL(file.file); + }); + } + + ngOnDestroy() { + this.autoUploadSubscription.unsubscribe(); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.viewInited) { + if (this.disabled) { + this.flowDrop.disable(); + } else { + this.flowDrop.enable(); + } + } + } + + writeValue(value: string[]): void { + this.imageUrls = value || []; + this.safeImageUrls = this.imageUrls.map(imageUrl => this.sanitizer.bypassSecurityTrustUrl(imageUrl)); + } + + private updateModel() { + this.cd.markForCheck(); + this.propagateChange(this.imageUrls); + } + + clearImage(index: number) { + this.imageUrls.splice(index, 1); + this.safeImageUrls.splice(index, 1); + this.updateModel(); + } + + imageDragStart(index: number) { + setTimeout(() => { + this.dragIndex = index; + this.cd.markForCheck(); + }); + } + + imageDragEnd() { + this.dragIndex = -1; + this.cd.markForCheck(); + } + + imageDrop(event: DndDropEvent) { + let index = event.index; + if (isUndefined(index)) { + index = this.safeImageUrls.length; + } + moveItemInArray(this.imageUrls, this.dragIndex, index); + moveItemInArray(this.safeImageUrls, this.dragIndex, index); + this.dragIndex = -1; + this.updateModel(); + } +} diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index ed10cf4a1a..3a7ef4426f 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -33,6 +33,7 @@ import { AbstractControl, FormGroup } from '@angular/forms'; import { Observable } from 'rxjs'; import { Dashboard } from '@shared/models/dashboard.models'; import { IAliasController } from '@core/api/widget-api.models'; +import { isEmptyStr } from '@core/utils'; export enum widgetType { timeseries = 'timeseries', @@ -624,6 +625,19 @@ export interface IWidgetSettingsComponent { [key: string]: any; } +function removeEmptyWidgetSettings(settings: WidgetSettings): WidgetSettings { + if (settings) { + const keys = Object.keys(settings); + for (const key of keys) { + const val = settings[key]; + if (val === null || isEmptyStr(val)) { + delete settings[key]; + } + } + } + return settings; +} + @Directive() // tslint:disable-next-line:directive-class-suffix export abstract class WidgetSettingsComponent extends PageComponent implements @@ -713,9 +727,9 @@ export abstract class WidgetSettingsComponent extends PageComponent implements } protected onSettingsChanged(updated: WidgetSettings) { - this.settingsValue = updated; + this.settingsValue = removeEmptyWidgetSettings(updated); if (this.validateSettings()) { - this.settingsChangedEmitter.emit(updated); + this.settingsChangedEmitter.emit(this.settingsValue); } else { this.settingsChangedEmitter.emit(null); } diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 26a03c3049..8d8de8e74a 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -136,6 +136,7 @@ import { TbJsonToStringDirective } from '@shared/components/directives/tb-json-t import { JsonObjectEditDialogComponent } from '@shared/components/dialog/json-object-edit-dialog.component'; import { HistorySelectorComponent } from '@shared/components/time/history-selector/history-selector.component'; import { EntityGatewaySelectComponent } from '@shared/components/entity/entity-gateway-select.component'; +import { DndModule } from 'ngx-drag-drop'; import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list.component'; import { ContactComponent } from '@shared/components/contact.component'; import { TimezoneSelectComponent } from '@shared/components/time/timezone-select.component'; @@ -161,6 +162,7 @@ import { CssComponent } from '@shared/components/css.component'; import { HtmlComponent } from '@shared/components/html.component'; import { SafePipe } from '@shared/pipe/safe.pipe'; import { DragDropModule } from '@angular/cdk/drag-drop'; +import { MultipleImageInputComponent } from '@shared/components/multiple-image-input.component'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -256,6 +258,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) NodeScriptTestDialogComponent, JsonFormComponent, ImageInputComponent, + MultipleImageInputComponent, FileInputComponent, MessageTypeAutocompleteComponent, KeyValMapComponent, @@ -330,6 +333,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) HotkeyModule, ColorPickerModule, NgxHmCarouselModule, + DndModule, NgxFlowModule, NgxFlowchartModule, // ngx-markdown @@ -440,6 +444,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) HotkeyModule, ColorPickerModule, NgxHmCarouselModule, + DndModule, NgxFlowchartModule, MarkdownModule, ConfirmDialogComponent, @@ -452,6 +457,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) NodeScriptTestDialogComponent, JsonFormComponent, ImageInputComponent, + MultipleImageInputComponent, FileInputComponent, MessageTypeAutocompleteComponent, KeyValMapComponent, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 6a1b6a7bf7..aadfe8f481 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2257,10 +2257,22 @@ "avatar": "Avatar", "open-user-menu": "Open user menu" }, + "file-input": { + "browse-file": "Browse file", + "browse-files": "Browse files" + }, + "image-input": { + "drop-image-or": "Drag and drop an image or", + "drop-images-or": "Drag and drop an images or", + "no-images": "No images selected", + "images": "images" + }, "import": { "no-file": "No file selected", "drop-file": "Drop a JSON file or click to select a file to upload.", + "drop-json-file-or": "Drag and drop a JSON file or", "drop-file-csv": "Drop a CSV file or click to select a file to upload.", + "drop-file-csv-or": "Drag and drop a CSV file or", "column-value": "Value", "column-title": "Title", "column-example": "Example value data", @@ -2427,6 +2439,7 @@ "direct-url-required": "Direct URL is required", "download": "Download package", "drop-file": "Drop a package file or click to select a file to upload.", + "drop-package-file-or": "Drag and drop a package file or", "file-name": "File name", "file-size": "File size", "file-size-bytes": "File size in bytes", @@ -2530,6 +2543,7 @@ "delete-resources-title": "Are you sure you want to delete { count, plural, 1 {1 resource} other {# resources} }?", "download": "Download resource", "drop-file": "Drop a resource file or click to select a file to upload.", + "drop-resource-file-or": "Drag and drop a resource file or", "empty": "Resource is empty", "file-name": "File name", "idCopiedMessage": "Resource Id has been copied to clipboard", @@ -3895,7 +3909,184 @@ "deleteButton": "Remove", "drawCircleMarkerButton": "Create circle marker", "rotateButton": "Rotate polygon" - } + }, + "map-provider-settings": "Map provider settings", + "map-provider": "Map provider", + "map-provider-google": "Google maps", + "map-provider-openstreet": "OpenStreet maps", + "map-provider-here": "HERE maps", + "map-provider-image": "Image map", + "map-provider-tencent": "Tencent maps", + "openstreet-provider": "OpenStreet map provider", + "openstreet-provider-mapnik": "OpenStreetMap.Mapnik (Default)", + "openstreet-provider-hot": "OpenStreetMap.HOT", + "openstreet-provider-esri-street": "Esri.WorldStreetMap", + "openstreet-provider-esri-topo": "Esri.WorldTopoMap", + "openstreet-provider-cartodb-positron": "CartoDB.Positron", + "openstreet-provider-cartodb-dark-matter": "CartoDB.DarkMatter", + "use-custom-provider": "Use custom provider", + "custom-provider-tile-url": "Custom provider tile URL", + "google-maps-api-key": "Google Maps API Key", + "default-map-type": "Default map type", + "google-map-type-roadmap": "Roadmap", + "google-map-type-satelite": "Satellite", + "google-map-type-hybrid": "Hybrid", + "google-map-type-terrain": "Terrain", + "map-layer": "Map layer", + "here-map-normal-day": "HERE.normalDay (Default)", + "here-map-normal-night": "HERE.normalNight", + "here-map-hybrid-day": "HERE.hybridDay", + "here-map-terrain-day": "HERE.terrainDay", + "credentials": "Credentials", + "here-app-id": "HERE app id", + "here-app-code": "HERE app code", + "tencent-maps-api-key": "Tencent Maps API Key", + "tencent-map-type-roadmap": "Roadmap", + "tencent-map-type-satelite": "Satellite", + "tencent-map-type-hybrid": "Hybrid", + "image-map-background": "Image map background", + "image-map-background-from-entity-attribute": "Take image map background from entity attribute", + "image-url-source-entity-alias": "Image URL source entity alias", + "image-url-source-entity-attribute": "Image URL source entity attribute", + "common-map-settings": "Common map settings", + "x-pos-key-name": "X position key name", + "y-pos-key-name": "Y position key name", + "latitude-key-name": "Latitude key name", + "longitude-key-name": "Longitude key name", + "default-map-zoom-level": "Default map zoom level (0 - 20)", + "default-map-center-position": "Default map center position (0,0)", + "disable-scroll-zooming": "Disable scroll zooming", + "disable-zoom-control-buttons": "Disable zoom control buttons", + "fit-map-bounds": "Fit map bounds to cover all markers", + "use-default-map-center-position": "Use default map center position", + "entities-limit": "Limit of entities to load", + "markers-settings": "Markers settings", + "marker-offset-x": "Marker X offset relative to position multiplied by marker width", + "marker-offset-y": "Marker Y offset relative to position multiplied by marker height", + "position-function": "Position conversion function, should return x,y coordinates as double from 0 to 1 each", + "draggable-marker": "Draggable marker", + "label": "Label", + "show-label": "Show label", + "use-label-function": "Use label function", + "label-pattern": "Label (pattern examples: '${entityName}', '${entityName}: (Text ${keyName} units.)' )", + "label-function": "Label function", + "tooltip": "Tooltip", + "show-tooltip": "Show tooltip", + "show-tooltip-action": "Action for displaying the tooltip", + "show-tooltip-action-click": "Show tooltip on click (Default)", + "show-tooltip-action-hover": "Show tooltip on hover", + "auto-close-tooltips": "Auto-close tooltips", + "use-tooltip-function": "Use tooltip function", + "tooltip-pattern": "Tooltip (for ex. 'Text ${keyName} units.' or Link text')", + "tooltip-function": "Tooltip function", + "tooltip-offset-x": "Tooltip X offset relative to marker anchor multiplied by marker width", + "tooltip-offset-y": "Tooltip Y offset relative to marker anchor multiplied by marker height", + "color": "Color", + "use-color-function": "Use color function", + "color-function": "Color function", + "marker-image": "Marker image", + "use-marker-image-function": "Use marker image function", + "custom-marker-image": "Custom marker image", + "custom-marker-image-size": "Custom marker image size (px)", + "marker-image-function": "Marker image function", + "marker-images": "Marker images", + "polygon-settings": "Polygon settings", + "show-polygon": "Show polygon", + "polygon-key-name": "Polygon key name", + "enable-polygon-edit": "Enable polygon edit", + "polygon-label": "Polygon label", + "show-polygon-label": "Show polygon label", + "use-polygon-label-function": "Use polygon label function", + "polygon-label-pattern": "Polygon label (pattern examples: '${entityName}', '${entityName}: (Text ${keyName} units.)' )", + "polygon-label-function": "Polygon label function", + "polygon-tooltip": "Polygon tooltip", + "show-polygon-tooltip": "Show polygon tooltip", + "auto-close-polygon-tooltips": "Auto-close polygon tooltips", + "use-polygon-tooltip-function": "Use polygon tooltip function", + "polygon-tooltip-pattern": "Tooltip (for ex. 'Text ${keyName} units.' or Link text')", + "polygon-tooltip-function": "Polygon tooltip function", + "polygon-color": "Polygon color", + "polygon-opacity": "Polygon opacity", + "use-polygon-color-function": "Use polygon color function", + "polygon-color-function": "Polygon color function", + "polygon-stroke": "Polygon stroke", + "stroke-color": "Stroke color", + "stroke-opacity": "Stroke opacity", + "stroke-weight": "Stroke weight", + "use-polygon-stroke-color-function": "Use polygon stroke color function", + "polygon-stroke-color-function": "Polygon stroke color function", + "circle-settings": "Circle settings", + "show-circle": "Show circle", + "circle-key-name": "Circle key name", + "enable-circle-edit": "Enable circle edit", + "circle-label": "Circle label", + "show-circle-label": "Show circle label", + "use-circle-label-function": "Use circle label function", + "circle-label-pattern": "Circle label (pattern examples: '${entityName}', '${entityName}: (Text ${keyName} units.)' )", + "circle-label-function": "Circle label function", + "circle-tooltip": "Circle tooltip", + "show-circle-tooltip": "Show circle tooltip", + "auto-close-circle-tooltips": "Auto-close circle tooltips", + "use-circle-tooltip-function": "Use circle tooltip function", + "circle-tooltip-pattern": "Tooltip (for ex. 'Text ${keyName} units.' or Link text')", + "circle-tooltip-function": "Circle tooltip function", + "circle-fill-color": "Circle fill color", + "circle-fill-color-opacity": "Circle fill color opacity", + "use-circle-fill-color-function": "Use circle fill color function", + "circle-fill-color-function": "Circle fill color function", + "circle-stroke": "Circle stroke", + "use-circle-stroke-color-function": "Use circle stroke color function", + "circle-stroke-color-function": "Circle stroke color function", + "markers-clustering-settings": "Markers clustering settings", + "use-map-markers-clustering": "Use map markers clustering", + "zoom-on-cluster-click": "Zoom when clicking on a cluster", + "max-cluster-zoom": "The maximum zoom level when a marker can be part of a cluster (0 - 18)", + "max-cluster-radius-pixels": "Maximum radius that a cluster will cover in pixels", + "cluster-zoom-animation": "Show animation on markers when zooming", + "show-markers-bounds-on-cluster-mouse-over": "Show the bounds of markers when mouse over a cluster", + "spiderfy-max-zoom-level": "Spiderfy at the max zoom level (to see all cluster markers)", + "load-optimization": "Load optimization", + "cluster-chunked-loading": "Use chunks for adding markers so that the page does not freeze", + "cluster-markers-lazy-load": "Use lazy load for adding markers", + "editor-settings": "Editor settings", + "enable-snapping": "Enable snapping to other vertices for precision drawing", + "init-draggable-mode": "Initialize map in draggable mode", + "hide-all-edit-buttons": "Hide all edit control buttons", + "hide-draw-buttons": "Hide draw buttons", + "hide-edit-buttons": "Hide edit buttons", + "hide-remove-button": "Hide remove button", + "route-map-settings": "Route map settings", + "trip-animation-settings": "Trip animation settings", + "normalization-step": "Normalization data step (ms)", + "tooltip-background-color": "Tooltip background color", + "tooltip-font-color": "Tooltip font color", + "tooltip-opacity": "Tooltip opacity (0-1)", + "auto-close-tooltip": "Auto-close tooltip", + "rotation-angle": "Set additional rotation angle for marker (deg)", + "path-settings": "Path settings", + "path-color": "Path color", + "use-path-color-function": "Use path color function", + "path-color-function": "Path color function", + "path-decorator": "Path decorator", + "use-path-decorator": "Use path decorator", + "decorator-symbol": "Decorator symbol", + "decorator-symbol-arrow-head": "Arrow", + "decorator-symbol-dash": "Dash", + "decorator-symbol-size": "Decorator symbol size (px)", + "use-path-decorator-custom-color": "Use path decorator custom color", + "decorator-custom-color": "Decorator custom color", + "decorator-offset": "Decorator offset", + "end-decorator-offset": "End decorator offset", + "decorator-repeat": "Decorator repeat", + "points-settings": "Points settings", + "show-points": "Show points", + "point-color": "Point color", + "point-size": "Point size (px)", + "use-point-color-function": "Use point color function", + "point-color-function": "Point color function", + "use-point-as-anchor": "Use point as anchor", + "point-as-anchor-function": "Point as anchor function", + "independent-point-tooltip": "Independent point tooltip" }, "markdown": { "use-markdown-text-function": "Use markdown/HTML value function", diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index ad051179cd..b6bf5fdfd2 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -1364,6 +1364,11 @@ mat-label { &::after { transition: none; } + &.dragging { + .mat-chip-ripple { + display: none !important; + } + } &.dropping { //border: dashed 2px; //opacity: .5; diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 86344d06a3..31688602db 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -6630,6 +6630,13 @@ ngx-daterangepicker-material@^5.0.1: dayjs "^1.10.4" tslib "^2.0.0" +ngx-drag-drop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ngx-drag-drop/-/ngx-drag-drop-2.0.0.tgz#65d970229964803726fb7b9af4aec24005c810c7" + integrity sha512-t+4/eiC8zaXKqU1ruNfFEfGs1GpMNwpffD0baopvZFKjQHCb5rhNqFilJ54wO4T0OwGp4/RnsVhlcxe1mX6UJg== + dependencies: + tslib "^1.9.0" + "ngx-flowchart@https://github.com/thingsboard/ngx-flowchart.git#release/1.0.0": version "0.0.1" resolved "https://github.com/thingsboard/ngx-flowchart.git#aac96f7e0490a386d58864d7819873e7c63cbb48" @@ -9472,6 +9479,11 @@ ts-node@^10.0.0, ts-node@^10.4.0: make-error "^1.1.1" yn "3.1.1" +ts-transformer-keys@^0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/ts-transformer-keys/-/ts-transformer-keys-0.4.3.tgz#d62389a40f430c00ef98fb9575fb6778a196e3ed" + integrity sha512-pOTLlet1SnAvhKNr9tMAFwuv5283OkUNiq1fXTEK+vrSv+kxU3e2Ijr/UkqyX2vuMmvcNHdpXC31hob7ljH//g== + tsconfig-paths@^3.9.0: version "3.12.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz#19769aca6ee8f6a1a341e38c8fa45dd9fb18899b" From 7ca1f5068bce40720d30c993048fc36afb64b8c3 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Wed, 11 May 2022 16:40:18 +0300 Subject: [PATCH 133/276] No Transactional with Supports propogation --- .../server/dao/asset/BaseAssetService.java | 4 --- .../device/DeviceCredentialsServiceImpl.java | 3 -- .../dao/device/DeviceProfileServiceImpl.java | 4 --- .../server/dao/device/DeviceServiceImpl.java | 28 ------------------- .../server/dao/edge/EdgeServiceImpl.java | 5 ---- .../dao/entityview/EntityViewServiceImpl.java | 5 ---- .../server/dao/ota/BaseOtaPackageService.java | 3 -- .../dao/relation/BaseRelationService.java | 9 ------ .../dao/tenant/TenantProfileServiceImpl.java | 3 -- .../service/BaseDeviceProfileServiceTest.java | 1 + 10 files changed, 1 insertion(+), 64 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java index c769adcd60..729c923e54 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java @@ -118,7 +118,6 @@ public class BaseAssetService extends AbstractCachedEntityService deviceValidator; - @Transactional(propagation = Propagation.SUPPORTS) @Override public DeviceInfo findDeviceInfoById(TenantId tenantId, DeviceId deviceId) { log.trace("Executing findDeviceInfoById [{}]", deviceId); @@ -120,7 +119,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService deviceDao.findDeviceByTenantIdAndName(tenantId.getId(), name).orElse(null), true); } - @Transactional(propagation = Propagation.SUPPORTS) @Override public Device saveDeviceWithAccessToken(Device device, String accessToken) { return doSaveDevice(device, accessToken, true); } - @Transactional(propagation = Propagation.SUPPORTS) @Override public Device saveDevice(Device device, boolean doValidate) { return doSaveDevice(device, null, doValidate); } - @Transactional(propagation = Propagation.SUPPORTS) @Override public Device saveDevice(Device device) { return doSaveDevice(device, null, true); @@ -340,7 +334,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService findDevicesByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findDevicesByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); @@ -349,7 +342,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService findDeviceInfosByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findDeviceInfosByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); @@ -358,7 +350,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService findDevicesByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink) { log.trace("Executing findDevicesByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink); @@ -368,7 +359,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService findDevicesByTenantIdAndTypeAndEmptyOtaPackage(TenantId tenantId, DeviceProfileId deviceProfileId, @@ -382,7 +372,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService findDeviceInfosByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink) { log.trace("Executing findDeviceInfosByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink); @@ -401,7 +389,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService findDeviceInfosByTenantIdAndDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId, PageLink pageLink) { log.trace("Executing findDeviceInfosByTenantIdAndDeviceProfileId, tenantId [{}], deviceProfileId [{}], pageLink [{}]", tenantId, deviceProfileId, pageLink); @@ -411,7 +398,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List deviceIds) { log.trace("Executing findDevicesByTenantIdAndIdsAsync, tenantId [{}], deviceIds [{}]", tenantId, deviceIds); @@ -428,7 +414,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService findDevicesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { log.trace("Executing findDevicesByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); @@ -438,7 +423,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService findDeviceInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { log.trace("Executing findDeviceInfosByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); @@ -448,7 +432,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService findDevicesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) { log.trace("Executing findDevicesByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink); @@ -459,7 +442,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService findDeviceInfosByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) { log.trace("Executing findDeviceInfosByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink); @@ -470,7 +452,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, PageLink pageLink) { log.trace("Executing findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId, tenantId [{}], customerId [{}], deviceProfileId [{}], pageLink [{}]", tenantId, customerId, deviceProfileId, pageLink); @@ -481,7 +462,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List deviceIds) { log.trace("Executing findDevicesByTenantIdCustomerIdAndIdsAsync, tenantId [{}], customerId [{}], deviceIds [{}]", tenantId, customerId, deviceIds); @@ -492,7 +472,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService> findDevicesByQuery(TenantId tenantId, DeviceSearchQuery query) { ListenableFuture> relations = relationService.findByQuery(tenantId, query.toEntitySearchQuery()); @@ -528,7 +506,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService> findDeviceTypesByTenantId(TenantId tenantId) { log.trace("Executing findDeviceTypesByTenantId, tenantId [{}]", tenantId); @@ -623,13 +600,11 @@ public class DeviceServiceImpl extends AbstractCachedEntityService findDevicesIdsByDeviceProfileTransportType(DeviceTransportType transportType, PageLink pageLink) { return deviceDao.findDevicesIdsByDeviceProfileTransportType(transportType, pageLink); } - @Transactional(propagation = Propagation.SUPPORTS) @Override public Device assignDeviceToEdge(TenantId tenantId, DeviceId deviceId, EdgeId edgeId) { Device device = findDeviceById(tenantId, deviceId); @@ -649,7 +624,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService findDevicesByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink) { log.trace("Executing findDevicesByTenantIdAndEdgeId, tenantId [{}], edgeId [{}], pageLink [{}]", tenantId, edgeId, pageLink); @@ -679,7 +652,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService findDevicesByTenantIdAndEdgeIdAndType(TenantId tenantId, EdgeId edgeId, String type, PageLink pageLink) { log.trace("Executing findDevicesByTenantIdAndEdgeIdAndType, tenantId [{}], edgeId [{}], type [{}] pageLink [{}]", tenantId, edgeId, type, pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index 07b00a1747..271c74a3e1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -147,7 +147,6 @@ public class EdgeServiceImpl extends AbstractCachedEntityService new EntityViewCacheValue(null, v), true)); } - @Transactional(propagation = Propagation.SUPPORTS) @Override public void deleteEntityView(TenantId tenantId, EntityViewId entityViewId) { log.trace("Executing deleteEntityView [{}]", entityViewId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java index a1d074048b..4d0bb18a43 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java @@ -69,7 +69,6 @@ public class BaseOtaPackageService extends AbstractCachedEntityService RelationCacheValue.builder().relation(relations).build(), false); } - @Transactional(propagation = Propagation.SUPPORTS) @Override public boolean saveRelation(TenantId tenantId, EntityRelation relation) { log.trace("Executing saveRelation [{}]", relation); @@ -130,7 +128,6 @@ public class BaseRelationService implements RelationService { return future; } - @Transactional(propagation = Propagation.SUPPORTS) @Override public boolean deleteRelation(TenantId tenantId, EntityRelation relation) { log.trace("Executing DeleteRelation [{}]", relation); @@ -150,7 +147,6 @@ public class BaseRelationService implements RelationService { return future; } - @Transactional(propagation = Propagation.SUPPORTS) @Override public boolean deleteRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { log.trace("Executing deleteRelation [{}][{}][{}][{}]", from, to, relationType, typeGroup); @@ -171,7 +167,6 @@ public class BaseRelationService implements RelationService { return future; } - @Transactional(propagation = Propagation.SUPPORTS) @Override public void deleteEntityRelations(TenantId tenantId, EntityId entityId) { log.trace("Executing deleteEntityRelations [{}]", entityId); @@ -269,7 +264,6 @@ public class BaseRelationService implements RelationService { return false; } - @Transactional(propagation = Propagation.SUPPORTS) @Override public List findByFrom(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup) { validate(from); @@ -315,7 +309,6 @@ public class BaseRelationService implements RelationService { }, MoreExecutors.directExecutor()); } - @Transactional(propagation = Propagation.SUPPORTS) @Override public List findByFromAndType(TenantId tenantId, EntityId from, String relationType, RelationTypeGroup typeGroup) { RelationCacheKey cacheKey = RelationCacheKey.builder().from(from).type(relationType).typeGroup(typeGroup).direction(EntitySearchDirection.FROM).build(); @@ -334,7 +327,6 @@ public class BaseRelationService implements RelationService { return executor.submit(() -> findByFromAndType(tenantId, from, relationType, typeGroup)); } - @Transactional(propagation = Propagation.SUPPORTS) @Override public List findByTo(TenantId tenantId, EntityId to, RelationTypeGroup typeGroup) { validate(to); @@ -463,7 +455,6 @@ public class BaseRelationService implements RelationService { }, MoreExecutors.directExecutor()); } - @Transactional(propagation = Propagation.SUPPORTS) @Override public void removeRelations(TenantId tenantId, EntityId entityId) { log.trace("removeRelations {}", entityId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java index 65cbd6f3a1..71140140c7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java @@ -81,7 +81,6 @@ public class TenantProfileServiceImpl extends AbstractCachedEntityService Date: Wed, 11 May 2022 17:16:27 +0300 Subject: [PATCH 134/276] Restore log level --- application/src/test/resources/logback-test.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/application/src/test/resources/logback-test.xml b/application/src/test/resources/logback-test.xml index f051721391..d3301bf660 100644 --- a/application/src/test/resources/logback-test.xml +++ b/application/src/test/resources/logback-test.xml @@ -9,8 +9,7 @@ - - + From fa9f464810056ac1ddb0865555ec0033312d06fb Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Wed, 11 May 2022 17:53:26 +0300 Subject: [PATCH 135/276] refactoring: - Customer controller - finish --- .../server/controller/CustomerController.java | 30 +++++-------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java index b419121459..8d67fe02de 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; +import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; @@ -33,7 +34,6 @@ import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.audit.ActionType; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EdgeId; @@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.customer.TbCustomerService; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; @@ -64,9 +65,12 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI @RestController @TbCoreComponent +@RequiredArgsConstructor @RequestMapping("/api") public class CustomerController extends BaseController { + private final TbCustomerService tbCustomerService; + public static final String IS_PUBLIC = "isPublic"; public static final String CUSTOMER_SECURITY_CHECK = "If the user has the authority of 'Tenant Administrator', the server checks that the customer is owned by the same tenant. " + "If the user has the authority of 'Customer User', the server checks that the user belongs to the customer."; @@ -145,30 +149,10 @@ public class CustomerController extends BaseController { @RequestMapping(value = "/customer", method = RequestMethod.POST) @ResponseBody public Customer saveCustomer(@ApiParam(value = "A JSON value representing the customer.") @RequestBody Customer customer) throws ThingsboardException { - try { customer.setTenantId(getCurrentUser().getTenantId()); - checkEntity(customer.getId(), customer, Resource.CUSTOMER); - - Customer savedCustomer = checkNotNull(customerService.saveCustomer(customer)); - - logEntityAction(savedCustomer.getId(), savedCustomer, - savedCustomer.getId(), - customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); - - if (customer.getId() != null) { - sendEntityNotificationMsg(savedCustomer.getTenantId(), savedCustomer.getId(), EdgeEventActionType.UPDATED); - } - - return savedCustomer; - } catch (Exception e) { - - logEntityAction(emptyId(EntityType.CUSTOMER), customer, - null, customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e); - - throw handleException(e); - } - } + return tbCustomerService.save(customer, getCurrentUser()); + } @ApiOperation(value = "Delete Customer (deleteCustomer)", notes = "Deletes the Customer and all customer Users. " + From 33e5b033ec396e8a0f6a8718ebf26f3cfc13911b Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 11 May 2022 18:16:30 +0300 Subject: [PATCH 136/276] UI: Widget settings forms support for extensions --- ui-ngx/extra-webpack.config.js | 10 +++-- .../src/app/core/http/rule-chain.service.ts | 2 +- .../app/core/services/resources.service.ts | 41 ++++++++++------- ...led-indicator-widget-settings.component.ts | 2 +- .../round-switch-widget-settings.component.ts | 2 +- .../slide-toggle-widget-settings.component.ts | 2 +- ...witch-control-widget-settings.component.ts | 2 +- .../widget/widget-component.service.ts | 45 ++++++++++++++----- 8 files changed, 71 insertions(+), 35 deletions(-) diff --git a/ui-ngx/extra-webpack.config.js b/ui-ngx/extra-webpack.config.js index 1be975de4f..f445eccafb 100644 --- a/ui-ngx/extra-webpack.config.js +++ b/ui-ngx/extra-webpack.config.js @@ -60,21 +60,23 @@ module.exports = (config, options) => { ); const index = config.plugins.findIndex(p => p instanceof ngWebpack.ivy.AngularWebpackPlugin || p instanceof ngWebpack.AngularWebpackPlugin); - const angularWebpackPlugin = config.plugins[index]; - - addTransformerToAngularWebpackPlugin(angularWebpackPlugin, keysTransformer); + let angularWebpackPlugin = config.plugins[index]; if (config.mode === 'production') { const angularCompilerOptions = angularWebpackPlugin.pluginOptions; angularCompilerOptions.emitClassMetadata = true; angularCompilerOptions.emitNgModuleScope = true; config.plugins.splice(index, 1); - config.plugins.push(new ngWebpack.ivy.AngularWebpackPlugin(angularCompilerOptions)); + angularWebpackPlugin = new ngWebpack.ivy.AngularWebpackPlugin(angularCompilerOptions); + config.plugins.push(angularWebpackPlugin); const javascriptOptimizerOptions = config.optimization.minimizer[1].options; delete javascriptOptimizerOptions.define.ngJitMode; config.optimization.minimizer.splice(1, 1); config.optimization.minimizer.push(new JavaScriptOptimizerPlugin(javascriptOptimizerOptions)); } + + addTransformerToAngularWebpackPlugin(angularWebpackPlugin, keysTransformer); + return config; }; diff --git a/ui-ngx/src/app/core/http/rule-chain.service.ts b/ui-ngx/src/app/core/http/rule-chain.service.ts index 1b77bf4ead..6b0f3c3f22 100644 --- a/ui-ngx/src/app/core/http/rule-chain.service.ts +++ b/ui-ngx/src/app/core/http/rule-chain.service.ts @@ -219,7 +219,7 @@ export class RuleChainService { map((res) => { if (nodeDefinition.configDirective && nodeDefinition.configDirective.length) { const selector = snakeCase(nodeDefinition.configDirective, '-'); - const componentFactory = res.find((factory) => + const componentFactory = res.factories.find((factory) => factory.selector === selector); if (componentFactory) { this.ruleNodeConfigFactories[nodeDefinition.configDirective] = componentFactory; diff --git a/ui-ngx/src/app/core/services/resources.service.ts b/ui-ngx/src/app/core/services/resources.service.ts index 763617eda9..688c0da31f 100644 --- a/ui-ngx/src/app/core/services/resources.service.ts +++ b/ui-ngx/src/app/core/services/resources.service.ts @@ -30,6 +30,11 @@ import { IModulesMap } from '@modules/common/modules-map.models'; declare const System; +export interface ModulesWithFactories { + modules: Type[]; + factories: ComponentFactory[]; +} + @Injectable({ providedIn: 'root' }) @@ -37,7 +42,7 @@ export class ResourcesService { private loadedResources: { [url: string]: ReplaySubject } = {}; private loadedModules: { [url: string]: ReplaySubject[]> } = {}; - private loadedFactories: { [url: string]: ReplaySubject[]> } = {}; + private loadedModulesAndFactories: { [url: string]: ReplaySubject } = {}; private anchor = this.document.getElementsByTagName('head')[0] || this.document.getElementsByTagName('body')[0]; @@ -64,13 +69,13 @@ export class ResourcesService { return this.loadResourceByType(fileType, url); } - public loadFactories(url: string, modulesMap: IModulesMap): Observable[]> { - if (this.loadedFactories[url]) { - return this.loadedFactories[url].asObservable(); + public loadFactories(url: string, modulesMap: IModulesMap): Observable { + if (this.loadedModulesAndFactories[url]) { + return this.loadedModulesAndFactories[url].asObservable(); } modulesMap.init(); - const subject = new ReplaySubject[]>(); - this.loadedFactories[url] = subject; + const subject = new ReplaySubject(); + this.loadedModulesAndFactories[url] = subject; import('@angular/compiler').then( () => { System.import(url).then( @@ -88,25 +93,29 @@ export class ResourcesService { c.ngModuleFactory.create(this.injector); componentFactories.push(...c.componentFactories); } - this.loadedFactories[url].next(componentFactories); - this.loadedFactories[url].complete(); + const modulesWithFactories: ModulesWithFactories = { + modules, + factories: componentFactories + }; + this.loadedModulesAndFactories[url].next(modulesWithFactories); + this.loadedModulesAndFactories[url].complete(); } catch (e) { - this.loadedFactories[url].error(new Error(`Unable to init module from url: ${url}`)); - delete this.loadedFactories[url]; + this.loadedModulesAndFactories[url].error(new Error(`Unable to init module from url: ${url}`)); + delete this.loadedModulesAndFactories[url]; } }, (e) => { - this.loadedFactories[url].error(new Error(`Unable to compile module from url: ${url}`)); - delete this.loadedFactories[url]; + this.loadedModulesAndFactories[url].error(new Error(`Unable to compile module from url: ${url}`)); + delete this.loadedModulesAndFactories[url]; }); } else { - this.loadedFactories[url].error(new Error(`Module '${url}' doesn't have default export!`)); - delete this.loadedFactories[url]; + this.loadedModulesAndFactories[url].error(new Error(`Module '${url}' doesn't have default export!`)); + delete this.loadedModulesAndFactories[url]; } }, (e) => { - this.loadedFactories[url].error(new Error(`Unable to load module from url: ${url}`)); - delete this.loadedFactories[url]; + this.loadedModulesAndFactories[url].error(new Error(`Unable to load module from url: ${url}`)); + delete this.loadedModulesAndFactories[url]; } ); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.ts index d940e0f64a..8ee8ebe127 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.ts @@ -32,7 +32,7 @@ export class LedIndicatorWidgetSettingsComponent extends WidgetSettingsComponent functionScopeVariables = this.widgetService.getWidgetScopeVariables(); get targetDeviceAliasId(): string { - const aliasIds = this.widget.config.targetDeviceAliasIds; + const aliasIds = this.widget?.config?.targetDeviceAliasIds; if (aliasIds && aliasIds.length) { return aliasIds[0]; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/round-switch-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/round-switch-widget-settings.component.ts index cfbdd757f7..cf7ee3bbb9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/round-switch-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/round-switch-widget-settings.component.ts @@ -37,7 +37,7 @@ export class RoundSwitchWidgetSettingsComponent extends WidgetSettingsComponent } get targetDeviceAliasId(): string { - const aliasIds = this.widget.config.targetDeviceAliasIds; + const aliasIds = this.widget?.config?.targetDeviceAliasIds; if (aliasIds && aliasIds.length) { return aliasIds[0]; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts index 47cf13da53..f95473dbb9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts @@ -37,7 +37,7 @@ export class SlideToggleWidgetSettingsComponent extends WidgetSettingsComponent } get targetDeviceAliasId(): string { - const aliasIds = this.widget.config.targetDeviceAliasIds; + const aliasIds = this.widget?.config?.targetDeviceAliasIds; if (aliasIds && aliasIds.length) { return aliasIds[0]; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.ts index 33e928f2c5..65f09f2dd8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.ts @@ -37,7 +37,7 @@ export class SwitchControlWidgetSettingsComponent extends WidgetSettingsComponen } get targetDeviceAliasId(): string { - const aliasIds = this.widget.config.targetDeviceAliasIds; + const aliasIds = this.widget?.config?.targetDeviceAliasIds; if (aliasIds && aliasIds.length) { return aliasIds[0]; } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts index e4760c9185..67541e82cd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Inject, Injectable, Optional, Type } from '@angular/core'; +import { ComponentFactory, Inject, Injectable, Optional, Type } from '@angular/core'; import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; import { WidgetService } from '@core/http/widget.service'; import { forkJoin, from, Observable, of, ReplaySubject, Subject, throwError } from 'rxjs'; @@ -28,7 +28,7 @@ import { } from '@home/models/widget-component.models'; import cssjs from '@core/css/css'; import { UtilsService } from '@core/services/utils.service'; -import { ResourcesService } from '@core/services/resources.service'; +import { ModulesWithFactories, ResourcesService } from '@core/services/resources.service'; import { Widget, widgetActionSources, WidgetControllerDescriptor, WidgetType } from '@shared/models/widget.models'; import { catchError, map, mergeMap, switchMap, tap } from 'rxjs/operators'; import { isFunction, isUndefined } from '@core/utils'; @@ -46,6 +46,7 @@ import * as tinycolor_ from 'tinycolor2'; import moment from 'moment'; import { IModulesMap } from '@modules/common/modules-map.models'; import { HOME_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens'; +import { widgetSettingsComponentsMap } from '@home/components/widget/lib/settings/widget-settings.module'; const tinycolor = tinycolor_; @@ -314,12 +315,12 @@ export class WidgetComponentService { this.cssParser.cssPreviewNamespace = widgetNamespace; this.cssParser.createStyleElement(widgetNamespace, widgetInfo.templateCss); const resourceTasks: Observable[] = []; - const modulesTasks: Observable[] | string>[] = []; + const modulesTasks: Observable[] = []; if (widgetInfo.resources.length > 0) { widgetInfo.resources.filter(r => r.isModule).forEach( (resource) => { modulesTasks.push( - this.resources.loadModules(resource.url, this.modulesMap).pipe( + this.resources.loadFactories(resource.url, this.modulesMap).pipe( catchError((e: Error) => of(e?.message ? e.message : `Failed to load widget resource module: '${resource.url}'`)) ) ); @@ -336,7 +337,7 @@ export class WidgetComponentService { } ); - let modulesObservable: Observable[]>; + let modulesObservable: Observable; if (modulesTasks.length) { modulesObservable = forkJoin(modulesTasks).pipe( map(res => { @@ -344,16 +345,20 @@ export class WidgetComponentService { if (msg) { return msg as string; } else { - let resModules = (res as Type[][]).flat(); + const modulesWithFactoriesList = res as ModulesWithFactories[]; + const resModulesWithFactories: ModulesWithFactories = { + modules: modulesWithFactoriesList.map(mf => mf.modules).flat(), + factories: modulesWithFactoriesList.map(mf => mf.factories).flat() + }; if (modules && modules.length) { - resModules = resModules.concat(modules); + resModulesWithFactories.modules.concat(modules); } - return resModules; + return resModulesWithFactories; } }) ); } else { - modulesObservable = modules && modules.length ? of(modules) : of([]); + modulesObservable = modules && modules.length ? of({modules, factories: []}) : of({modules: [], factories: []}); } resourceTasks.push( @@ -362,10 +367,11 @@ export class WidgetComponentService { if (typeof resolvedModules === 'string') { return of(resolvedModules); } else { + this.registerWidgetSettingsForms(widgetInfo, resolvedModules.factories); return this.dynamicComponentFactoryService.createDynamicComponentFactory( class DynamicWidgetComponentInstance extends DynamicWidgetComponent {}, widgetInfo.templateHtml, - resolvedModules + resolvedModules.modules ).pipe( map((factory) => { widgetInfo.componentFactory = factory; @@ -395,6 +401,25 @@ export class WidgetComponentService { )); } + private registerWidgetSettingsForms(widgetInfo: WidgetInfo, factories: ComponentFactory[]) { + const directives: string[] = []; + if (widgetInfo.settingsDirective && widgetInfo.settingsDirective.length) { + directives.push(widgetInfo.settingsDirective); + } + if (widgetInfo.dataKeySettingsDirective && widgetInfo.dataKeySettingsDirective.length) { + directives.push(widgetInfo.dataKeySettingsDirective); + } + if (widgetInfo.latestDataKeySettingsDirective && widgetInfo.latestDataKeySettingsDirective.length) { + directives.push(widgetInfo.latestDataKeySettingsDirective); + } + if (directives.length) { + factories.filter((factory) => directives.includes(factory.selector)) + .forEach((foundFactory) => { + widgetSettingsComponentsMap[foundFactory.selector] = foundFactory.componentType; + }); + } + } + private createWidgetControllerDescriptor(widgetInfo: WidgetInfo, name: string): WidgetControllerDescriptor { let widgetTypeFunctionBody = `return function _${name} (ctx) {\n` + ' var self = this;\n' + From 321730ed0220d595354464ac7f486f7406319fb1 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 11 May 2022 23:02:03 +0200 Subject: [PATCH 137/276] added upgrade for queues --- .../main/data/upgrade/3.3.4/schema_update.sql | 30 ++++++ .../3.3.4/schema_update_device_profile.sql | 49 ++++++++++ .../install/ThingsboardInstallService.java | 1 + .../install/SqlDatabaseUpgradeService.java | 98 +++++++++++++++++++ .../TbRuleEngineQueueConfigService.java | 48 +++++++++ .../update/DefaultCacheCleanupService.java | 4 + .../queue/TbQueueServiceDeprecated.java | 28 ------ .../server/dao/rule/RuleChainService.java | 2 + .../DefaultTbQueueServiceDeprecated.java | 61 ------------ .../settings/TbQueueRuleEngineSettings.java | 24 +---- .../server/dao/rule/BaseRuleChainService.java | 8 ++ .../rule/engine/flow/TbCheckpointNode.java | 7 -- .../flow/TbCheckpointNodeConfiguration.java | 6 +- 13 files changed, 244 insertions(+), 122 deletions(-) create mode 100644 application/src/main/data/upgrade/3.3.4/schema_update.sql create mode 100644 application/src/main/data/upgrade/3.3.4/schema_update_device_profile.sql create mode 100644 application/src/main/java/org/thingsboard/server/service/install/TbRuleEngineQueueConfigService.java delete mode 100644 common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueServiceDeprecated.java delete mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/DefaultTbQueueServiceDeprecated.java diff --git a/application/src/main/data/upgrade/3.3.4/schema_update.sql b/application/src/main/data/upgrade/3.3.4/schema_update.sql new file mode 100644 index 0000000000..2559de38df --- /dev/null +++ b/application/src/main/data/upgrade/3.3.4/schema_update.sql @@ -0,0 +1,30 @@ +-- +-- Copyright © 2016-2022 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. +-- + +CREATE TABLE IF NOT EXISTS queue ( + id uuid NOT NULL CONSTRAINT queue_pkey PRIMARY KEY, + created_time bigint NOT NULL, + tenant_id uuid, + name varchar(255), + topic varchar(255), + poll_interval int, + partitions int, + consumer_per_partition boolean, + pack_processing_timeout bigint, + submit_strategy varchar(255), + processing_strategy varchar(255), + additional_info varchar +); diff --git a/application/src/main/data/upgrade/3.3.4/schema_update_device_profile.sql b/application/src/main/data/upgrade/3.3.4/schema_update_device_profile.sql new file mode 100644 index 0000000000..5f739e3a98 --- /dev/null +++ b/application/src/main/data/upgrade/3.3.4/schema_update_device_profile.sql @@ -0,0 +1,49 @@ +-- +-- Copyright © 2016-2022 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. +-- + +ALTER TABLE device_profile + ADD COLUMN IF NOT EXISTS default_queue_id uuid; + +DO +$$ + BEGIN + IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'fk_default_queue_device_profile') THEN + ALTER TABLE device_profile + ADD CONSTRAINT fk_default_queue_device_profile FOREIGN KEY (default_queue_id) REFERENCES queue (id); + END IF; + END; +$$; + +DO +$$ + BEGIN + IF EXISTS + (SELECT column_name + FROM information_schema.columns + WHERE table_name = 'device_profile' + AND column_name = 'default_queue_name' + ) + THEN + UPDATE device_profile + SET default_queue_id = q.id + FROM queue as q + WHERE default_queue_name = q.name; + END IF; + END +$$; + +ALTER TABLE device_profile + DROP COLUMN IF EXISTS default_queue_name; diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 4c8e4333a6..5ed8f588e2 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -219,6 +219,7 @@ public class ThingsboardInstallService { databaseEntitiesUpgradeService.upgradeDatabase("3.3.3"); case "3.3.4": log.info("Upgrading ThingsBoard from version 3.3.4 to 3.4.0 ..."); + databaseEntitiesUpgradeService.upgradeDatabase("3.3.4"); log.info("Updating system data..."); systemDataLoaderService.updateSystemWidgets(); break; diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index f7b9e5cad1..473f3accda 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -15,18 +15,31 @@ */ package org.thingsboard.server.service.install; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.id.QueueId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.queue.ProcessingStrategy; +import org.thingsboard.server.common.data.queue.ProcessingStrategyType; +import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.common.data.queue.SubmitStrategy; +import org.thingsboard.server.common.data.queue.SubmitStrategyType; +import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.queue.QueueService; +import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.service.install.sql.SqlDbHelper; @@ -43,7 +56,9 @@ import java.sql.SQLSyntaxErrorException; import java.sql.SQLWarning; import java.sql.Statement; import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import static org.thingsboard.server.service.install.DatabaseHelper.ADDITIONAL_INFO; import static org.thingsboard.server.service.install.DatabaseHelper.ASSIGNED_CUSTOMERS; @@ -101,6 +116,14 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService @Autowired private ApiUsageStateService apiUsageStateService; + @Autowired + private QueueService queueService; + + @Autowired + private TbRuleEngineQueueConfigService queueConfig; + + @Autowired + private RuleChainService ruleChainService; @Override public void upgradeDatabase(String fromVersion) throws Exception { @@ -534,6 +557,81 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService log.error("Failed updating schema!!!", e); } break; + case "3.3.4": + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + log.info("Updating schema ..."); + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.3.4", SCHEMA_UPDATE_SQL); + loadSql(schemaUpdateFile, conn); + + log.info("Loading queues..."); + try { + if (!CollectionUtils.isEmpty(queueConfig.getQueues())) { + queueConfig.getQueues().forEach(queueSettings -> { + Queue queue = new Queue(); + queue.setTenantId(TenantId.SYS_TENANT_ID); + queue.setName(queueSettings.getName()); + queue.setTopic(queueSettings.getTopic()); + queue.setPollInterval(queueSettings.getPollInterval()); + queue.setPartitions(queueSettings.getPartitions()); + queue.setPackProcessingTimeout(queueSettings.getPackProcessingTimeout()); + SubmitStrategy submitStrategy = new SubmitStrategy(); + submitStrategy.setBatchSize(queueSettings.getSubmitStrategy().getBatchSize()); + submitStrategy.setType(SubmitStrategyType.valueOf(queueSettings.getSubmitStrategy().getType())); + queue.setSubmitStrategy(submitStrategy); + ProcessingStrategy processingStrategy = new ProcessingStrategy(); + processingStrategy.setType(ProcessingStrategyType.valueOf(queueSettings.getProcessingStrategy().getType())); + processingStrategy.setRetries(queueSettings.getProcessingStrategy().getRetries()); + processingStrategy.setFailurePercentage(queueSettings.getProcessingStrategy().getFailurePercentage()); + processingStrategy.setPauseBetweenRetries(queueSettings.getProcessingStrategy().getPauseBetweenRetries()); + processingStrategy.setMaxPauseBetweenRetries(queueSettings.getProcessingStrategy().getMaxPauseBetweenRetries()); + queue.setProcessingStrategy(processingStrategy); + queue.setConsumerPerPartition(queueSettings.isConsumerPerPartition()); + queueService.saveQueue(queue); + }); + } else { + systemDataLoaderService.createQueues(); + } + } catch (Exception e) { + } + + log.info("Updating device profiles..."); + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.3.4", "schema_update_device_profile.sql"); + loadSql(schemaUpdateFile, conn); + + log.info("Updating checkpoint rule nodes..."); + PageLink pageLink = new PageLink(100); + PageData pageData; + do { + pageData = tenantService.findTenants(pageLink); + for (Tenant tenant : pageData.getData()) { + TenantId tenantId = tenant.getId(); + Map queues = + queueService.findQueuesByTenantId(tenantId).stream().collect(Collectors.toMap(Queue::getName, Queue::getId)); + try { + List checkpointNodes = + ruleChainService.findRuleNodesByTenantIdAndType(tenantId, "org.thingsboard.rule.engine.flow.TbCheckpointNode"); + checkpointNodes.forEach(node -> { + ObjectNode configuration = (ObjectNode) node.getConfiguration(); + JsonNode queueNameNode = configuration.remove("queueName"); + if (queueNameNode != null) { + String queueName = queueNameNode.asText(); + configuration.put("queueId", queues.get(queueName).toString()); + ruleChainService.saveRuleNode(tenantId, node); + } + }); + } catch (Exception e) { + } + } + pageLink = pageLink.nextPageLink(); + } while (pageData.hasNext()); + + log.info("Updating schema settings..."); + conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3004000;"); + log.info("Schema updated."); + } catch (Exception e) { + log.error("Failed updating schema!!!", e); + } + break; default: throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); } diff --git a/application/src/main/java/org/thingsboard/server/service/install/TbRuleEngineQueueConfigService.java b/application/src/main/java/org/thingsboard/server/service/install/TbRuleEngineQueueConfigService.java new file mode 100644 index 0000000000..1125467082 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/TbRuleEngineQueueConfigService.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.install; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.thingsboard.server.queue.settings.TbRuleEngineQueueConfiguration; + +import javax.annotation.PostConstruct; +import java.util.List; + +@Slf4j +@Data +@EnableAutoConfiguration +@Configuration +@ConfigurationProperties(prefix = "queue.rule-engine") +@Profile("install") +public class TbRuleEngineQueueConfigService { + + private String topic; + private List queues; + + @PostConstruct + public void validate() { + queues.stream().filter(queue -> queue.getName().equals("Main")).findFirst().orElseThrow(() -> { + log.error("Main queue is not configured in thingsboard.yml"); + return new RuntimeException("No \"Main\" queue configured!"); + }); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java index c41eebdbb6..d099f4fb7c 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java @@ -68,6 +68,10 @@ public class DefaultCacheCleanupService implements CacheCleanupService { log.info("Clear cache to upgrade from version 3.3.3 to 3.3.4 ..."); clearAll(); break; + case "3.3.4": + log.info("Clear cache to upgrade from version 3.3.4 to 3.4.0 ..."); + clearCacheByName("deviceProfiles"); + break; default: //Do nothing, since cache cleanup is optional. } diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueServiceDeprecated.java b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueServiceDeprecated.java deleted file mode 100644 index 2328893abe..0000000000 --- a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueServiceDeprecated.java +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.queue; - -import org.thingsboard.server.common.msg.queue.ServiceType; - -import java.util.Set; - -public interface TbQueueServiceDeprecated { - - Set getQueuesByServiceType(ServiceType serviceType); - - String resolve(ServiceType serviceType, String queueName); - -} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java index 5089a6286e..1d99534636 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java @@ -92,5 +92,7 @@ public interface RuleChainService { List findRuleNodesByTenantIdAndType(TenantId tenantId, String name, String toString); + List findRuleNodesByTenantIdAndType(TenantId tenantId, String type); + RuleNode saveRuleNode(TenantId tenantId, RuleNode ruleNode); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/DefaultTbQueueServiceDeprecated.java b/common/queue/src/main/java/org/thingsboard/server/queue/DefaultTbQueueServiceDeprecated.java deleted file mode 100644 index 8f4175dad1..0000000000 --- a/common/queue/src/main/java/org/thingsboard/server/queue/DefaultTbQueueServiceDeprecated.java +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.queue; - -import lombok.RequiredArgsConstructor; -import org.springframework.util.StringUtils; -import org.thingsboard.server.common.msg.queue.ServiceQueue; -import org.thingsboard.server.common.msg.queue.ServiceType; -import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings; -import org.thingsboard.server.queue.settings.TbRuleEngineQueueConfiguration; - -import javax.annotation.PostConstruct; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.Set; -import java.util.stream.Collectors; - -//@Service -@RequiredArgsConstructor -public class DefaultTbQueueServiceDeprecated implements TbQueueServiceDeprecated { - - private final TbQueueRuleEngineSettings ruleEngineSettings; - private Set ruleEngineQueues; - - @PostConstruct - public void init() { - ruleEngineQueues = ruleEngineSettings.getQueues().stream() - .map(TbRuleEngineQueueConfiguration::getName).collect(Collectors.toCollection(LinkedHashSet::new)); - } - - @Override - public Set getQueuesByServiceType(ServiceType type) { - if (type == ServiceType.TB_RULE_ENGINE) { - return ruleEngineQueues; - } else { - return Collections.emptySet(); - } - } - - @Override - public String resolve(ServiceType serviceType, String queueName) { - if (StringUtils.isEmpty(queueName) || !getQueuesByServiceType(serviceType).contains(queueName)) { - return ServiceQueue.MAIN; - } else { - return queueName; - } - } -} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueRuleEngineSettings.java b/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueRuleEngineSettings.java index eed0b8a910..eee70e9e22 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueRuleEngineSettings.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueRuleEngineSettings.java @@ -16,32 +16,14 @@ package org.thingsboard.server.queue.settings; import lombok.Data; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; -import javax.annotation.PostConstruct; -import java.util.List; - -@Slf4j @Data -@EnableAutoConfiguration -@Configuration -@ConfigurationProperties(prefix = "queue.rule-engine") +@Component public class TbQueueRuleEngineSettings { + @Value("${queue.rule-engine.topic}") private String topic; - private List queues; - - @PostConstruct - public void validate() { - queues.stream().filter(queue -> queue.getName().equals("Main")).findFirst().orElseThrow(() -> { - log.error("Main queue is not configured in thingsboard.yml"); - return new RuntimeException("No \"Main\" queue configured!"); - }); - } } 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 097779f33e..166d142baf 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 @@ -668,6 +668,14 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC return ruleNodeDao.findRuleNodesByTenantIdAndType(tenantId, type, search); } + @Override + public List findRuleNodesByTenantIdAndType(TenantId tenantId, String type) { + log.trace("Executing findRuleNodes, tenantId [{}], type {}", tenantId, type); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateString(type, "Incorrect type of the rule node"); + return ruleNodeDao.findRuleNodesByTenantIdAndType(tenantId, type, ""); + } + @Override public RuleNode saveRuleNode(TenantId tenantId, RuleNode ruleNode) { return ruleNodeDao.save(tenantId, ruleNode); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java index 9bd51dfdf4..470a774b90 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java @@ -24,7 +24,6 @@ import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbRelationTypes; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; -import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.TbMsg; @Slf4j @@ -44,12 +43,6 @@ public class TbCheckpointNode implements TbNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { this.config = TbNodeUtils.convert(configuration, TbCheckpointNodeConfiguration.class); - if (config.getQueueId() == null) { - Queue foundQueue = ctx.getQueueService().findQueueByTenantIdAndName(ctx.getTenantId(), config.getQueueName()); - if (foundQueue != null) { - config.setQueueId(foundQueue.getId()); - } - } } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNodeConfiguration.java index b2da9142d7..0546732874 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNodeConfiguration.java @@ -22,14 +22,10 @@ import org.thingsboard.server.common.data.id.QueueId; @Data public class TbCheckpointNodeConfiguration implements NodeConfiguration { - private String queueName; - private QueueId queueId; @Override public TbCheckpointNodeConfiguration defaultConfiguration() { - TbCheckpointNodeConfiguration configuration = new TbCheckpointNodeConfiguration(); - configuration.setQueueName("HighPriority"); - return configuration; + return new TbCheckpointNodeConfiguration(); } } From bc71c22e6216c0b43c388dbb8454bed2bb5f4103 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Thu, 12 May 2022 09:00:30 +0300 Subject: [PATCH 138/276] refactoring: - Customer controller - fix bug: org.thingsboard.server.edge.sql.EdgeSqlTest.testAlarms(org.thingsboard.server.edge.sql.EdgeSqlTest) --- .../DefaultTbNotificationEntityService.java | 25 ++++++++++++++----- .../entitiy/TbNotificationEntityService.java | 3 ++- .../entitiy/alarm/DefaultTbAlarmService.java | 4 +-- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java index b5595ec552..c18e175fe2 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java @@ -71,9 +71,9 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS public void notifyDeleteEntity(TenantId tenantId, I entityId, E entity, CustomerId customerId, ActionType actionType, List relatedEdgeIds, - SecurityUser user, boolean isBody, Object... additionalInfo) { + SecurityUser user, Object... additionalInfo) { logEntityAction(tenantId, entityId, entity, customerId, actionType, user, additionalInfo); - sendDeleteNotificationMsg(tenantId, entityId, entity, relatedEdgeIds, isBody); + sendDeleteNotificationMsg(tenantId, entityId, entity, relatedEdgeIds); } @Override @@ -194,10 +194,16 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS sendEntityNotificationMsg(alarm.getTenantId(), alarm.getId(), edgeTypeByActionType (actionType)); } + @Override + public void notifyDeleteAlarm(Alarm alarm, SecurityUser user, List relatedEdgeIds) { + logEntityAction(alarm.getTenantId(), alarm.getOriginator(), alarm, alarm.getCustomerId(), ActionType.ALARM_DELETE, user, null); + sendAlarmDeleteNotificationMsg(alarm, relatedEdgeIds); + } + @Override public void notifyDeleteCustomer(Customer customer, SecurityUser user, List edgeIds) { logEntityAction(customer.getTenantId(), customer.getId(), customer, customer.getId(), ActionType.DELETED, user, null); - sendDeleteNotificationMsg(customer.getTenantId(), customer.getId(), customer, edgeIds, false); + sendDeleteNotificationMsg(customer.getTenantId(), customer.getId(), customer, edgeIds); tbClusterService.broadcastEntityStateChangeEvent(customer.getTenantId(), customer.getId(), ComponentLifecycleEvent.DELETED); } @@ -227,15 +233,22 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS } } - protected void sendDeleteNotificationMsg(TenantId tenantId, I entityId, E entity, List edgeIds, boolean isBody) { + protected void sendDeleteNotificationMsg(TenantId tenantId, I entityId, E entity, List edgeIds) { try { - String body = isBody ? json.writeValueAsString(entity) : null; - sendDeleteNotificationMsg(tenantId, entityId, edgeIds, body); + sendDeleteNotificationMsg(tenantId, entityId, edgeIds, null); } catch (Exception e) { log.warn("Failed to push delete " + entity.getClass().getName() + " msg to core: {}", entity, e); } } + protected void sendAlarmDeleteNotificationMsg(Alarm alarm, List relatedEdgeIds) { + try { + sendDeleteNotificationMsg(alarm.getTenantId(), alarm.getId(), relatedEdgeIds, json.writeValueAsString(alarm)); + } catch (Exception e) { + log.warn("Failed to push delete alarm msg to core: {}", alarm, e); + } + } + private void sendDeleteNotificationMsg(TenantId tenantId, EntityId entityId, List edgeIds, String body) { if (edgeIds != null && !edgeIds.isEmpty()) { for (EdgeId edgeId : edgeIds) { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java index ed416ca051..43c0cc4c91 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java @@ -46,7 +46,7 @@ public interface TbNotificationEntityService { void notifyDeleteEntity(TenantId tenantId, I entityId, E entity, CustomerId customerId, ActionType actionType, List relatedEdgeIds, SecurityUser user, - boolean isBody, Object... additionalInfo); + Object... additionalInfo); void notifyAssignOrUnassignEntityToCustomer(TenantId tenantId, I entityId, CustomerId customerId, E entity, @@ -81,6 +81,7 @@ public interface TbNotificationEntityService { void notifyCreateOrUpdateAlarm(Alarm alarm, ActionType actionType, SecurityUser user, Object... additionalInfo); + void notifyDeleteAlarm(Alarm alarm, SecurityUser user, List relatedEdgeIds); void notifyDeleteCustomer(Customer customer, SecurityUser user, List relatedEdgeIds); } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java index 22196c4984..1fb19e3c41 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.entitiy.alarm; - import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; @@ -79,8 +78,7 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb @Override public Boolean delete(Alarm alarm, SecurityUser user) throws ThingsboardException { List relatedEdgeIds = findRelatedEdgeIds(alarm.getTenantId(), alarm.getOriginator()); - notificationEntityService.notifyDeleteEntity(alarm.getTenantId(), alarm.getOriginator(), alarm, alarm.getCustomerId(), - ActionType.ALARM_DELETE, relatedEdgeIds, user, true); + notificationEntityService.notifyDeleteAlarm(alarm, user, relatedEdgeIds); return alarmService.deleteAlarm(alarm.getTenantId(), alarm.getId()).isSuccessful(); } } From 8c07aca7ef9999e6cb22fa7d4a1c8aaba002d9ca Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Thu, 12 May 2022 10:21:50 +0300 Subject: [PATCH 139/276] Fix tests --- .../server/dao/service/BaseDeviceCredentialsCacheTest.java | 2 +- .../thingsboard/server/dao/service/BaseRelationCacheTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java index fc1c976664..3bbe6f22ab 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java @@ -138,7 +138,7 @@ public abstract class BaseDeviceCredentialsCacheTest extends AbstractServiceTest Object target = ((Advised) deviceCredentialsService).getTargetSource().getTarget(); return (DeviceCredentialsService) target; } - return null; + return deviceCredentialsService; } private DeviceCredentials createDummyDeviceCredentialsEntity(String deviceCredentialsId) { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationCacheTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationCacheTest.java index 17dc127edb..4ef6df8015 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationCacheTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationCacheTest.java @@ -68,7 +68,7 @@ public abstract class BaseRelationCacheTest extends AbstractServiceTest { Object target = ((Advised) relationService).getTargetSource().getTarget(); return (RelationService) target; } - return null; + return relationService; } @Test From d7291d1171f1b7a4fc66113653cbb40b0cf72556 Mon Sep 17 00:00:00 2001 From: ShvaykaD Date: Thu, 12 May 2022 10:59:39 +0300 Subject: [PATCH 140/276] updated timeseries and attributes tests & cleanup code --- .../transport/mqtt/MqttTestCallback.java | 4 + ...sBackwardCompatibilityIntegrationTest.java | 17 ++- .../MqttAttributesUpdatesIntegrationTest.java | 11 +- ...tAttributesUpdatesJsonIntegrationTest.java | 11 +- ...AttributesUpdatesProtoIntegrationTest.java | 14 +- .../mqtt/claim/MqttClaimDeviceTest.java | 8 +- .../mqtt/claim/MqttClaimProtoDeviceTest.java | 1 - .../credentials/BasicMqttCredentialsTest.java | 26 +--- .../MqttAttributesIntegrationTest.java | 26 ++-- .../MqttAttributesProtoIntegrationTest.java | 10 +- ...AbstractMqttTimeseriesIntegrationTest.java | 132 +++++------------- ...ractMqttTimeseriesJsonIntegrationTest.java | 46 +++--- ...actMqttTimeseriesProtoIntegrationTest.java | 87 ++++++------ 13 files changed, 171 insertions(+), 222 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestCallback.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestCallback.java index 49533aaf37..c8c5b2560c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestCallback.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestCallback.java @@ -20,6 +20,7 @@ import lombok.extern.slf4j.Slf4j; import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; import org.eclipse.paho.client.mqttv3.MqttCallback; import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.eclipse.paho.client.mqttv3.internal.wire.MqttWireMessage; import java.util.concurrent.CountDownLatch; @@ -32,6 +33,7 @@ public class MqttTestCallback implements MqttCallback { private int qoS; private byte[] payloadBytes; private String awaitSubTopic; + private boolean pubAckReceived; public MqttTestCallback(String awaitSubTopic) { this.subscribeLatch = new CountDownLatch(1); @@ -47,6 +49,7 @@ public class MqttTestCallback implements MqttCallback { @Override public void connectionLost(Throwable throwable) { log.warn("connectionLost: ", throwable); + deliveryLatch.countDown(); } @Override @@ -69,6 +72,7 @@ public class MqttTestCallback implements MqttCallback { @Override public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { log.warn("delivery complete: {}", iMqttDeliveryToken.getResponse()); + pubAckReceived = iMqttDeliveryToken.getResponse().getType() == MqttWireMessage.MESSAGE_TYPE_PUBACK; deliveryLatch.countDown(); } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/MqttAttributesUpdatesBackwardCompatibilityIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/MqttAttributesUpdatesBackwardCompatibilityIntegrationTest.java index 2d72af2526..05253cd969 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/MqttAttributesUpdatesBackwardCompatibilityIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/MqttAttributesUpdatesBackwardCompatibilityIntegrationTest.java @@ -16,14 +16,17 @@ package org.thingsboard.server.transport.mqtt.attributes.updates; import lombok.extern.slf4j.Slf4j; -import org.junit.After; import org.junit.Test; import org.thingsboard.server.common.data.TransportPayloadType; -import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; import org.thingsboard.server.transport.mqtt.attributes.AbstractMqttAttributesIntegrationTest; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_ATTRIBUTES_TOPIC; + @Slf4j @DaoSqlTest public class MqttAttributesUpdatesBackwardCompatibilityIntegrationTest extends AbstractMqttAttributesIntegrationTest { @@ -36,7 +39,7 @@ public class MqttAttributesUpdatesBackwardCompatibilityIntegrationTest extends A .enableCompatibilityWithJsonPayloadFormat(true) .build(); processBeforeTest(configProperties); - processProtoTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_TOPIC); + processProtoTestSubscribeToAttributesUpdates(DEVICE_ATTRIBUTES_TOPIC); } @Test @@ -48,7 +51,7 @@ public class MqttAttributesUpdatesBackwardCompatibilityIntegrationTest extends A .useJsonPayloadFormatForDefaultDownlinkTopics(true) .build(); super.processBeforeTest(configProperties); - processJsonTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_TOPIC); + processJsonTestSubscribeToAttributesUpdates(DEVICE_ATTRIBUTES_TOPIC); } @Test @@ -60,7 +63,7 @@ public class MqttAttributesUpdatesBackwardCompatibilityIntegrationTest extends A .useJsonPayloadFormatForDefaultDownlinkTopics(true) .build(); super.processBeforeTest(configProperties); - processProtoTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC); + processProtoTestSubscribeToAttributesUpdates(DEVICE_ATTRIBUTES_SHORT_TOPIC); } @Test @@ -72,7 +75,7 @@ public class MqttAttributesUpdatesBackwardCompatibilityIntegrationTest extends A .useJsonPayloadFormatForDefaultDownlinkTopics(true) .build(); super.processBeforeTest(configProperties); - processJsonTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC); + processJsonTestSubscribeToAttributesUpdates(DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC); } @Test @@ -84,7 +87,7 @@ public class MqttAttributesUpdatesBackwardCompatibilityIntegrationTest extends A .useJsonPayloadFormatForDefaultDownlinkTopics(true) .build(); super.processBeforeTest(configProperties); - processProtoTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC); + processProtoTestSubscribeToAttributesUpdates(DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC); } @Test diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/MqttAttributesUpdatesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/MqttAttributesUpdatesIntegrationTest.java index 04224321d5..01a33c5b5c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/MqttAttributesUpdatesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/MqttAttributesUpdatesIntegrationTest.java @@ -19,11 +19,14 @@ import lombok.extern.slf4j.Slf4j; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.TransportPayloadType; -import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; import org.thingsboard.server.transport.mqtt.attributes.AbstractMqttAttributesIntegrationTest; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_ATTRIBUTES_TOPIC; + @Slf4j @DaoSqlTest public class MqttAttributesUpdatesIntegrationTest extends AbstractMqttAttributesIntegrationTest { @@ -40,17 +43,17 @@ public class MqttAttributesUpdatesIntegrationTest extends AbstractMqttAttributes @Test public void testJsonSubscribeToAttributesUpdatesFromTheServer() throws Exception { - processJsonTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_TOPIC); + processJsonTestSubscribeToAttributesUpdates(DEVICE_ATTRIBUTES_TOPIC); } @Test public void testJsonSubscribeToAttributesUpdatesFromTheServerOnShortTopic() throws Exception { - processJsonTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC); + processJsonTestSubscribeToAttributesUpdates(DEVICE_ATTRIBUTES_SHORT_TOPIC); } @Test public void testJsonSubscribeToAttributesUpdatesFromTheServerOnShortJsonTopic() throws Exception { - processJsonTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC); + processJsonTestSubscribeToAttributesUpdates(DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC); } @Test diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/MqttAttributesUpdatesJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/MqttAttributesUpdatesJsonIntegrationTest.java index 36d79602ec..f35ce1e084 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/MqttAttributesUpdatesJsonIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/MqttAttributesUpdatesJsonIntegrationTest.java @@ -19,11 +19,14 @@ import lombok.extern.slf4j.Slf4j; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.TransportPayloadType; -import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; import org.thingsboard.server.transport.mqtt.attributes.AbstractMqttAttributesIntegrationTest; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_ATTRIBUTES_TOPIC; + @Slf4j @DaoSqlTest public class MqttAttributesUpdatesJsonIntegrationTest extends AbstractMqttAttributesIntegrationTest { @@ -40,17 +43,17 @@ public class MqttAttributesUpdatesJsonIntegrationTest extends AbstractMqttAttrib @Test public void testJsonSubscribeToAttributesUpdatesFromTheServer() throws Exception { - processJsonTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_TOPIC); + processJsonTestSubscribeToAttributesUpdates(DEVICE_ATTRIBUTES_TOPIC); } @Test public void testJsonSubscribeToAttributesUpdatesFromTheServerOnShortTopic() throws Exception { - processJsonTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC); + processJsonTestSubscribeToAttributesUpdates(DEVICE_ATTRIBUTES_SHORT_TOPIC); } @Test public void testJsonSubscribeToAttributesUpdatesFromTheServerOnShortJsonTopic() throws Exception { - processJsonTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC); + processJsonTestSubscribeToAttributesUpdates(DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC); } @Test diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/MqttAttributesUpdatesProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/MqttAttributesUpdatesProtoIntegrationTest.java index 868707ee70..dc7e92a128 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/MqttAttributesUpdatesProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/MqttAttributesUpdatesProtoIntegrationTest.java @@ -19,11 +19,15 @@ import lombok.extern.slf4j.Slf4j; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.TransportPayloadType; -import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; import org.thingsboard.server.transport.mqtt.attributes.AbstractMqttAttributesIntegrationTest; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_ATTRIBUTES_TOPIC; + @Slf4j @DaoSqlTest public class MqttAttributesUpdatesProtoIntegrationTest extends AbstractMqttAttributesIntegrationTest { @@ -40,22 +44,22 @@ public class MqttAttributesUpdatesProtoIntegrationTest extends AbstractMqttAttri @Test public void testProtoSubscribeToAttributesUpdatesFromTheServer() throws Exception { - processProtoTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_TOPIC); + processProtoTestSubscribeToAttributesUpdates(DEVICE_ATTRIBUTES_TOPIC); } @Test public void testProtoSubscribeToAttributesUpdatesFromTheServerOnShortTopic() throws Exception { - processProtoTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC); + processProtoTestSubscribeToAttributesUpdates(DEVICE_ATTRIBUTES_SHORT_TOPIC); } @Test public void testProtoSubscribeToAttributesUpdatesFromTheServerOnShortJsonTopic() throws Exception { - processJsonTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC); + processJsonTestSubscribeToAttributesUpdates(DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC); } @Test public void testProtoSubscribeToAttributesUpdatesFromTheServerOnShortProtoTopic() throws Exception { - processProtoTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC); + processProtoTestSubscribeToAttributesUpdates(DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC); } @Test diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/MqttClaimDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/MqttClaimDeviceTest.java index 79b50941c0..df3ac12e45 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/MqttClaimDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/MqttClaimDeviceTest.java @@ -22,7 +22,6 @@ import org.thingsboard.server.common.data.ClaimRequest; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.device.claim.ClaimResponse; import org.thingsboard.server.dao.device.claim.ClaimResult; @@ -35,6 +34,7 @@ import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_CLAIM_TOPIC; import static org.thingsboard.server.common.data.device.profile.MqttTopics.GATEWAY_CLAIM_TOPIC; @Slf4j @@ -112,7 +112,7 @@ public class MqttClaimDeviceTest extends AbstractMqttIntegrationTest { } protected void validateClaimResponse(boolean emptyPayload, MqttTestClient client, byte[] payloadBytes, byte[] failurePayloadBytes) throws Exception { - client.publishAndWait(MqttTopics.DEVICE_CLAIM_TOPIC, failurePayloadBytes); + client.publishAndWait(DEVICE_CLAIM_TOPIC, failurePayloadBytes); loginUser(customerAdmin.getName(), CUSTOMER_USER_PASSWORD); ClaimRequest claimRequest; @@ -130,7 +130,8 @@ public class MqttClaimDeviceTest extends AbstractMqttIntegrationTest { assertEquals(claimResponse, ClaimResponse.FAILURE); - client.publishAndWait(MqttTopics.DEVICE_CLAIM_TOPIC, payloadBytes); + client.publishAndWait(DEVICE_CLAIM_TOPIC, payloadBytes); + client.disconnect(); ClaimResult claimResult = doExecuteWithRetriesAndInterval( () -> doPostClaimAsync("/api/customer/device/" + savedDevice.getName() + "/claim", claimRequest, ClaimResult.class, status().isOk()), @@ -170,6 +171,7 @@ public class MqttClaimDeviceTest extends AbstractMqttIntegrationTest { assertEquals(claimResponse, ClaimResponse.FAILURE); client.publishAndWait(GATEWAY_CLAIM_TOPIC, payloadBytes); + client.disconnect(); ClaimResult claimResult = doExecuteWithRetriesAndInterval( () -> doPostClaimAsync("/api/customer/device/" + deviceName + "/claim", claimRequest, ClaimResult.class, status().isOk()), diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/MqttClaimProtoDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/MqttClaimProtoDeviceTest.java index 440014f8b2..89c8c5f231 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/MqttClaimProtoDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/MqttClaimProtoDeviceTest.java @@ -16,7 +16,6 @@ package org.thingsboard.server.transport.mqtt.claim; import lombok.extern.slf4j.Slf4j; -import org.eclipse.paho.client.mqttv3.MqttAsyncClient; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.TransportPayloadType; diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/credentials/BasicMqttCredentialsTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/credentials/BasicMqttCredentialsTest.java index 795ca7f660..8ef02a63dc 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/credentials/BasicMqttCredentialsTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/credentials/BasicMqttCredentialsTest.java @@ -17,18 +17,12 @@ package org.thingsboard.server.transport.mqtt.credentials; import com.fasterxml.jackson.core.type.TypeReference; import org.apache.commons.lang3.RandomStringUtils; -import org.eclipse.paho.client.mqttv3.MqttAsyncClient; -import org.eclipse.paho.client.mqttv3.MqttConnectOptions; -import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttSecurityException; -import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; import org.junit.Before; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; -import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -44,6 +38,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_TELEMETRY_TOPIC; @DaoSqlTest public class BasicMqttCredentialsTest extends AbstractMqttIntegrationTest { @@ -138,7 +133,7 @@ public class BasicMqttCredentialsTest extends AbstractMqttIntegrationTest { private void testTelemetryIsDelivered(Device device, MqttTestClient client, boolean ok) throws Exception { String randomKey = RandomStringUtils.randomAlphanumeric(10); List expectedKeys = Arrays.asList(randomKey); - client.publishAndWait(MqttTopics.DEVICE_TELEMETRY_TOPIC, JacksonUtil.toString(JacksonUtil.newObjectNode().put(randomKey, true)).getBytes()); + client.publishAndWait(DEVICE_TELEMETRY_TOPIC, JacksonUtil.toString(JacksonUtil.newObjectNode().put(randomKey, true)).getBytes()); String deviceId = device.getId().getId().toString(); @@ -168,23 +163,6 @@ public class BasicMqttCredentialsTest extends AbstractMqttIntegrationTest { client.disconnect(); } - protected MqttAsyncClient getMqttAsyncClient(String clientId, String username, String password) throws MqttException { - if (StringUtils.isEmpty(clientId)) { - clientId = MqttAsyncClient.generateClientId(); - } - MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId, new MemoryPersistence()); - - MqttConnectOptions options = new MqttConnectOptions(); - if (StringUtils.isNotEmpty(username)) { - options.setUserName(username); - } - if (StringUtils.isNotEmpty(password)) { - options.setPassword(password.toCharArray()); - } - client.connect(options).waitForCompletion(); - return client; - } - private Device createDevice(String deviceName, BasicMqttCredentials clientIdCredValue) throws Exception { Device device = new Device(); device.setName(deviceName); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/MqttAttributesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/MqttAttributesIntegrationTest.java index ff97cf8716..f37eedfb04 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/MqttAttributesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/MqttAttributesIntegrationTest.java @@ -18,14 +18,13 @@ package org.thingsboard.server.transport.mqtt.telemetry.attributes; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import lombok.extern.slf4j.Slf4j; -import org.eclipse.paho.client.mqttv3.MqttAsyncClient; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; +import org.thingsboard.server.transport.mqtt.MqttTestClient; import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; import java.util.Arrays; @@ -38,6 +37,10 @@ import java.util.Set; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_ATTRIBUTES_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.GATEWAY_ATTRIBUTES_TOPIC; @Slf4j @DaoSqlTest @@ -58,19 +61,19 @@ public class MqttAttributesIntegrationTest extends AbstractMqttIntegrationTest { @Test public void testPushAttributes() throws Exception { List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); - processJsonPayloadAttributesTest(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, expectedKeys, PAYLOAD_VALUES_STR.getBytes()); + processJsonPayloadAttributesTest(DEVICE_ATTRIBUTES_TOPIC, expectedKeys, PAYLOAD_VALUES_STR.getBytes()); } @Test public void testPushAttributesOnShortTopic() throws Exception { List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); - processJsonPayloadAttributesTest(MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC, expectedKeys, PAYLOAD_VALUES_STR.getBytes()); + processJsonPayloadAttributesTest(DEVICE_ATTRIBUTES_SHORT_TOPIC, expectedKeys, PAYLOAD_VALUES_STR.getBytes()); } @Test public void testPushAttributesOnShortJsonTopic() throws Exception { List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); - processJsonPayloadAttributesTest(MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC, expectedKeys, PAYLOAD_VALUES_STR.getBytes()); + processJsonPayloadAttributesTest(DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC, expectedKeys, PAYLOAD_VALUES_STR.getBytes()); } @Test @@ -87,9 +90,11 @@ public class MqttAttributesIntegrationTest extends AbstractMqttIntegrationTest { } protected void processAttributesTest(String topic, List expectedKeys, byte[] payload, boolean presenceFieldsTest) throws Exception { - MqttAsyncClient client = getMqttAsyncClient(accessToken); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(accessToken); - publishMqttMsg(client, payload, topic); + client.publishAndWait(topic, payload); + client.disconnect(); DeviceId deviceId = savedDevice.getId(); @@ -125,9 +130,11 @@ public class MqttAttributesIntegrationTest extends AbstractMqttIntegrationTest { } protected void processGatewayAttributesTest(List expectedKeys, byte[] payload, String firstDeviceName, String secondDeviceName) throws Exception { - MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(gatewayAccessToken); - publishMqttMsg(client, payload, MqttTopics.GATEWAY_ATTRIBUTES_TOPIC); + client.publishAndWait(GATEWAY_ATTRIBUTES_TOPIC, payload); + client.disconnect(); Device firstDevice = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + firstDeviceName, Device.class), 20, @@ -141,6 +148,7 @@ public class MqttAttributesIntegrationTest extends AbstractMqttIntegrationTest { assertNotNull(secondDevice); + // todo removed sleep Thread.sleep(2000); List firstDeviceActualKeys = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + firstDevice.getId() + "/keys/attributes/CLIENT_SCOPE", new TypeReference<>() {}); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/MqttAttributesProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/MqttAttributesProtoIntegrationTest.java index e6295a1a3d..4904ed7a5d 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/MqttAttributesProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/MqttAttributesProtoIntegrationTest.java @@ -25,7 +25,6 @@ import org.junit.Test; import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; -import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -38,6 +37,9 @@ import java.util.List; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC; @Slf4j @DaoSqlTest @@ -122,7 +124,7 @@ public class MqttAttributesProtoIntegrationTest extends MqttAttributesIntegratio .build(); processBeforeTest(configProperties); DynamicMessage postAttributesMsg = getDefaultDynamicMessage(); - processAttributesTest(MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC, Arrays.asList("key1", "key2", "key3", "key4", "key5"), postAttributesMsg.toByteArray(), false); + processAttributesTest(DEVICE_ATTRIBUTES_SHORT_TOPIC, Arrays.asList("key1", "key2", "key3", "key4", "key5"), postAttributesMsg.toByteArray(), false); } @Test @@ -133,7 +135,7 @@ public class MqttAttributesProtoIntegrationTest extends MqttAttributesIntegratio .attributesTopicFilter(POST_DATA_ATTRIBUTES_TOPIC) .build(); processBeforeTest(configProperties); - processJsonPayloadAttributesTest(MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC, Arrays.asList("key1", "key2", "key3", "key4", "key5"), PAYLOAD_VALUES_STR.getBytes()); + processJsonPayloadAttributesTest(DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC, Arrays.asList("key1", "key2", "key3", "key4", "key5"), PAYLOAD_VALUES_STR.getBytes()); } @Test @@ -145,7 +147,7 @@ public class MqttAttributesProtoIntegrationTest extends MqttAttributesIntegratio .build(); processBeforeTest(configProperties); DynamicMessage postAttributesMsg = getDefaultDynamicMessage(); - processAttributesTest(MqttTopics.DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC, Arrays.asList("key1", "key2", "key3", "key4", "key5"), postAttributesMsg.toByteArray(), false); + processAttributesTest(DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC, Arrays.asList("key1", "key2", "key3", "key4", "key5"), postAttributesMsg.toByteArray(), false); } @Test diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java index 8b1242e592..9058107829 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java @@ -18,19 +18,13 @@ package org.thingsboard.server.transport.mqtt.telemetry.timeseries; import com.fasterxml.jackson.core.type.TypeReference; import io.netty.handler.codec.mqtt.MqttQoS; import lombok.extern.slf4j.Slf4j; -import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; -import org.eclipse.paho.client.mqttv3.MqttAsyncClient; -import org.eclipse.paho.client.mqttv3.MqttCallback; -import org.eclipse.paho.client.mqttv3.MqttConnectOptions; -import org.eclipse.paho.client.mqttv3.MqttMessage; -import org.eclipse.paho.client.mqttv3.internal.wire.MqttWireMessage; -import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; +import org.thingsboard.server.transport.mqtt.MqttTestCallback; +import org.thingsboard.server.transport.mqtt.MqttTestClient; import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; import java.util.Arrays; @@ -38,12 +32,17 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_ATTRIBUTES_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_TELEMETRY_SHORT_JSON_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_TELEMETRY_SHORT_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_TELEMETRY_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.GATEWAY_CONNECT_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.GATEWAY_TELEMETRY_TOPIC; @Slf4j public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqttIntegrationTest { @@ -66,26 +65,26 @@ public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqtt @Test public void testPushTelemetry() throws Exception { List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); - processJsonPayloadTelemetryTest(MqttTopics.DEVICE_TELEMETRY_TOPIC, expectedKeys, PAYLOAD_VALUES_STR.getBytes(), false); + processJsonPayloadTelemetryTest(DEVICE_TELEMETRY_TOPIC, expectedKeys, PAYLOAD_VALUES_STR.getBytes(), false); } @Test public void testPushTelemetryWithTs() throws Exception { String payloadStr = "{\"ts\": 10000, \"values\": " + PAYLOAD_VALUES_STR + "}"; List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); - processJsonPayloadTelemetryTest(MqttTopics.DEVICE_TELEMETRY_TOPIC, expectedKeys, payloadStr.getBytes(), true); + processJsonPayloadTelemetryTest(DEVICE_TELEMETRY_TOPIC, expectedKeys, payloadStr.getBytes(), true); } @Test public void testPushTelemetryOnShortTopic() throws Exception { List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); - processJsonPayloadTelemetryTest(MqttTopics.DEVICE_TELEMETRY_SHORT_TOPIC, expectedKeys, PAYLOAD_VALUES_STR.getBytes(), false); + processJsonPayloadTelemetryTest(DEVICE_TELEMETRY_SHORT_TOPIC, expectedKeys, PAYLOAD_VALUES_STR.getBytes(), false); } @Test public void testPushTelemetryOnShortJsonTopic() throws Exception { List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); - processJsonPayloadTelemetryTest(MqttTopics.DEVICE_TELEMETRY_SHORT_JSON_TOPIC, expectedKeys, PAYLOAD_VALUES_STR.getBytes(), false); + processJsonPayloadTelemetryTest(DEVICE_TELEMETRY_SHORT_JSON_TOPIC, expectedKeys, PAYLOAD_VALUES_STR.getBytes(), false); } @Test @@ -94,14 +93,15 @@ public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqtt String deviceName1 = "Device A"; String deviceName2 = "Device B"; String payload = getGatewayTelemetryJsonPayload(deviceName1, deviceName2, "10000", "20000"); - processGatewayTelemetryTest(MqttTopics.GATEWAY_TELEMETRY_TOPIC, expectedKeys, payload.getBytes(), deviceName1, deviceName2); + processGatewayTelemetryTest(GATEWAY_TELEMETRY_TOPIC, expectedKeys, payload.getBytes(), deviceName1, deviceName2); } @Test public void testGatewayConnect() throws Exception { String payload = "{\"device\":\"Device A\"}"; - MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); - publishMqttMsg(client, payload.getBytes(), MqttTopics.GATEWAY_CONNECT_TOPIC); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(gatewayAccessToken); + client.publish(GATEWAY_CONNECT_TOPIC, payload.getBytes()); String deviceName = "Device A"; @@ -110,6 +110,7 @@ public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqtt 100); assertNotNull(device); + client.disconnect(); } protected void processJsonPayloadTelemetryTest(String topic, List expectedKeys, byte[] payload, boolean withTs) throws Exception { @@ -117,8 +118,10 @@ public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqtt } protected void processTelemetryTest(String topic, List expectedKeys, byte[] payload, boolean withTs, boolean presenceFieldsTest) throws Exception { - MqttAsyncClient client = getMqttAsyncClient(accessToken); - publishMqttMsg(client, payload, topic); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(accessToken); + client.publishAndWait(topic, payload); + client.disconnect(); String deviceId = savedDevice.getId().getId().toString(); @@ -187,9 +190,10 @@ public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqtt } protected void processGatewayTelemetryTest(String topic, List expectedKeys, byte[] payload, String firstDeviceName, String secondDeviceName) throws Exception { - MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); - - publishMqttMsg(client, payload, topic); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(gatewayAccessToken); + client.publishAndWait(topic, payload); + client.disconnect(); Device firstDevice = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + firstDeviceName, Device.class), 20, @@ -203,6 +207,7 @@ public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqtt assertNotNull(secondDevice); + // TODO remove sleep Thread.sleep(2000); List firstDeviceActualKeys = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + firstDevice.getId() + "/keys/timeseries", new TypeReference<>() {}); @@ -312,93 +317,20 @@ public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqtt // @Test - Unstable public void testMqttQoSLevel() throws Exception { - String clientId = MqttAsyncClient.generateClientId(); - MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId, new MemoryPersistence()); - - MqttConnectOptions options = new MqttConnectOptions(); - options.setUserName(accessToken); - CountDownLatch latch = new CountDownLatch(1); - TestMqttCallback callback = new TestMqttCallback(client, latch); + MqttTestClient client = new MqttTestClient(); + MqttTestCallback callback = new MqttTestCallback(); client.setCallback(callback); - client.connect(options).waitForCompletion(5000); - client.subscribe("v1/devices/me/attributes", MqttQoS.AT_MOST_ONCE.value()); + client.connectAndWait(accessToken); + client.subscribe(DEVICE_ATTRIBUTES_TOPIC, MqttQoS.AT_MOST_ONCE); String payload = "{\"key\":\"uniqueValue\"}"; // TODO 3.1: we need to acknowledge subscription only after it is processed by device actor and not when the message is pushed to queue. // MqttClient -> SUB REQUEST -> Transport -> Kafka -> Device Actor (subscribed) // MqttClient <- SUB_ACK <- Transport Thread.sleep(5000); doPostAsync("/api/plugins/telemetry/" + savedDevice.getId() + "/SHARED_SCOPE", payload, String.class, status().isOk()); - latch.await(10, TimeUnit.SECONDS); - assertEquals(payload, callback.getPayload()); + callback.getSubscribeLatch().await(10, TimeUnit.SECONDS); + assertEquals(payload.getBytes(), callback.getPayloadBytes()); assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); } - private static class TestMqttCallback implements MqttCallback { - - private final MqttAsyncClient client; - private final CountDownLatch latch; - private volatile Integer qoS; - private volatile String payload; - - String getPayload() { - return payload; - } - - TestMqttCallback(MqttAsyncClient client, CountDownLatch latch) { - this.client = client; - this.latch = latch; - } - - int getQoS() { - return qoS; - } - - @Override - public void connectionLost(Throwable throwable) { - log.error("Client connection lost", throwable); - } - - @Override - public void messageArrived(String requestTopic, MqttMessage mqttMessage) { - payload = new String(mqttMessage.getPayload()); - qoS = mqttMessage.getQos(); - latch.countDown(); - } - - @Override - public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { - - } - } - - public static class TestMqttPublishCallback implements MqttCallback { - - private final CountDownLatch latch; - private boolean pubAckReceived; - - public boolean isPubAckReceived() { - return pubAckReceived; - } - - public TestMqttPublishCallback(CountDownLatch latch) { - this.latch = latch; - } - - @Override - public void connectionLost(Throwable throwable) { - latch.countDown(); - } - - @Override - public void messageArrived(String s, MqttMessage mqttMessage) throws Exception { - } - - @Override - public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { - pubAckReceived = iMqttDeliveryToken.getResponse().getType() == MqttWireMessage.MESSAGE_TYPE_PUBACK; - latch.countDown(); - } - } - - } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java index 920a5692a5..4a7037f6e8 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java @@ -16,15 +16,15 @@ package org.thingsboard.server.transport.mqtt.telemetry.timeseries; import lombok.extern.slf4j.Slf4j; -import org.eclipse.paho.client.mqttv3.MqttAsyncClient; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.transport.mqtt.MqttTestCallback; +import org.thingsboard.server.transport.mqtt.MqttTestClient; import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; import java.util.Arrays; import java.util.List; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertFalse; @@ -121,13 +121,14 @@ public abstract class AbstractMqttTimeseriesJsonIntegrationTest extends Abstract .sendAckOnValidationException(true) .build(); processBeforeTest(configProperties); - CountDownLatch latch = new CountDownLatch(1); - MqttAsyncClient client = getMqttAsyncClient(accessToken); - TestMqttPublishCallback callback = new TestMqttPublishCallback(latch); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(accessToken); + MqttTestCallback callback = new MqttTestCallback(); client.setCallback(callback); - publishMqttMsg(client, MALFORMED_JSON_PAYLOAD.getBytes(), POST_DATA_TELEMETRY_TOPIC); - latch.await(3, TimeUnit.SECONDS); + client.publish(POST_DATA_TELEMETRY_TOPIC, MALFORMED_JSON_PAYLOAD.getBytes()); + callback.getDeliveryLatch().await(3, TimeUnit.SECONDS); assertTrue(callback.isPubAckReceived()); + client.disconnect(); } @Test @@ -138,12 +139,12 @@ public abstract class AbstractMqttTimeseriesJsonIntegrationTest extends Abstract .telemetryTopicFilter(POST_DATA_TELEMETRY_TOPIC) .build(); processBeforeTest(configProperties); - CountDownLatch latch = new CountDownLatch(1); - MqttAsyncClient client = getMqttAsyncClient(accessToken); - TestMqttPublishCallback callback = new TestMqttPublishCallback(latch); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(accessToken); + MqttTestCallback callback = new MqttTestCallback(); client.setCallback(callback); - publishMqttMsg(client, MALFORMED_JSON_PAYLOAD.getBytes(), POST_DATA_TELEMETRY_TOPIC); - latch.await(3, TimeUnit.SECONDS); + client.publish(POST_DATA_TELEMETRY_TOPIC, MALFORMED_JSON_PAYLOAD.getBytes()); + callback.getDeliveryLatch().await(3, TimeUnit.SECONDS); assertFalse(callback.isPubAckReceived()); } @@ -157,13 +158,14 @@ public abstract class AbstractMqttTimeseriesJsonIntegrationTest extends Abstract .sendAckOnValidationException(true) .build(); processBeforeTest(configProperties); - CountDownLatch latch = new CountDownLatch(1); - MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); - TestMqttPublishCallback callback = new TestMqttPublishCallback(latch); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(gatewayAccessToken); + MqttTestCallback callback = new MqttTestCallback(); client.setCallback(callback); - publishMqttMsg(client, MALFORMED_JSON_PAYLOAD.getBytes(), POST_DATA_TELEMETRY_TOPIC); - latch.await(3, TimeUnit.SECONDS); + client.publish(POST_DATA_TELEMETRY_TOPIC, MALFORMED_JSON_PAYLOAD.getBytes()); + callback.getDeliveryLatch().await(3, TimeUnit.SECONDS); assertTrue(callback.isPubAckReceived()); + client.disconnect(); } @Test @@ -175,12 +177,12 @@ public abstract class AbstractMqttTimeseriesJsonIntegrationTest extends Abstract .telemetryTopicFilter(POST_DATA_TELEMETRY_TOPIC) .build(); processBeforeTest(configProperties); - CountDownLatch latch = new CountDownLatch(1); - MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); - TestMqttPublishCallback callback = new TestMqttPublishCallback(latch); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(gatewayAccessToken); + MqttTestCallback callback = new MqttTestCallback(); client.setCallback(callback); - publishMqttMsg(client, MALFORMED_JSON_PAYLOAD.getBytes(), POST_DATA_TELEMETRY_TOPIC); - latch.await(3, TimeUnit.SECONDS); + client.publish(POST_DATA_TELEMETRY_TOPIC, MALFORMED_JSON_PAYLOAD.getBytes()); + callback.getDeliveryLatch().await(3, TimeUnit.SECONDS); assertFalse(callback.isPubAckReceived()); } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java index f9bf5471ef..95de5f0069 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java @@ -20,28 +20,32 @@ import com.google.protobuf.Descriptors; import com.google.protobuf.DynamicMessage; import com.squareup.wire.schema.internal.parser.ProtoFileElement; import lombok.extern.slf4j.Slf4j; -import org.eclipse.paho.client.mqttv3.MqttAsyncClient; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; -import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; import org.thingsboard.server.gen.transport.TransportApiProtos; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.transport.mqtt.MqttTestCallback; +import org.thingsboard.server.transport.mqtt.MqttTestClient; import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; import java.util.Arrays; import java.util.List; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_TELEMETRY_SHORT_JSON_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_TELEMETRY_SHORT_PROTO_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_TELEMETRY_SHORT_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.GATEWAY_CONNECT_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.GATEWAY_TELEMETRY_TOPIC; @Slf4j public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends AbstractMqttTimeseriesIntegrationTest { @@ -272,7 +276,7 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac .build(); processBeforeTest(configProperties); DynamicMessage postTelemetryMsg = getDefaultDynamicMessage(); - processTelemetryTest(MqttTopics.DEVICE_TELEMETRY_SHORT_TOPIC, Arrays.asList("key1", "key2", "key3", "key4", "key5"), postTelemetryMsg.toByteArray(), false, false); + processTelemetryTest(DEVICE_TELEMETRY_SHORT_TOPIC, Arrays.asList("key1", "key2", "key3", "key4", "key5"), postTelemetryMsg.toByteArray(), false, false); } @Test @@ -283,7 +287,7 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac .telemetryTopicFilter(POST_DATA_TELEMETRY_TOPIC) .build(); processBeforeTest(configProperties); - processJsonPayloadTelemetryTest(MqttTopics.DEVICE_TELEMETRY_SHORT_JSON_TOPIC, Arrays.asList("key1", "key2", "key3", "key4", "key5"), PAYLOAD_VALUES_STR.getBytes(), false); + processJsonPayloadTelemetryTest(DEVICE_TELEMETRY_SHORT_JSON_TOPIC, Arrays.asList("key1", "key2", "key3", "key4", "key5"), PAYLOAD_VALUES_STR.getBytes(), false); } @Test @@ -295,7 +299,7 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac .build(); processBeforeTest(configProperties); DynamicMessage postTelemetryMsg = getDefaultDynamicMessage(); - processTelemetryTest(MqttTopics.DEVICE_TELEMETRY_SHORT_PROTO_TOPIC, Arrays.asList("key1", "key2", "key3", "key4", "key5"), postTelemetryMsg.toByteArray(), false, false); + processTelemetryTest(DEVICE_TELEMETRY_SHORT_PROTO_TOPIC, Arrays.asList("key1", "key2", "key3", "key4", "key5"), postTelemetryMsg.toByteArray(), false, false); } @Test @@ -315,7 +319,7 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac TransportApiProtos.TelemetryMsg deviceBTelemetryMsgProto = getDeviceTelemetryMsgProto(deviceName2, expectedKeys, 10000, 20000); gatewayTelemetryMsgProtoBuilder.addAllMsg(Arrays.asList(deviceATelemetryMsgProto, deviceBTelemetryMsgProto)); TransportApiProtos.GatewayTelemetryMsg gatewayTelemetryMsg = gatewayTelemetryMsgProtoBuilder.build(); - processGatewayTelemetryTest(MqttTopics.GATEWAY_TELEMETRY_TOPIC, expectedKeys, gatewayTelemetryMsg.toByteArray(), deviceName1, deviceName2); + processGatewayTelemetryTest(GATEWAY_TELEMETRY_TOPIC, expectedKeys, gatewayTelemetryMsg.toByteArray(), deviceName1, deviceName2); } @Test @@ -329,14 +333,16 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac processBeforeTest(configProperties); String deviceName = "Device A"; TransportApiProtos.ConnectMsg connectMsgProto = getConnectProto(deviceName); - MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); - publishMqttMsg(client, connectMsgProto.toByteArray(), MqttTopics.GATEWAY_CONNECT_TOPIC); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(gatewayAccessToken); + client.publish(GATEWAY_CONNECT_TOPIC, connectMsgProto.toByteArray()); Device device = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class), 20, 100); assertNotNull(device); + client.disconnect(); } @@ -349,13 +355,14 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac .sendAckOnValidationException(true) .build(); processBeforeTest(configProperties); - CountDownLatch latch = new CountDownLatch(1); - MqttAsyncClient client = getMqttAsyncClient(accessToken); - TestMqttPublishCallback callback = new TestMqttPublishCallback(latch); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(accessToken); + MqttTestCallback callback = new MqttTestCallback(); client.setCallback(callback); - publishMqttMsg(client, MALFORMED_PROTO_PAYLOAD.getBytes(), POST_DATA_TELEMETRY_TOPIC); - latch.await(3, TimeUnit.SECONDS); + client.publish(POST_DATA_TELEMETRY_TOPIC, MALFORMED_PROTO_PAYLOAD.getBytes()); + callback.getDeliveryLatch().await(3, TimeUnit.SECONDS); assertTrue(callback.isPubAckReceived()); + client.disconnect(); } @Test @@ -366,12 +373,12 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac .telemetryTopicFilter(POST_DATA_TELEMETRY_TOPIC) .build(); processBeforeTest(configProperties); - CountDownLatch latch = new CountDownLatch(1); - MqttAsyncClient client = getMqttAsyncClient(accessToken); - TestMqttPublishCallback callback = new TestMqttPublishCallback(latch); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(accessToken); + MqttTestCallback callback = new MqttTestCallback(); client.setCallback(callback); - publishMqttMsg(client, MALFORMED_PROTO_PAYLOAD.getBytes(), POST_DATA_TELEMETRY_TOPIC); - latch.await(3, TimeUnit.SECONDS); + client.publish(POST_DATA_TELEMETRY_TOPIC, MALFORMED_PROTO_PAYLOAD.getBytes()); + callback.getDeliveryLatch().await(3, TimeUnit.SECONDS); assertFalse(callback.isPubAckReceived()); } @@ -385,13 +392,14 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac .sendAckOnValidationException(true) .build(); processBeforeTest(configProperties); - CountDownLatch latch = new CountDownLatch(1); - MqttAsyncClient client = getMqttAsyncClient(accessToken); - TestMqttPublishCallback callback = new TestMqttPublishCallback(latch); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(accessToken); + MqttTestCallback callback = new MqttTestCallback(); client.setCallback(callback); - publishMqttMsg(client, MALFORMED_JSON_PAYLOAD.getBytes(), POST_DATA_TELEMETRY_TOPIC); - latch.await(3, TimeUnit.SECONDS); + client.publish(POST_DATA_TELEMETRY_TOPIC, MALFORMED_JSON_PAYLOAD.getBytes()); + callback.getDeliveryLatch().await(3, TimeUnit.SECONDS); assertTrue(callback.isPubAckReceived()); + client.disconnect(); } @Test @@ -403,12 +411,12 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac .enableCompatibilityWithJsonPayloadFormat(true) .build(); processBeforeTest(configProperties); - CountDownLatch latch = new CountDownLatch(1); - MqttAsyncClient client = getMqttAsyncClient(accessToken); - TestMqttPublishCallback callback = new TestMqttPublishCallback(latch); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(accessToken); + MqttTestCallback callback = new MqttTestCallback(); client.setCallback(callback); - publishMqttMsg(client, MALFORMED_JSON_PAYLOAD.getBytes(), POST_DATA_TELEMETRY_TOPIC); - latch.await(3, TimeUnit.SECONDS); + client.publish(POST_DATA_TELEMETRY_TOPIC, MALFORMED_JSON_PAYLOAD.getBytes()); + callback.getDeliveryLatch().await(3, TimeUnit.SECONDS); assertFalse(callback.isPubAckReceived()); } @@ -422,13 +430,14 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac .sendAckOnValidationException(true) .build(); processBeforeTest(configProperties); - CountDownLatch latch = new CountDownLatch(1); - MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); - TestMqttPublishCallback callback = new TestMqttPublishCallback(latch); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(gatewayAccessToken); + MqttTestCallback callback = new MqttTestCallback(); client.setCallback(callback); - publishMqttMsg(client, MALFORMED_PROTO_PAYLOAD.getBytes(), POST_DATA_TELEMETRY_TOPIC); - latch.await(3, TimeUnit.SECONDS); + client.publish(POST_DATA_TELEMETRY_TOPIC, MALFORMED_PROTO_PAYLOAD.getBytes()); + callback.getDeliveryLatch().await(3, TimeUnit.SECONDS); assertTrue(callback.isPubAckReceived()); + client.disconnect(); } @Test @@ -440,12 +449,12 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac .telemetryTopicFilter(POST_DATA_TELEMETRY_TOPIC) .build(); processBeforeTest(configProperties); - CountDownLatch latch = new CountDownLatch(1); - MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); - TestMqttPublishCallback callback = new TestMqttPublishCallback(latch); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(gatewayAccessToken); + MqttTestCallback callback = new MqttTestCallback(); client.setCallback(callback); - publishMqttMsg(client, MALFORMED_PROTO_PAYLOAD.getBytes(), POST_DATA_TELEMETRY_TOPIC); - latch.await(3, TimeUnit.SECONDS); + client.publish(POST_DATA_TELEMETRY_TOPIC, MALFORMED_PROTO_PAYLOAD.getBytes()); + callback.getDeliveryLatch().await(3, TimeUnit.SECONDS); assertFalse(callback.isPubAckReceived()); } From 0879b1afae268a8c9c88304478d527122056db29 Mon Sep 17 00:00:00 2001 From: ShvaykaD Date: Thu, 12 May 2022 14:04:26 +0300 Subject: [PATCH 141/276] replaced thread sleeps in telemetry tests --- .../MqttAttributesIntegrationTest.java | 24 ++++++++--- ...AbstractMqttTimeseriesIntegrationTest.java | 40 ++++++++++--------- 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/MqttAttributesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/MqttAttributesIntegrationTest.java index f37eedfb04..1d986d0453 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/MqttAttributesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/MqttAttributesIntegrationTest.java @@ -148,13 +148,12 @@ public class MqttAttributesIntegrationTest extends AbstractMqttIntegrationTest { assertNotNull(secondDevice); - // todo removed sleep - Thread.sleep(2000); - - List firstDeviceActualKeys = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + firstDevice.getId() + "/keys/attributes/CLIENT_SCOPE", new TypeReference<>() {}); + List firstDeviceActualKeys = getActualKeysList(firstDevice.getId(), expectedKeys); + assertNotNull(firstDeviceActualKeys); Set firstDeviceActualKeySet = new HashSet<>(firstDeviceActualKeys); - List secondDeviceActualKeys = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + secondDevice.getId() + "/keys/attributes/CLIENT_SCOPE", new TypeReference<>() {}); + List secondDeviceActualKeys = getActualKeysList(secondDevice.getId(), expectedKeys); + assertNotNull(secondDeviceActualKeys); Set secondDeviceActualKeySet = new HashSet<>(secondDeviceActualKeys); Set expectedKeySet = new HashSet<>(expectedKeys); @@ -173,6 +172,21 @@ public class MqttAttributesIntegrationTest extends AbstractMqttIntegrationTest { } + private List getActualKeysList(DeviceId deviceId, List expectedKeys) throws Exception { + long start = System.currentTimeMillis(); + long end = System.currentTimeMillis() + 3000; + List firstDeviceActualKeys = null; + while (start <= end) { + firstDeviceActualKeys = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + deviceId + "/keys/attributes/CLIENT_SCOPE", new TypeReference<>() {}); + if (firstDeviceActualKeys.size() == expectedKeys.size()) { + break; + } + Thread.sleep(100); + start += 100; + } + return firstDeviceActualKeys; + } + @SuppressWarnings("unchecked") protected void assertAttributesValues(List> deviceValues, Set keySet) throws JsonProcessingException { for (Map map : deviceValues) { diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java index 9058107829..85faebb590 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java @@ -123,20 +123,11 @@ public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqtt client.publishAndWait(topic, payload); client.disconnect(); - String deviceId = savedDevice.getId().getId().toString(); + DeviceId deviceId = savedDevice.getId(); - long start = System.currentTimeMillis(); - long end = System.currentTimeMillis() + 5000; - - List actualKeys = null; - while (start <= end) { - actualKeys = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + deviceId + "/keys/timeseries", new TypeReference<>() {}); - if (actualKeys.size() == expectedKeys.size()) { - break; - } - Thread.sleep(100); - start += 100; - } + List actualKeys = getActualKeysList(deviceId, expectedKeys); + long end; + long start; assertNotNull(actualKeys); Set actualKeySet = new HashSet<>(actualKeys); @@ -207,13 +198,10 @@ public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqtt assertNotNull(secondDevice); - // TODO remove sleep - Thread.sleep(2000); - - List firstDeviceActualKeys = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + firstDevice.getId() + "/keys/timeseries", new TypeReference<>() {}); + List firstDeviceActualKeys = getActualKeysList(firstDevice.getId(), expectedKeys); Set firstDeviceActualKeySet = new HashSet<>(firstDeviceActualKeys); - List secondDeviceActualKeys = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + secondDevice.getId() + "/keys/timeseries", new TypeReference<>() {}); + List secondDeviceActualKeys = getActualKeysList(secondDevice.getId(), expectedKeys); Set secondDeviceActualKeySet = new HashSet<>(secondDeviceActualKeys); Set expectedKeySet = new HashSet<>(expectedKeys); @@ -231,6 +219,22 @@ public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqtt assertGatewayDeviceData(secondDeviceValues, expectedKeys); } + private List getActualKeysList(DeviceId deviceId, List expectedKeys) throws Exception { + long start = System.currentTimeMillis(); + long end = System.currentTimeMillis() + 3000; + + List actualKeys = null; + while (start <= end) { + actualKeys = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + deviceId + "/keys/timeseries", new TypeReference<>() {}); + if (actualKeys.size() == expectedKeys.size()) { + break; + } + Thread.sleep(100); + start += 100; + } + return actualKeys; + } + protected String getGatewayTelemetryJsonPayload(String deviceA, String deviceB, String firstTsValue, String secondTsValue) { String payload = "[{\"ts\": " + firstTsValue + ", \"values\": " + PAYLOAD_VALUES_STR + "}, " + "{\"ts\": " + secondTsValue + ", \"values\": " + PAYLOAD_VALUES_STR + "}]"; From 838370b62531e44f83cccaa739f89a0a71a8c6bc Mon Sep 17 00:00:00 2001 From: ShvaykaD Date: Thu, 12 May 2022 14:07:06 +0300 Subject: [PATCH 142/276] fix typo in AbstractMqttTimeseriesIntegrationTest --- .../timeseries/AbstractMqttTimeseriesIntegrationTest.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java index 85faebb590..5bb0d28a21 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java @@ -126,8 +126,6 @@ public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqtt DeviceId deviceId = savedDevice.getId(); List actualKeys = getActualKeysList(deviceId, expectedKeys); - long end; - long start; assertNotNull(actualKeys); Set actualKeySet = new HashSet<>(actualKeys); @@ -141,8 +139,8 @@ public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqtt } else { getTelemetryValuesUrl = "/api/plugins/telemetry/DEVICE/" + deviceId + "/values/timeseries?keys=" + String.join(",", actualKeySet); } - start = System.currentTimeMillis(); - end = System.currentTimeMillis() + 5000; + long start = System.currentTimeMillis(); + long end = System.currentTimeMillis() + 5000; Map>> values = null; while (start <= end) { values = doGetAsyncTyped(getTelemetryValuesUrl, new TypeReference<>() {}); From 61b07f6031ce809337c1526c3551ab91e14b77ac Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 12 May 2022 14:31:47 +0300 Subject: [PATCH 143/276] UI: Fix json content component css --- ui-ngx/src/app/shared/components/json-content.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/components/json-content.component.scss b/ui-ngx/src/app/shared/components/json-content.component.scss index 124092718a..b85b48b5da 100644 --- a/ui-ngx/src/app/shared/components/json-content.component.scss +++ b/ui-ngx/src/app/shared/components/json-content.component.scss @@ -16,7 +16,7 @@ .tb-json-content { position: relative; - .fill-height { + &.fill-height { height: 100%; } From 23485d43b0add13a2704f984006d8f13e8f7db1d Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Thu, 12 May 2022 15:23:04 +0300 Subject: [PATCH 144/276] Improvements to TbCustomerService --- .../server/controller/CustomerController.java | 30 +++++-------------- .../entitiy/SimpleTbEntityService.java | 25 ++++++++++++++++ .../service/entitiy/alarm/TbAlarmService.java | 12 ++++---- .../service/entitiy/asset/TbAssetService.java | 4 +-- .../entitiy/customer/TbCustomerService.java | 7 +++-- .../entitiy/tenant/TbTenantService.java | 2 ++ 6 files changed, 46 insertions(+), 34 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/entitiy/SimpleTbEntityService.java diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java index 8d67fe02de..2fc4b7c748 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java @@ -149,10 +149,10 @@ public class CustomerController extends BaseController { @RequestMapping(value = "/customer", method = RequestMethod.POST) @ResponseBody public Customer saveCustomer(@ApiParam(value = "A JSON value representing the customer.") @RequestBody Customer customer) throws ThingsboardException { - customer.setTenantId(getCurrentUser().getTenantId()); - checkEntity(customer.getId(), customer, Resource.CUSTOMER); - return tbCustomerService.save(customer, getCurrentUser()); - } + customer.setTenantId(getCurrentUser().getTenantId()); + checkEntity(customer.getId(), customer, Resource.CUSTOMER); + return tbCustomerService.save(customer, getCurrentUser()); + } @ApiOperation(value = "Delete Customer (deleteCustomer)", notes = "Deletes the Customer and all customer Users. " + @@ -164,27 +164,11 @@ public class CustomerController extends BaseController { public void deleteCustomer(@ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable(CUSTOMER_ID) String strCustomerId) throws ThingsboardException { checkParameter(CUSTOMER_ID, strCustomerId); + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + Customer customer = checkCustomerId(customerId, Operation.DELETE); try { - CustomerId customerId = new CustomerId(toUUID(strCustomerId)); - Customer customer = checkCustomerId(customerId, Operation.DELETE); - - List relatedEdgeIds = findRelatedEdgeIds(getTenantId(), customerId); - - customerService.deleteCustomer(getTenantId(), customerId); - - logEntityAction(customerId, customer, - customer.getId(), - ActionType.DELETED, null, strCustomerId); - - sendDeleteNotificationMsg(getTenantId(), customerId, relatedEdgeIds); - tbClusterService.broadcastEntityStateChangeEvent(getTenantId(), customerId, ComponentLifecycleEvent.DELETED); + tbCustomerService.delete(customer, getCurrentUser()); } catch (Exception e) { - - logEntityAction(emptyId(EntityType.CUSTOMER), - null, - null, - ActionType.DELETED, e, strCustomerId); - throw handleException(e); } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/SimpleTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/SimpleTbEntityService.java new file mode 100644 index 0000000000..84f5658015 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/SimpleTbEntityService.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.entitiy; + +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.service.security.model.SecurityUser; + +public interface SimpleTbEntityService { + + T save(T entity, SecurityUser user) throws ThingsboardException; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java index 7c3613d730..f73e44bc69 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java @@ -16,16 +16,16 @@ package org.thingsboard.server.service.entitiy.alarm; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.service.entitiy.SimpleTbEntityService; import org.thingsboard.server.service.security.model.SecurityUser; -public interface TbAlarmService { +public interface TbAlarmService extends SimpleTbEntityService { - Alarm save (Alarm alarm, SecurityUser user) throws ThingsboardException; + void ack(Alarm alarm, SecurityUser user) throws ThingsboardException; - void ack (Alarm alarm, SecurityUser user) throws ThingsboardException; + void clear(Alarm alarm, SecurityUser user) throws ThingsboardException; - void clear (Alarm alarm, SecurityUser user) throws ThingsboardException; - - Boolean delete (Alarm alarm, SecurityUser user) throws ThingsboardException; + Boolean delete(Alarm alarm, SecurityUser user) throws ThingsboardException; } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java index c6e1c29940..3bc6dadf3d 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java @@ -22,10 +22,10 @@ import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.service.entitiy.SimpleTbEntityService; import org.thingsboard.server.service.security.model.SecurityUser; -public interface TbAssetService { - Asset save(Asset asset, SecurityUser user) throws ThingsboardException; +public interface TbAssetService extends SimpleTbEntityService { ListenableFuture delete(Asset asset, SecurityUser user) throws ThingsboardException; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java index 36c5a0c0ed..f34d796db1 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java @@ -17,10 +17,11 @@ package org.thingsboard.server.service.entitiy.customer; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.service.entitiy.SimpleTbEntityService; import org.thingsboard.server.service.security.model.SecurityUser; -public interface TbCustomerService { - Customer save (Customer customer, SecurityUser user) throws ThingsboardException; +public interface TbCustomerService extends SimpleTbEntityService { + + void delete(Customer customer, SecurityUser user) throws ThingsboardException; - void delete (Customer customer, SecurityUser user) throws ThingsboardException; } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/TbTenantService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/TbTenantService.java index 0ff7bc749c..c265b568cb 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/TbTenantService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/TbTenantService.java @@ -19,7 +19,9 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.exception.ThingsboardException; public interface TbTenantService { + Tenant save(Tenant tenant) throws ThingsboardException; void delete(Tenant tenant) throws ThingsboardException; + } From adc675822e3dc14d8f0f9fae03886303182588f0 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Thu, 12 May 2022 16:40:38 +0300 Subject: [PATCH 145/276] Remove redundant test that breaks every time someone adds new service --- .../server/actors/ActorSystemContextTest.java | 257 ------------------ 1 file changed, 257 deletions(-) delete mode 100644 application/src/test/java/org/thingsboard/server/actors/ActorSystemContextTest.java diff --git a/application/src/test/java/org/thingsboard/server/actors/ActorSystemContextTest.java b/application/src/test/java/org/thingsboard/server/actors/ActorSystemContextTest.java deleted file mode 100644 index ae1398f3a3..0000000000 --- a/application/src/test/java/org/thingsboard/server/actors/ActorSystemContextTest.java +++ /dev/null @@ -1,257 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.actors; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.thingsboard.rule.engine.api.MailService; -import org.thingsboard.rule.engine.api.SmsService; -import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; -import org.thingsboard.server.actors.service.ActorService; -import org.thingsboard.server.cluster.TbClusterService; -import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; -import org.thingsboard.server.dao.asset.AssetService; -import org.thingsboard.server.dao.attributes.AttributesService; -import org.thingsboard.server.dao.audit.AuditLogService; -import org.thingsboard.server.dao.cassandra.CassandraCluster; -import org.thingsboard.server.dao.customer.CustomerService; -import org.thingsboard.server.dao.dashboard.DashboardService; -import org.thingsboard.server.dao.device.ClaimDevicesService; -import org.thingsboard.server.dao.device.DeviceService; -import org.thingsboard.server.dao.edge.EdgeEventService; -import org.thingsboard.server.dao.edge.EdgeService; -import org.thingsboard.server.dao.entityview.EntityViewService; -import org.thingsboard.server.dao.event.EventService; -import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor; -import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor; -import org.thingsboard.server.dao.ota.OtaPackageService; -import org.thingsboard.server.dao.relation.RelationService; -import org.thingsboard.server.dao.resource.ResourceService; -import org.thingsboard.server.dao.rule.RuleChainService; -import org.thingsboard.server.dao.rule.RuleNodeStateService; -import org.thingsboard.server.dao.tenant.TbTenantProfileCache; -import org.thingsboard.server.dao.tenant.TenantProfileService; -import org.thingsboard.server.dao.tenant.TenantService; -import org.thingsboard.server.dao.timeseries.TimeseriesService; -import org.thingsboard.server.dao.user.UserService; -import org.thingsboard.server.queue.discovery.PartitionService; -import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; -import org.thingsboard.server.queue.usagestats.TbApiUsageClient; -import org.thingsboard.server.service.apiusage.TbApiUsageStateService; -import org.thingsboard.server.service.component.ComponentDiscoveryService; -import org.thingsboard.server.service.edge.rpc.EdgeRpcService; -import org.thingsboard.server.service.executors.DbCallbackExecutorService; -import org.thingsboard.server.service.executors.ExternalCallExecutorService; -import org.thingsboard.server.service.executors.SharedEventLoopGroupService; -import org.thingsboard.server.service.mail.MailExecutorService; -import org.thingsboard.server.service.profile.TbDeviceProfileCache; -import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService; -import org.thingsboard.server.service.rpc.TbRpcService; -import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService; -import org.thingsboard.server.service.script.JsInvokeService; -import org.thingsboard.server.service.session.DeviceSessionCacheService; -import org.thingsboard.server.service.sms.SmsExecutorService; -import org.thingsboard.server.service.state.DeviceStateService; -import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; -import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; -import org.thingsboard.server.service.transport.TbCoreToTransportService; - -import static org.assertj.core.api.Assertions.assertThat; - -@ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = ActorSystemContext.class) -@EnableConfigurationProperties -@TestPropertySource(properties = { - "cache.type=caffeine", -}) -public class ActorSystemContextTest { - - @Autowired - ActorSystemContext ctx; - - @MockBean - private TbApiUsageStateService apiUsageStateService; - - @MockBean - private TbApiUsageClient apiUsageClient; - - @MockBean - private TbServiceInfoProvider serviceInfoProvider; - - @MockBean - private ActorService actorService; - - @MockBean - private ComponentDiscoveryService componentService; - - @MockBean - private DataDecodingEncodingService encodingService; - - @MockBean - private DeviceService deviceService; - - @MockBean - private TbTenantProfileCache tenantProfileCache; - - @MockBean - private TbDeviceProfileCache deviceProfileCache; - - @MockBean - private AssetService assetService; - - @MockBean - private DashboardService dashboardService; - - @MockBean - private TenantService tenantService; - - @MockBean - private TenantProfileService tenantProfileService; - - @MockBean - private CustomerService customerService; - - @MockBean - private UserService userService; - - @MockBean - private RuleChainService ruleChainService; - - @MockBean - private RuleNodeStateService ruleNodeStateService; - - @MockBean - private PartitionService partitionService; - - @MockBean - private TbClusterService clusterService; - - @MockBean - private TimeseriesService tsService; - - @MockBean - private AttributesService attributesService; - - @MockBean - private EventService eventService; - - @MockBean - private RelationService relationService; - - @MockBean - private AuditLogService auditLogService; - - @MockBean - private EntityViewService entityViewService; - - @MockBean - private TelemetrySubscriptionService tsSubService; - - @MockBean - private AlarmSubscriptionService alarmService; - - @MockBean - private JsInvokeService jsSandbox; - - @MockBean - private MailExecutorService mailExecutor; - - @MockBean - private SmsExecutorService smsExecutor; - - @MockBean - private DbCallbackExecutorService dbCallbackExecutor; - - @MockBean - private ExternalCallExecutorService externalCallExecutorService; - - @MockBean - private SharedEventLoopGroupService sharedEventLoopGroupService; - - @MockBean - private MailService mailService; - - @MockBean - private SmsService smsService; - - @MockBean - private SmsSenderFactory smsSenderFactory; - - @MockBean - private ClaimDevicesService claimDevicesService; - - @MockBean - private JsInvokeStats jsInvokeStats; - - @MockBean - private DeviceStateService deviceStateService; - - @MockBean - private DeviceSessionCacheService deviceSessionCacheService; - - @MockBean - private TbCoreToTransportService tbCoreToTransportService; - - @MockBean - private TbRuleEngineDeviceRpcService tbRuleEngineDeviceRpcService; - - @MockBean - private TbCoreDeviceRpcService tbCoreDeviceRpcService; - - @MockBean - private EdgeService edgeService; - - @MockBean - private EdgeEventService edgeEventService; - - @MockBean - private EdgeRpcService edgeRpcService; - - @MockBean - private ResourceService resourceService; - - @MockBean - private OtaPackageService otaPackageService; - - @MockBean - private TbRpcService tbRpcService; - - @MockBean - private CassandraCluster cassandraCluster; - - @MockBean - private CassandraBufferedRateReadExecutor cassandraBufferedRateReadExecutor; - - @MockBean - private CassandraBufferedRateWriteExecutor cassandraBufferedRateWriteExecutor; - - @MockBean - private RedisTemplate redisTemplate; - - @Test - void givenCaffeineCache_whenInit_thenIsLocalCacheTrue() { - assertThat(ctx.getCacheType()).isEqualTo("caffeine"); - assertThat(ctx.isLocalCacheType()).as("caffeine is the local cache type").isTrue(); - } - -} From a352452093a3946ed408a3701c36ec82b205b4a3 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Fri, 13 May 2022 10:11:06 +0300 Subject: [PATCH 146/276] Move Device Cache to common package --- .../service/session/SessionCaffeineCache.java | 2 +- .../service/session/SessionRedisCache.java | 5 +- .../cache/CaffeineTbCacheTransaction.java | 3 +- .../cache/CaffeineTbTransactionalCache.java | 5 +- .../cache/RedisTbCacheTransaction.java | 3 +- .../cache/RedisTbTransactionalCache.java | 17 +++---- .../cache/SimpleTbCacheValueWrapper.java | 3 +- .../server}/cache/TbRedisSerializer.java | 2 +- .../cache}/device/DeviceCacheEvictEvent.java | 4 +- .../server/cache}/device/DeviceCacheKey.java | 2 +- .../cache}/device/DeviceCaffeineCache.java | 4 +- .../cache}/device/DeviceRedisCache.java | 6 +-- .../server/dao/asset/AssetCaffeineCache.java | 2 +- .../server/dao/asset/AssetRedisCache.java | 4 +- .../attributes/AttributeCaffeineCache.java | 2 +- .../dao/attributes/AttributeRedisCache.java | 4 +- .../CaffeineCacheTransactionStorage.java | 30 ------------ ...eviousDeviceCredentialsIdKeyGenerator.java | 46 ------------------- .../DeviceCredentialsCaffeineCache.java | 2 +- .../device/DeviceCredentialsRedisCache.java | 4 +- .../device/DeviceProfileCaffeineCache.java | 2 +- .../dao/device/DeviceProfileRedisCache.java | 4 +- .../server/dao/device/DeviceServiceImpl.java | 3 +- .../server/dao/edge/EdgeCaffeineCache.java | 2 +- .../server/dao/edge/EdgeRedisCache.java | 4 +- .../entityview/EntityViewCaffeineCache.java | 2 +- .../dao/entityview/EntityViewRedisCache.java | 4 +- .../dao/ota/OtaPackageCaffeineCache.java | 2 +- .../server/dao/ota/OtaPackageRedisCache.java | 4 +- .../dao/relation/RelationCaffeineCache.java | 2 +- .../dao/relation/RelationRedisCache.java | 4 +- .../tenant/TenantProfileCaffeineCache.java | 2 +- .../dao/tenant/TenantProfileRedisCache.java | 4 +- 33 files changed, 50 insertions(+), 139 deletions(-) rename {dao/src/main/java/org/thingsboard/server/dao => common/cache/src/main/java/org/thingsboard/server}/cache/CaffeineTbCacheTransaction.java (94%) rename {dao/src/main/java/org/thingsboard/server/dao => common/cache/src/main/java/org/thingsboard/server}/cache/CaffeineTbTransactionalCache.java (96%) rename {dao/src/main/java/org/thingsboard/server/dao => common/cache/src/main/java/org/thingsboard/server}/cache/RedisTbCacheTransaction.java (94%) rename {dao/src/main/java/org/thingsboard/server/dao => common/cache/src/main/java/org/thingsboard/server}/cache/RedisTbTransactionalCache.java (90%) rename {dao/src/main/java/org/thingsboard/server/dao => common/cache/src/main/java/org/thingsboard/server}/cache/SimpleTbCacheValueWrapper.java (93%) rename {dao/src/main/java/org/thingsboard/server/dao => common/cache/src/main/java/org/thingsboard/server}/cache/TbRedisSerializer.java (96%) rename {dao/src/main/java/org/thingsboard/server/dao => common/cache/src/main/java/org/thingsboard/server/cache}/device/DeviceCacheEvictEvent.java (91%) rename {dao/src/main/java/org/thingsboard/server/dao => common/cache/src/main/java/org/thingsboard/server/cache}/device/DeviceCacheKey.java (97%) rename {dao/src/main/java/org/thingsboard/server/dao => common/cache/src/main/java/org/thingsboard/server/cache}/device/DeviceCaffeineCache.java (91%) rename {dao/src/main/java/org/thingsboard/server/dao => common/cache/src/main/java/org/thingsboard/server/cache}/device/DeviceRedisCache.java (89%) delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineCacheTransactionStorage.java delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/cache/PreviousDeviceCredentialsIdKeyGenerator.java diff --git a/application/src/main/java/org/thingsboard/server/service/session/SessionCaffeineCache.java b/application/src/main/java/org/thingsboard/server/service/session/SessionCaffeineCache.java index 97bfc95f01..92eba5c4e2 100644 --- a/application/src/main/java/org/thingsboard/server/service/session/SessionCaffeineCache.java +++ b/application/src/main/java/org/thingsboard/server/service/session/SessionCaffeineCache.java @@ -20,7 +20,7 @@ import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; import org.thingsboard.server.gen.transport.TransportProtos; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) diff --git a/application/src/main/java/org/thingsboard/server/service/session/SessionRedisCache.java b/application/src/main/java/org/thingsboard/server/service/session/SessionRedisCache.java index 210410fe8e..cd3e8cb56b 100644 --- a/application/src/main/java/org/thingsboard/server/service/session/SessionRedisCache.java +++ b/application/src/main/java/org/thingsboard/server/service/session/SessionRedisCache.java @@ -24,11 +24,8 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.cache.CacheSpecsMap; import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.dao.asset.AssetCacheKey; -import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; -import org.thingsboard.server.dao.cache.TbRedisSerializer; +import org.thingsboard.server.cache.RedisTbTransactionalCache; import org.thingsboard.server.gen.transport.TransportProtos; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbCacheTransaction.java b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbCacheTransaction.java similarity index 94% rename from dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbCacheTransaction.java rename to common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbCacheTransaction.java index dad5da394d..00231ad674 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbCacheTransaction.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbCacheTransaction.java @@ -13,13 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.cache; +package org.thingsboard.server.cache; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.server.cache.TbCacheTransaction; import java.io.Serializable; import java.util.LinkedHashMap; diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCache.java similarity index 96% rename from dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbTransactionalCache.java rename to common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCache.java index d9fb5cd774..a18402493c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineTbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCache.java @@ -13,14 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.cache; +package org.thingsboard.server.cache; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.cache.CacheManager; -import org.thingsboard.server.cache.TbCacheTransaction; -import org.thingsboard.server.cache.TbCacheValueWrapper; -import org.thingsboard.server.cache.TbTransactionalCache; import java.io.Serializable; import java.util.Collection; diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/RedisTbCacheTransaction.java b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbCacheTransaction.java similarity index 94% rename from dao/src/main/java/org/thingsboard/server/dao/cache/RedisTbCacheTransaction.java rename to common/cache/src/main/java/org/thingsboard/server/cache/RedisTbCacheTransaction.java index ee0329efaf..bd4ac13543 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/RedisTbCacheTransaction.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbCacheTransaction.java @@ -13,13 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.cache; +package org.thingsboard.server.cache; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisStringCommands; -import org.thingsboard.server.cache.TbCacheTransaction; import java.io.Serializable; import java.util.Objects; diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/RedisTbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java similarity index 90% rename from dao/src/main/java/org/thingsboard/server/dao/cache/RedisTbTransactionalCache.java rename to common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java index 426dbc8cec..ed39bf3716 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/RedisTbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.cache; +package org.thingsboard.server.cache; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -24,12 +24,6 @@ import org.springframework.data.redis.connection.RedisStringCommands; import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; -import org.thingsboard.server.cache.CacheSpecs; -import org.thingsboard.server.cache.CacheSpecsMap; -import org.thingsboard.server.cache.TBRedisCacheConfiguration; -import org.thingsboard.server.cache.TbCacheTransaction; -import org.thingsboard.server.cache.TbCacheValueWrapper; -import org.thingsboard.server.cache.TbTransactionalCache; import java.io.Serializable; import java.util.Arrays; @@ -59,11 +53,12 @@ public abstract class RedisTbTransactionalCache implements TbCacheValueWrapper { diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/TbRedisSerializer.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbRedisSerializer.java similarity index 96% rename from dao/src/main/java/org/thingsboard/server/dao/cache/TbRedisSerializer.java rename to common/cache/src/main/java/org/thingsboard/server/cache/TbRedisSerializer.java index 6ffb50c941..574038ea91 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/TbRedisSerializer.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbRedisSerializer.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.cache; +package org.thingsboard.server.cache; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.SerializationException; diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCacheEvictEvent.java b/common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCacheEvictEvent.java similarity index 91% rename from dao/src/main/java/org/thingsboard/server/dao/device/DeviceCacheEvictEvent.java rename to common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCacheEvictEvent.java index b6f8ab4beb..d192b97249 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCacheEvictEvent.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCacheEvictEvent.java @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.device; +package org.thingsboard.server.cache.device; import lombok.Data; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; @Data -class DeviceCacheEvictEvent { +public class DeviceCacheEvictEvent { private final TenantId tenantId; private final DeviceId deviceId; diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCacheKey.java b/common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCacheKey.java similarity index 97% rename from dao/src/main/java/org/thingsboard/server/dao/device/DeviceCacheKey.java rename to common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCacheKey.java index 6e62a07e1b..77e08a0fd8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCacheKey.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCacheKey.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.device; +package org.thingsboard.server.cache.device; import lombok.Builder; import lombok.EqualsAndHashCode; diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCaffeineCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCaffeineCache.java similarity index 91% rename from dao/src/main/java/org/thingsboard/server/dao/device/DeviceCaffeineCache.java rename to common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCaffeineCache.java index 2349acbc53..09e97ed205 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCaffeineCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCaffeineCache.java @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.device; +package org.thingsboard.server.cache.device; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) @Service("DeviceCache") diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceRedisCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceRedisCache.java similarity index 89% rename from dao/src/main/java/org/thingsboard/server/dao/device/DeviceRedisCache.java rename to common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceRedisCache.java index e79b6d52e5..99577f0039 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceRedisCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceRedisCache.java @@ -13,17 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.device; +package org.thingsboard.server.cache.device; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.stereotype.Service; import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.RedisTbTransactionalCache; import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.cache.TbRedisSerializer; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; -import org.thingsboard.server.dao.cache.TbRedisSerializer; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("DeviceCache") diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCaffeineCache.java index daced88f34..7d9aa706d0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCaffeineCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetCaffeineCache.java @@ -20,7 +20,7 @@ import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) @Service("AssetCache") diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetRedisCache.java index d343ec1d35..9a8526f3cf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetRedisCache.java @@ -22,8 +22,8 @@ import org.thingsboard.server.cache.CacheSpecsMap; import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; -import org.thingsboard.server.dao.cache.TbRedisSerializer; +import org.thingsboard.server.cache.RedisTbTransactionalCache; +import org.thingsboard.server.cache.TbRedisSerializer; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("AssetCache") diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCaffeineCache.java index 01dbde9998..5a44f47110 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCaffeineCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCaffeineCache.java @@ -20,7 +20,7 @@ import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) @Service("AttributeCache") diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java index 1f10027fda..80c52105fd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java @@ -22,8 +22,8 @@ import org.thingsboard.server.cache.CacheSpecsMap; import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; -import org.thingsboard.server.dao.cache.TbRedisSerializer; +import org.thingsboard.server.cache.RedisTbTransactionalCache; +import org.thingsboard.server.cache.TbRedisSerializer; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("AttributeCache") diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineCacheTransactionStorage.java b/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineCacheTransactionStorage.java deleted file mode 100644 index 1b1cee537a..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineCacheTransactionStorage.java +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.dao.cache; - -import lombok.RequiredArgsConstructor; - -import java.io.Serializable; - -@RequiredArgsConstructor -class CaffeineCacheTransactionStorage { - - - - - - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/PreviousDeviceCredentialsIdKeyGenerator.java b/dao/src/main/java/org/thingsboard/server/dao/cache/PreviousDeviceCredentialsIdKeyGenerator.java deleted file mode 100644 index 15c560caed..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/PreviousDeviceCredentialsIdKeyGenerator.java +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ -package org.thingsboard.server.dao.cache; - -import org.springframework.cache.interceptor.KeyGenerator; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.security.DeviceCredentials; -import org.thingsboard.server.dao.device.DeviceCredentialsService; - -import java.lang.reflect.Method; - -import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CREDENTIALS_CACHE; - -@Component("previousDeviceCredentialsId") -public class PreviousDeviceCredentialsIdKeyGenerator implements KeyGenerator { - - private static final String NOT_VALID_DEVICE = DEVICE_CREDENTIALS_CACHE + "_notValidDeviceCredentialsId"; - - @Override - public Object generate(Object o, Method method, Object... objects) { - DeviceCredentialsService deviceCredentialsService = (DeviceCredentialsService) o; - TenantId tenantId = (TenantId) objects[0]; - DeviceCredentials deviceCredentials = (DeviceCredentials) objects[1]; - if (deviceCredentials.getDeviceId() != null) { - DeviceCredentials oldDeviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, deviceCredentials.getDeviceId()); - if (oldDeviceCredentials != null) { - return DEVICE_CREDENTIALS_CACHE + "_" + oldDeviceCredentials.getCredentialsId(); - } - } - return NOT_VALID_DEVICE; - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsCaffeineCache.java index 8744cdf924..1568363275 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsCaffeineCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsCaffeineCache.java @@ -20,7 +20,7 @@ import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.security.DeviceCredentials; -import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) @Service("DeviceCredentialsCache") diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsRedisCache.java index 4e2f4b9af5..1d8a9455dc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsRedisCache.java @@ -22,8 +22,8 @@ import org.thingsboard.server.cache.CacheSpecsMap; import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.security.DeviceCredentials; -import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; -import org.thingsboard.server.dao.cache.TbRedisSerializer; +import org.thingsboard.server.cache.RedisTbTransactionalCache; +import org.thingsboard.server.cache.TbRedisSerializer; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("DeviceCredentialsCache") diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCaffeineCache.java index b6d2448487..2e369dec9c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCaffeineCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCaffeineCache.java @@ -20,7 +20,7 @@ import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) @Service("DeviceProfileCache") diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileRedisCache.java index 5a046a3ba7..f3d7ad634f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileRedisCache.java @@ -22,8 +22,8 @@ import org.thingsboard.server.cache.CacheSpecsMap; import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; -import org.thingsboard.server.dao.cache.TbRedisSerializer; +import org.thingsboard.server.cache.RedisTbTransactionalCache; +import org.thingsboard.server.cache.TbRedisSerializer; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("DeviceProfileCache") diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index afa6258a16..c486473e38 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -24,11 +24,12 @@ import org.apache.commons.lang3.RandomStringUtils; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; import org.springframework.util.CollectionUtils; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.cache.device.DeviceCacheEvictEvent; +import org.thingsboard.server.cache.device.DeviceCacheKey; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.DeviceProfile; diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCaffeineCache.java index dd8677218c..1a7956e3a0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCaffeineCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCaffeineCache.java @@ -20,7 +20,7 @@ import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) @Service("EdgeCache") diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeRedisCache.java index dc444f693e..ab0b44a525 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeRedisCache.java @@ -22,8 +22,8 @@ import org.thingsboard.server.cache.CacheSpecsMap; import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; -import org.thingsboard.server.dao.cache.TbRedisSerializer; +import org.thingsboard.server.cache.RedisTbTransactionalCache; +import org.thingsboard.server.cache.TbRedisSerializer; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("EdgeCache") diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCaffeineCache.java index 7a8176e0f3..38d144814a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCaffeineCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCaffeineCache.java @@ -19,7 +19,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) @Service("EntityViewCache") diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewRedisCache.java index 77beb90211..e2ade492ff 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewRedisCache.java @@ -21,8 +21,8 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.cache.CacheSpecsMap; import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; -import org.thingsboard.server.dao.cache.TbRedisSerializer; +import org.thingsboard.server.cache.RedisTbTransactionalCache; +import org.thingsboard.server.cache.TbRedisSerializer; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("EntityViewCache") diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCaffeineCache.java index 02eb967091..b1b5faa50d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCaffeineCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageCaffeineCache.java @@ -20,7 +20,7 @@ import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.OtaPackageInfo; -import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) @Service("OtaPackageCache") diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageRedisCache.java index 35ac206c10..1257932b79 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageRedisCache.java @@ -22,8 +22,8 @@ import org.thingsboard.server.cache.CacheSpecsMap; import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.OtaPackageInfo; -import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; -import org.thingsboard.server.dao.cache.TbRedisSerializer; +import org.thingsboard.server.cache.RedisTbTransactionalCache; +import org.thingsboard.server.cache.TbRedisSerializer; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("OtaPackageCache") diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCaffeineCache.java index c795d2498a..a2fad34a56 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCaffeineCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationCaffeineCache.java @@ -19,7 +19,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) @Service("RelationCache") diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationRedisCache.java index 602337ef56..b849d36f63 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationRedisCache.java @@ -21,8 +21,8 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.cache.CacheSpecsMap; import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; -import org.thingsboard.server.dao.cache.TbRedisSerializer; +import org.thingsboard.server.cache.RedisTbTransactionalCache; +import org.thingsboard.server.cache.TbRedisSerializer; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("RelationCache") diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCaffeineCache.java index 6d6fa531b5..aca7a63d48 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCaffeineCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileCaffeineCache.java @@ -20,7 +20,7 @@ import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.TenantProfile; -import org.thingsboard.server.dao.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) @Service("TenantProfileCache") diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileRedisCache.java index 47592f83c5..da83473f44 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileRedisCache.java @@ -22,8 +22,8 @@ import org.thingsboard.server.cache.CacheSpecsMap; import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.TenantProfile; -import org.thingsboard.server.dao.cache.RedisTbTransactionalCache; -import org.thingsboard.server.dao.cache.TbRedisSerializer; +import org.thingsboard.server.cache.RedisTbTransactionalCache; +import org.thingsboard.server.cache.TbRedisSerializer; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("TenantProfileCache") From 6534db8ed2932931578be9cc52277657c2242f7d Mon Sep 17 00:00:00 2001 From: Jan Christoph Bernack Date: Fri, 13 May 2022 14:54:50 +0200 Subject: [PATCH 147/276] add fallback for piggybackTimeout of zero --- .../server/transport/coap/CoapTransportResource.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java index 983aec497c..0016cc05d8 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java @@ -355,7 +355,11 @@ public class CoapTransportResource extends AbstractCoapTransportResource { * Essentially this allows the use of piggybacked responses. */ private void deferAccept(CoapExchange exchange) { - transportContext.getScheduler().schedule(exchange::accept, piggybackTimeout, TimeUnit.MILLISECONDS); + if (piggybackTimeout > 0) { + transportContext.getScheduler().schedule(exchange::accept, piggybackTimeout, TimeUnit.MILLISECONDS); + } else { + exchange.accept(); + } } private UUID toSessionId(TransportProtos.SessionInfoProto sessionInfoProto) { From b85e33bf1045904abe45a3c1b106b68e29099a4d Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Fri, 13 May 2022 17:58:08 +0300 Subject: [PATCH 148/276] refactoring: - Dashboard controller --- .../controller/DashboardController.java | 307 ++++-------------- .../entitiy/AbstractTbEntityService.java | 4 +- .../dashboard/DefaultTbDashboardService.java | 254 +++++++++++++++ .../entitiy/dashboard/TbDashboardService.java | 43 +++ 4 files changed, 363 insertions(+), 245 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index 49c33045a5..98fc25cc54 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -22,6 +22,7 @@ import io.swagger.annotations.ApiParam; import io.swagger.annotations.ApiResponse; import io.swagger.annotations.Example; import io.swagger.annotations.ExampleProperty; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -41,7 +42,6 @@ import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HomeDashboard; import org.thingsboard.server.common.data.HomeDashboardInfo; -import org.thingsboard.server.common.data.ShortCustomerInfo; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; @@ -55,13 +55,12 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.dashboard.TbDashboardService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.stream.Collectors; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID; @@ -90,9 +89,11 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI @RestController @TbCoreComponent +@RequiredArgsConstructor @RequestMapping("/api") public class DashboardController extends BaseController { + private final TbDashboardService tbDashboardService; public static final String DASHBOARD_ID = "dashboardId"; private static final String HOME_DASHBOARD_ID = "homeDashboardId"; @@ -180,28 +181,9 @@ public class DashboardController extends BaseController { public Dashboard saveDashboard( @ApiParam(value = "A JSON value representing the dashboard.") @RequestBody Dashboard dashboard) throws ThingsboardException { - try { - dashboard.setTenantId(getCurrentUser().getTenantId()); - - checkEntity(dashboard.getId(), dashboard, Resource.DASHBOARD); - - Dashboard savedDashboard = checkNotNull(dashboardService.saveDashboard(dashboard)); - - logEntityAction(savedDashboard.getId(), savedDashboard, - null, - dashboard.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); - - if (dashboard.getId() != null) { - sendEntityNotificationMsg(savedDashboard.getTenantId(), savedDashboard.getId(), EdgeEventActionType.UPDATED); - } - - return savedDashboard; - } catch (Exception e) { - logEntityAction(emptyId(EntityType.DASHBOARD), dashboard, - null, dashboard.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e); - - throw handleException(e); - } + dashboard.setTenantId(getCurrentUser().getTenantId()); + checkEntity(dashboard.getId(), dashboard, Resource.DASHBOARD); + return tbDashboardService.save(dashboard, getCurrentUser()); } @ApiOperation(value = "Delete the Dashboard (deleteDashboard)", @@ -251,30 +233,13 @@ public class DashboardController extends BaseController { @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { checkParameter(CUSTOMER_ID, strCustomerId); checkParameter(DASHBOARD_ID, strDashboardId); - try { - CustomerId customerId = new CustomerId(toUUID(strCustomerId)); - Customer customer = checkCustomerId(customerId, Operation.READ); - - DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); - checkDashboardId(dashboardId, Operation.ASSIGN_TO_CUSTOMER); - - Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(getCurrentUser().getTenantId(), dashboardId, customerId)); - logEntityAction(dashboardId, savedDashboard, - customerId, - ActionType.ASSIGNED_TO_CUSTOMER, null, strDashboardId, strCustomerId, customer.getName()); + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + Customer customer = checkCustomerId(customerId, Operation.READ); - sendEntityAssignToCustomerNotificationMsg(savedDashboard.getTenantId(), savedDashboard.getId(), customerId, EdgeEventActionType.ASSIGNED_TO_CUSTOMER); - - return savedDashboard; - } catch (Exception e) { - - logEntityAction(emptyId(EntityType.DASHBOARD), null, - null, - ActionType.ASSIGNED_TO_CUSTOMER, e, strDashboardId, strCustomerId); - - throw handleException(e); - } + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + checkDashboardId(dashboardId, Operation.ASSIGN_TO_CUSTOMER); + return tbDashboardService.assignDashboardToCustomer(getTenantId(), dashboardId, customer, getCurrentUser()); } @ApiOperation(value = "Unassign the Dashboard (unassignDashboardFromCustomer)", @@ -331,69 +296,14 @@ public class DashboardController extends BaseController { @ApiParam(value = "JSON array with the list of customer ids, or empty to remove all customers") @RequestBody(required = false) String[] strCustomerIds) throws ThingsboardException { checkParameter(DASHBOARD_ID, strDashboardId); - try { - DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); - Dashboard dashboard = checkDashboardId(dashboardId, Operation.ASSIGN_TO_CUSTOMER); - - Set customerIds = new HashSet<>(); - if (strCustomerIds != null) { - for (String strCustomerId : strCustomerIds) { - customerIds.add(new CustomerId(toUUID(strCustomerId))); - } - } - - Set addedCustomerIds = new HashSet<>(); - Set removedCustomerIds = new HashSet<>(); - for (CustomerId customerId : customerIds) { - if (!dashboard.isAssignedToCustomer(customerId)) { - addedCustomerIds.add(customerId); - } - } - - Set assignedCustomers = dashboard.getAssignedCustomers(); - if (assignedCustomers != null) { - for (ShortCustomerInfo customerInfo : assignedCustomers) { - if (!customerIds.contains(customerInfo.getCustomerId())) { - removedCustomerIds.add(customerInfo.getCustomerId()); - } - } - } - - if (addedCustomerIds.isEmpty() && removedCustomerIds.isEmpty()) { - return dashboard; - } else { - Dashboard savedDashboard = null; - for (CustomerId customerId : addedCustomerIds) { - savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(getCurrentUser().getTenantId(), dashboardId, customerId)); - ShortCustomerInfo customerInfo = savedDashboard.getAssignedCustomerInfo(customerId); - logEntityAction(dashboardId, savedDashboard, - customerId, - ActionType.ASSIGNED_TO_CUSTOMER, null, strDashboardId, customerId.toString(), customerInfo.getTitle()); - sendEntityAssignToCustomerNotificationMsg(savedDashboard.getTenantId(), savedDashboard.getId(), customerId, EdgeEventActionType.ASSIGNED_TO_CUSTOMER); - } - for (CustomerId customerId : removedCustomerIds) { - ShortCustomerInfo customerInfo = dashboard.getAssignedCustomerInfo(customerId); - savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(getCurrentUser().getTenantId(), dashboardId, customerId)); - logEntityAction(dashboardId, dashboard, - customerId, - ActionType.UNASSIGNED_FROM_CUSTOMER, null, strDashboardId, customerId.toString(), customerInfo.getTitle()); - sendEntityAssignToCustomerNotificationMsg(savedDashboard.getTenantId(), savedDashboard.getId(), customerId, EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER); - } - return savedDashboard; - } - } catch (Exception e) { - - logEntityAction(emptyId(EntityType.DASHBOARD), null, - null, - ActionType.ASSIGNED_TO_CUSTOMER, e, strDashboardId); - - throw handleException(e); - } + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + Dashboard dashboard = checkDashboardId(dashboardId, Operation.ASSIGN_TO_CUSTOMER); + return tbDashboardService.updateDashboardCustomers(getTenantId(), dashboard, strCustomerIds, getCurrentUser()); } @ApiOperation(value = "Adds the Dashboard Customers (addDashboardCustomers)", notes = "Adds the list of Customers to the existing list of assignments for the Dashboard. Keeps previous assignments to customers that are not in the provided list. " + - "Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH, + "Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @@ -405,42 +315,9 @@ public class DashboardController extends BaseController { @ApiParam(value = "JSON array with the list of customer ids") @RequestBody String[] strCustomerIds) throws ThingsboardException { checkParameter(DASHBOARD_ID, strDashboardId); - try { - DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); - Dashboard dashboard = checkDashboardId(dashboardId, Operation.ASSIGN_TO_CUSTOMER); - - Set customerIds = new HashSet<>(); - if (strCustomerIds != null) { - for (String strCustomerId : strCustomerIds) { - CustomerId customerId = new CustomerId(toUUID(strCustomerId)); - if (!dashboard.isAssignedToCustomer(customerId)) { - customerIds.add(customerId); - } - } - } - - if (customerIds.isEmpty()) { - return dashboard; - } else { - Dashboard savedDashboard = null; - for (CustomerId customerId : customerIds) { - savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(getCurrentUser().getTenantId(), dashboardId, customerId)); - ShortCustomerInfo customerInfo = savedDashboard.getAssignedCustomerInfo(customerId); - logEntityAction(dashboardId, savedDashboard, - customerId, - ActionType.ASSIGNED_TO_CUSTOMER, null, strDashboardId, customerId.toString(), customerInfo.getTitle()); - sendEntityAssignToCustomerNotificationMsg(savedDashboard.getTenantId(), savedDashboard.getId(), customerId, EdgeEventActionType.ASSIGNED_TO_CUSTOMER); - } - return savedDashboard; - } - } catch (Exception e) { - - logEntityAction(emptyId(EntityType.DASHBOARD), null, - null, - ActionType.ASSIGNED_TO_CUSTOMER, e, strDashboardId); - - throw handleException(e); - } + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + Dashboard dashboard = checkDashboardId(dashboardId, Operation.ASSIGN_TO_CUSTOMER); + return tbDashboardService.addDashboardCustomers(getTenantId(), dashboard, strCustomerIds, getCurrentUser()); } @ApiOperation(value = "Remove the Dashboard Customers (removeDashboardCustomers)", @@ -457,42 +334,10 @@ public class DashboardController extends BaseController { @ApiParam(value = "JSON array with the list of customer ids") @RequestBody String[] strCustomerIds) throws ThingsboardException { checkParameter(DASHBOARD_ID, strDashboardId); - try { - DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); - Dashboard dashboard = checkDashboardId(dashboardId, Operation.UNASSIGN_FROM_CUSTOMER); + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + Dashboard dashboard = checkDashboardId(dashboardId, Operation.UNASSIGN_FROM_CUSTOMER); + return tbDashboardService.removeDashboardCustomers(getTenantId(), dashboard, strCustomerIds, getCurrentUser()); - Set customerIds = new HashSet<>(); - if (strCustomerIds != null) { - for (String strCustomerId : strCustomerIds) { - CustomerId customerId = new CustomerId(toUUID(strCustomerId)); - if (dashboard.isAssignedToCustomer(customerId)) { - customerIds.add(customerId); - } - } - } - - if (customerIds.isEmpty()) { - return dashboard; - } else { - Dashboard savedDashboard = null; - for (CustomerId customerId : customerIds) { - ShortCustomerInfo customerInfo = dashboard.getAssignedCustomerInfo(customerId); - savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(getCurrentUser().getTenantId(), dashboardId, customerId)); - logEntityAction(dashboardId, dashboard, - customerId, - ActionType.UNASSIGNED_FROM_CUSTOMER, null, strDashboardId, customerId.toString(), customerInfo.getTitle()); - sendEntityAssignToCustomerNotificationMsg(savedDashboard.getTenantId(), savedDashboard.getId(), customerId, EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER); - } - return savedDashboard; - } - } catch (Exception e) { - - logEntityAction(emptyId(EntityType.DASHBOARD), null, - null, - ActionType.UNASSIGNED_FROM_CUSTOMER, e, strDashboardId); - - throw handleException(e); - } } @ApiOperation(value = "Assign the Dashboard to Public Customer (assignDashboardToPublicCustomer)", @@ -510,26 +355,10 @@ public class DashboardController extends BaseController { @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION) @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { checkParameter(DASHBOARD_ID, strDashboardId); - try { - DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); - Dashboard dashboard = checkDashboardId(dashboardId, Operation.ASSIGN_TO_CUSTOMER); - Customer publicCustomer = customerService.findOrCreatePublicCustomer(dashboard.getTenantId()); - Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(getCurrentUser().getTenantId(), dashboardId, publicCustomer.getId())); - - logEntityAction(dashboardId, savedDashboard, - publicCustomer.getId(), - ActionType.ASSIGNED_TO_CUSTOMER, null, strDashboardId, publicCustomer.getId().toString(), publicCustomer.getName()); - - return savedDashboard; - } catch (Exception e) { - - logEntityAction(emptyId(EntityType.DASHBOARD), null, - null, - ActionType.ASSIGNED_TO_CUSTOMER, e, strDashboardId); - - throw handleException(e); - } - } + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + checkDashboardId(dashboardId, Operation.ASSIGN_TO_CUSTOMER); + return tbDashboardService.assignDashboardToPublicCustomer(getTenantId(), dashboardId, getCurrentUser()); + } @ApiOperation(value = "Unassign the Dashboard from Public Customer (unassignDashboardFromPublicCustomer)", notes = "Unassigns the dashboard from a special, auto-generated 'Public' Customer. Once unassigned, unauthenticated users may no longer browse the dashboard. " + @@ -775,6 +604,7 @@ public class DashboardController extends BaseController { public void setTenantHomeDashboardInfo( @ApiParam(value = "A JSON object that represents home dashboard id and other parameters", required = true) @RequestBody HomeDashboardInfo homeDashboardInfo) throws ThingsboardException { + try { if (homeDashboardInfo.getDashboardId() != null) { checkDashboardId(homeDashboardInfo.getDashboardId(), Operation.READ); @@ -847,30 +677,34 @@ public class DashboardController extends BaseController { @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { checkParameter("edgeId", strEdgeId); checkParameter(DASHBOARD_ID, strDashboardId); - try { - EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); - Edge edge = checkEdgeId(edgeId, Operation.READ); - - DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); - checkDashboardId(dashboardId, Operation.READ); - - Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToEdge(getCurrentUser().getTenantId(), dashboardId, edgeId)); - logEntityAction(dashboardId, savedDashboard, - null, - ActionType.ASSIGNED_TO_EDGE, null, strDashboardId, strEdgeId, edge.getName()); - - sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedDashboard.getId(), EdgeEventActionType.ASSIGNED_TO_EDGE); - - return savedDashboard; - } catch (Exception e) { - - logEntityAction(emptyId(EntityType.DASHBOARD), null, - null, - ActionType.ASSIGNED_TO_EDGE, e, strDashboardId, strEdgeId); - - throw handleException(e); - } + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.READ); + + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + checkDashboardId(dashboardId, Operation.READ); + return tbDashboardService.asignDashboardToEdge(getTenantId(), dashboardId, edge, getCurrentUser()); +// try { +// +// +// +// Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToEdge(getCurrentUser().getTenantId(), dashboardId, edgeId)); +// +// logEntityAction(dashboardId, savedDashboard, +// null, +// ActionType.ASSIGNED_TO_EDGE, null, strDashboardId, strEdgeId, edge.getName()); +// +// sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedDashboard.getId(), EdgeEventActionType.ASSIGNED_TO_EDGE); +// +// return savedDashboard; +// } catch (Exception e) { +// +// logEntityAction(emptyId(EntityType.DASHBOARD), null, +// null, +// ActionType.ASSIGNED_TO_EDGE, e, strDashboardId, strEdgeId); +// +// throw handleException(e); +// } } @ApiOperation(value = "Unassign dashboard from edge (unassignDashboardFromEdge)", @@ -886,37 +720,22 @@ public class DashboardController extends BaseController { @ResponseBody public Dashboard unassignDashboardFromEdge(@PathVariable("edgeId") String strEdgeId, @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { - checkParameter("edgeId", strEdgeId); + checkParameter(EDGE_ID, strEdgeId); checkParameter(DASHBOARD_ID, strDashboardId); - try { - EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); - Edge edge = checkEdgeId(edgeId, Operation.READ); - DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); - Dashboard dashboard = checkDashboardId(dashboardId, Operation.READ); - - Dashboard savedDashboard = checkNotNull(dashboardService.unassignDashboardFromEdge(getCurrentUser().getTenantId(), dashboardId, edgeId)); - - logEntityAction(dashboardId, dashboard, - null, - ActionType.UNASSIGNED_FROM_EDGE, null, strDashboardId, strEdgeId, edge.getName()); - - sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedDashboard.getId(), EdgeEventActionType.UNASSIGNED_FROM_EDGE); - return savedDashboard; - } catch (Exception e) { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.READ); - logEntityAction(emptyId(EntityType.DASHBOARD), null, - null, - ActionType.UNASSIGNED_FROM_EDGE, e, strDashboardId, strEdgeId); + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + Dashboard dashboard = checkDashboardId(dashboardId, Operation.READ); - throw handleException(e); - } + return tbDashboardService.unassignDeviceFromEdge(dashboard, edge, getCurrentUser()); } @ApiOperation(value = "Get Edge Dashboards (getEdgeDashboards)", - notes = "Returns a page of dashboard info objects assigned to the specified edge. " - + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, - produces = MediaType.APPLICATION_JSON_VALUE) + notes = "Returns a page of dashboard info objects assigned to the specified edge. " + + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/edge/{edgeId}/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java index e2ccc1f3d2..95bbe1e82f 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java @@ -22,7 +22,6 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.User; @@ -43,6 +42,7 @@ import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.customer.CustomerService; +import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.ClaimDevicesService; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceService; @@ -106,6 +106,8 @@ public abstract class AbstractTbEntityService { protected RuleChainService ruleChainService; @Autowired protected EdgeNotificationService edgeNotificationService; + @Autowired + protected DashboardService dashboardService; protected ListenableFuture removeAlarmsByEntityId(TenantId tenantId, EntityId entityId) { ListenableFuture> alarmsFuture = diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java new file mode 100644 index 0000000000..11fced7790 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java @@ -0,0 +1,254 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.entitiy.dashboard; + +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ShortCustomerInfo; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.AbstractTbEntityService; +import org.thingsboard.server.service.security.model.SecurityUser; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +@Service +@TbCoreComponent +@AllArgsConstructor +public class DefaultTbDashboardService extends AbstractTbEntityService implements TbDashboardService { + + @Override + public Dashboard save(Dashboard dashboard, SecurityUser user) throws ThingsboardException { + ActionType actionType = dashboard.getId() == null ? ActionType.ADDED : ActionType.UPDATED; + TenantId tenantId = dashboard.getTenantId(); + try { + Dashboard savedDashboard = checkNotNull(dashboardService.saveDashboard(dashboard)); + notificationEntityService.notifyCreateOrUpdateEntity(tenantId, savedDashboard.getId(), savedDashboard, + null, actionType, user); + return savedDashboard; + } catch (Exception e) { + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DASHBOARD), dashboard, null, actionType, user, e); + throw handleException(e); + } + } + + @Override + public Dashboard assignDashboardToCustomer(TenantId tenantId, DashboardId dashboardId, Customer customer, SecurityUser user) throws ThingsboardException { + ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER; + CustomerId customerId = customer.getId(); + try { + Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(tenantId, dashboardId, customerId)); + notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, customerId, savedDashboard, + actionType, EdgeEventActionType.ASSIGNED_TO_CUSTOMER, user, true, customerId.toString(), customer.getName()); + return savedDashboard; + } catch (Exception e) { + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DASHBOARD), null, null, + actionType, user, e, dashboardId.toString(), customerId.toString()); + throw handleException(e); + } + } + + @Override + public Dashboard assignDashboardToPublicCustomer(TenantId tenantId, DashboardId dashboardId, SecurityUser user) throws ThingsboardException { + ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER; + try { + + Customer publicCustomer = customerService.findOrCreatePublicCustomer(tenantId); + Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(tenantId, dashboardId, publicCustomer.getId())); + notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, user.getCustomerId(), savedDashboard, + actionType, null, user, false, dashboardId.toString(), + publicCustomer.getId().toString(), publicCustomer.getName()); + return savedDashboard; + } catch (Exception e) { + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DASHBOARD), null, null, + actionType, user, e, dashboardId.toString()); + throw handleException(e); + } + + } + + @Override + public Dashboard updateDashboardCustomers(TenantId tenantId, Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException { + ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER; + try { + + + Set customerIds = new HashSet<>(); + if (strCustomerIds != null) { + for (String strCustomerId : strCustomerIds) { + customerIds.add(new CustomerId(UUID.fromString(strCustomerId))); + } + } + + Set addedCustomerIds = new HashSet<>(); + Set removedCustomerIds = new HashSet<>(); + for (CustomerId customerId : customerIds) { + if (!dashboard.isAssignedToCustomer(customerId)) { + addedCustomerIds.add(customerId); + } + } + + Set assignedCustomers = dashboard.getAssignedCustomers(); + if (assignedCustomers != null) { + for (ShortCustomerInfo customerInfo : assignedCustomers) { + if (!customerIds.contains(customerInfo.getCustomerId())) { + removedCustomerIds.add(customerInfo.getCustomerId()); + } + } + } + + if (addedCustomerIds.isEmpty() && removedCustomerIds.isEmpty()) { + return dashboard; + } else { + Dashboard savedDashboard = null; + for (CustomerId customerId : addedCustomerIds) { + savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(tenantId, dashboard.getId(), customerId)); + ShortCustomerInfo customerInfo = savedDashboard.getAssignedCustomerInfo(customerId); + notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, savedDashboard.getId(), customerId, savedDashboard, + actionType, EdgeEventActionType.ASSIGNED_TO_CUSTOMER, user, true, customerInfo.getTitle()); + } + for (CustomerId customerId : removedCustomerIds) { + ShortCustomerInfo customerInfo = dashboard.getAssignedCustomerInfo(customerId); + savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(tenantId, dashboard.getId(), customerId)); + notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, savedDashboard.getId(), customerId, savedDashboard, + ActionType.UNASSIGNED_FROM_CUSTOMER, EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER, user, true, customerInfo.getTitle()); + } + return savedDashboard; + } + } catch (Exception e) { + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DASHBOARD), null, null, + actionType, user, e, dashboard.getId().toString()); + throw handleException(e); + } + } + + @Override + public Dashboard addDashboardCustomers(TenantId tenantId, Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException { + ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER; + try { + Set customerIds = new HashSet<>(); + if (strCustomerIds != null) { + for (String strCustomerId : strCustomerIds) { + CustomerId customerId = new CustomerId(UUID.fromString(strCustomerId)); + if (!dashboard.isAssignedToCustomer(customerId)) { + customerIds.add(customerId); + } + } + } + + if (customerIds.isEmpty()) { + return dashboard; + } else { + Dashboard savedDashboard = null; + for (CustomerId customerId : customerIds) { + savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(tenantId, dashboard.getId(), customerId)); + ShortCustomerInfo customerInfo = savedDashboard.getAssignedCustomerInfo(customerId); + notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, savedDashboard.getId(), customerId, savedDashboard, + actionType, EdgeEventActionType.ASSIGNED_TO_CUSTOMER, user, true, customerInfo.getTitle()); + } + return savedDashboard; + } + } catch (Exception e) { + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DASHBOARD), null, null, + actionType, user, e, dashboard.getId().toString()); + throw handleException(e); + } + } + + @Override + public Dashboard removeDashboardCustomers(TenantId tenantId, Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException { + ActionType actionType = ActionType.UNASSIGNED_FROM_CUSTOMER; + try { + + + Set customerIds = new HashSet<>(); + if (strCustomerIds != null) { + for (String strCustomerId : strCustomerIds) { + CustomerId customerId = new CustomerId(UUID.fromString(strCustomerId)); + if (dashboard.isAssignedToCustomer(customerId)) { + customerIds.add(customerId); + } + } + } + + if (customerIds.isEmpty()) { + return dashboard; + } else { + Dashboard savedDashboard = null; + for (CustomerId customerId : customerIds) { + ShortCustomerInfo customerInfo = dashboard.getAssignedCustomerInfo(customerId); + savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(tenantId, dashboard.getId(), customerId)); + notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, savedDashboard.getId(), customerId, savedDashboard, + actionType, EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER, user, true, customerInfo.getTitle()); + } + return savedDashboard; + } + } catch (Exception e) { + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DASHBOARD), null, null, + actionType, user, e, dashboard.getId().toString()); + throw handleException(e); + } + } + + @Override + public Dashboard asignDashboardToEdge(TenantId tenantId, DashboardId dashboardId, Edge edge, SecurityUser user) throws ThingsboardException { + ActionType actionType = ActionType.ASSIGNED_TO_EDGE; + EdgeId edgeId = edge.getId(); + try { + Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToEdge(tenantId, dashboardId, edgeId)); + notificationEntityService.notifyAssignOrUnassignEntityToEdge(tenantId, dashboardId, user.getCustomerId(), + edgeId, savedDashboard, actionType, EdgeEventActionType.ASSIGNED_TO_EDGE, user, dashboardId.toString(), + edgeId.toString(), edge.getName()); + return savedDashboard; + } catch (Exception e) { + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DEVICE), null, null, + actionType, user, e, dashboardId.toString(), edgeId.toString()); + throw handleException(e); + } + } + + @Override + public Dashboard unassignDeviceFromEdge(Dashboard dashboard, Edge edge, SecurityUser user) throws ThingsboardException { + ActionType actionType = ActionType.UNASSIGNED_FROM_EDGE; + TenantId tenantId = dashboard.getTenantId(); + DashboardId dashboardId = dashboard.getId(); + EdgeId edgeId = edge.getId(); + try { + Dashboard savedDevice = checkNotNull(dashboardService.unassignDashboardFromEdge(tenantId, dashboardId, edgeId)); + + notificationEntityService.notifyAssignOrUnassignEntityToEdge(tenantId, dashboardId, user.getCustomerId(), + edgeId, dashboard, actionType, EdgeEventActionType.UNASSIGNED_FROM_EDGE, user, dashboardId.toString(), + edgeId.toString(), edge.getName()); + return savedDevice; + } catch (Exception e) { + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DASHBOARD), null, null, + actionType, user, e, dashboardId.toString(), edgeId.toString()); + throw handleException(e); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java new file mode 100644 index 0000000000..0acb9a16c5 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2022 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. + */ +package org.thingsboard.server.service.entitiy.dashboard; + +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.service.entitiy.SimpleTbEntityService; +import org.thingsboard.server.service.security.model.SecurityUser; + +public interface TbDashboardService extends SimpleTbEntityService { + + Dashboard assignDashboardToCustomer(TenantId tenantId, DashboardId dashboardId, Customer customer, SecurityUser user) throws ThingsboardException; + + Dashboard assignDashboardToPublicCustomer(TenantId tenantId, DashboardId dashboardId, SecurityUser user) throws ThingsboardException; + + Dashboard updateDashboardCustomers(TenantId tenantId, Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException; + + Dashboard addDashboardCustomers(TenantId tenantId, Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException; + + Dashboard removeDashboardCustomers(TenantId tenantId, Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException; + + Dashboard asignDashboardToEdge(TenantId tenantId, DashboardId dashboardId, Edge edge, SecurityUser user) throws ThingsboardException; + + Dashboard unassignDeviceFromEdge(Dashboard dashboard, Edge edge, SecurityUser user) throws ThingsboardException; + +} From 90a8b03bced6723c35f211d0a8115af482d5821d Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Fri, 13 May 2022 23:04:16 +0300 Subject: [PATCH 149/276] refactoring: - add delete to SimpleTbEntityService --- .../controller/DashboardController.java | 27 ++---------- .../DefaultTbNotificationEntityService.java | 44 ++++++------------- .../entitiy/SimpleTbEntityService.java | 8 +++- .../entitiy/TbNotificationEntityService.java | 7 +-- .../entitiy/alarm/DefaultTbAlarmService.java | 21 +++++++-- .../service/entitiy/alarm/TbAlarmService.java | 6 +-- .../entitiy/asset/DefaultTbAssetService.java | 8 +++- .../service/entitiy/asset/TbAssetService.java | 2 +- .../customer/DefaultTbCustomerService.java | 14 +++--- .../entitiy/customer/TbCustomerService.java | 7 +-- .../dashboard/DefaultTbDashboardService.java | 18 ++++++++ .../entitiy/dashboard/TbDashboardService.java | 2 +- 12 files changed, 83 insertions(+), 81 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index 98fc25cc54..bd5938e9e5 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -195,29 +195,10 @@ public class DashboardController extends BaseController { @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION) @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { checkParameter(DASHBOARD_ID, strDashboardId); - try { - DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); - Dashboard dashboard = checkDashboardId(dashboardId, Operation.DELETE); - - List relatedEdgeIds = findRelatedEdgeIds(getTenantId(), dashboardId); - - dashboardService.deleteDashboard(getCurrentUser().getTenantId(), dashboardId); - - logEntityAction(dashboardId, dashboard, - null, - ActionType.DELETED, null, strDashboardId); - - sendDeleteNotificationMsg(getTenantId(), dashboardId, relatedEdgeIds); - } catch (Exception e) { - - logEntityAction(emptyId(EntityType.DASHBOARD), - null, - null, - ActionType.DELETED, e, strDashboardId); - - throw handleException(e); - } - } + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + Dashboard dashboard = checkDashboardId(dashboardId, Operation.DELETE); + tbDashboardService.delete(dashboard, dashboardId, getCurrentUser()); + } @ApiOperation(value = "Assign the Dashboard (assignDashboardToCustomer)", notes = "Assign the Dashboard to specified Customer or do nothing if the Dashboard is already assigned to that Customer. " + diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java index c18e175fe2..a70d068d40 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java @@ -71,9 +71,13 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS public void notifyDeleteEntity(TenantId tenantId, I entityId, E entity, CustomerId customerId, ActionType actionType, List relatedEdgeIds, - SecurityUser user, Object... additionalInfo) { + SecurityUser user, + String body, Object... additionalInfo) { logEntityAction(tenantId, entityId, entity, customerId, actionType, user, additionalInfo); - sendDeleteNotificationMsg(tenantId, entityId, entity, relatedEdgeIds); + sendDeleteNotificationMsg(tenantId, entityId, entity, relatedEdgeIds, body); + if (entity instanceof Customer) { + tbClusterService.broadcastEntityStateChangeEvent(tenantId, customerId, ComponentLifecycleEvent.DELETED); + } } @Override @@ -126,7 +130,7 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS gatewayNotificationsService.onDeviceDeleted(device); tbClusterService.onDeviceDeleted(device, null); - notifyDeleteEntity(tenantId, deviceId, device, customerId, ActionType.DELETED, relatedEdgeIds, user, false, additionalInfo); + notifyDeleteEntity(tenantId, deviceId, device, customerId, ActionType.DELETED, relatedEdgeIds, user,null, additionalInfo); } @Override @@ -145,9 +149,9 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS } @Override - public void notifyCreateOrUpdateEntity(TenantId tenantId, I entityId, E entity, CustomerId customerId, ActionType actionType, SecurityUser user, Object... additionalInfo) { + public void notifyCreateOrUpdateEntity(TenantId tenantId, I entityId, E entity, CustomerId customerId, ActionType actionType, SecurityUser user, Object... additionalInfo) { logEntityAction(tenantId, entityId, entity, customerId, actionType, user, additionalInfo); - if (actionType == ActionType.UPDATED) { + if (actionType == ActionType.UPDATED) { sendEntityNotificationMsg(tenantId, entityId, EdgeEventActionType.UPDATED); } } @@ -191,20 +195,7 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS @Override public void notifyCreateOrUpdateAlarm(Alarm alarm, ActionType actionType, SecurityUser user, Object... additionalInfo) { logEntityAction(alarm.getTenantId(), alarm.getOriginator(), alarm, alarm.getCustomerId(), actionType, user, additionalInfo); - sendEntityNotificationMsg(alarm.getTenantId(), alarm.getId(), edgeTypeByActionType (actionType)); - } - - @Override - public void notifyDeleteAlarm(Alarm alarm, SecurityUser user, List relatedEdgeIds) { - logEntityAction(alarm.getTenantId(), alarm.getOriginator(), alarm, alarm.getCustomerId(), ActionType.ALARM_DELETE, user, null); - sendAlarmDeleteNotificationMsg(alarm, relatedEdgeIds); - } - - @Override - public void notifyDeleteCustomer(Customer customer, SecurityUser user, List edgeIds) { - logEntityAction(customer.getTenantId(), customer.getId(), customer, customer.getId(), ActionType.DELETED, user, null); - sendDeleteNotificationMsg(customer.getTenantId(), customer.getId(), customer, edgeIds); - tbClusterService.broadcastEntityStateChangeEvent(customer.getTenantId(), customer.getId(), ComponentLifecycleEvent.DELETED); + sendEntityNotificationMsg(alarm.getTenantId(), alarm.getId(), edgeTypeByActionType(actionType)); } private void logEntityAction(TenantId tenantId, I entityId, E entity, CustomerId customerId, @@ -233,22 +224,15 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS } } - protected void sendDeleteNotificationMsg(TenantId tenantId, I entityId, E entity, List edgeIds) { + protected void sendDeleteNotificationMsg(TenantId tenantId, I entityId, E entity, + List edgeIds, String body) { try { - sendDeleteNotificationMsg(tenantId, entityId, edgeIds, null); + sendDeleteNotificationMsg(tenantId, entityId, edgeIds, body); } catch (Exception e) { log.warn("Failed to push delete " + entity.getClass().getName() + " msg to core: {}", entity, e); } } - protected void sendAlarmDeleteNotificationMsg(Alarm alarm, List relatedEdgeIds) { - try { - sendDeleteNotificationMsg(alarm.getTenantId(), alarm.getId(), relatedEdgeIds, json.writeValueAsString(alarm)); - } catch (Exception e) { - log.warn("Failed to push delete alarm msg to core: {}", alarm, e); - } - } - private void sendDeleteNotificationMsg(TenantId tenantId, EntityId entityId, List edgeIds, String body) { if (edgeIds != null && !edgeIds.isEmpty()) { for (EdgeId edgeId : edgeIds) { @@ -289,7 +273,7 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS return null; } - private EdgeEventActionType edgeTypeByActionType (ActionType actionType) { + private EdgeEventActionType edgeTypeByActionType(ActionType actionType) { switch (actionType) { case ADDED: return EdgeEventActionType.ADDED; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/SimpleTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/SimpleTbEntityService.java index 84f5658015..ea5bc92e3e 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/SimpleTbEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/SimpleTbEntityService.java @@ -15,11 +15,15 @@ */ package org.thingsboard.server.service.entitiy; +import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.security.model.SecurityUser; -public interface SimpleTbEntityService { +public interface SimpleTbEntityService { - T save(T entity, SecurityUser user) throws ThingsboardException; + E save(E entity, SecurityUser user) throws ThingsboardException; + + void delete (E entity, I entityId, SecurityUser user) throws ThingsboardException; } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java index 43c0cc4c91..70cac320d7 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.entitiy; -import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.Tenant; @@ -46,7 +45,7 @@ public interface TbNotificationEntityService { void notifyDeleteEntity(TenantId tenantId, I entityId, E entity, CustomerId customerId, ActionType actionType, List relatedEdgeIds, SecurityUser user, - Object... additionalInfo); + String body, Object... additionalInfo); void notifyAssignOrUnassignEntityToCustomer(TenantId tenantId, I entityId, CustomerId customerId, E entity, @@ -80,8 +79,4 @@ public interface TbNotificationEntityService { void notifyEdge(TenantId tenantId, EdgeId edgeId, CustomerId customerId, Edge edge, ActionType actionType, SecurityUser user, Object... additionalInfo); void notifyCreateOrUpdateAlarm(Alarm alarm, ActionType actionType, SecurityUser user, Object... additionalInfo); - - void notifyDeleteAlarm(Alarm alarm, SecurityUser user, List relatedEdgeIds); - - void notifyDeleteCustomer(Customer customer, SecurityUser user, List relatedEdgeIds); } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java index 1fb19e3c41..ad066f2233 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java @@ -15,14 +15,17 @@ */ package org.thingsboard.server.service.entitiy.alarm; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; @@ -34,6 +37,7 @@ import java.util.List; @TbCoreComponent @AllArgsConstructor public class DefaultTbAlarmService extends AbstractTbEntityService implements TbAlarmService { + private static final ObjectMapper json = new ObjectMapper(); @Override public Alarm save(Alarm alarm, SecurityUser user) throws ThingsboardException { @@ -76,9 +80,18 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb } @Override - public Boolean delete(Alarm alarm, SecurityUser user) throws ThingsboardException { - List relatedEdgeIds = findRelatedEdgeIds(alarm.getTenantId(), alarm.getOriginator()); - notificationEntityService.notifyDeleteAlarm(alarm, user, relatedEdgeIds); - return alarmService.deleteAlarm(alarm.getTenantId(), alarm.getId()).isSuccessful(); + public Boolean deleteAlarm(Alarm alarm, SecurityUser user) throws ThingsboardException { + try { + TenantId tenantId = user.getTenantId(); + List relatedEdgeIds = findRelatedEdgeIds(tenantId, alarm.getOriginator()); + notificationEntityService.notifyDeleteEntity(tenantId, alarm.getOriginator(), alarm, user.getCustomerId(), + ActionType.DELETED, relatedEdgeIds, user, json.writeValueAsString(alarm)); + return alarmService.deleteAlarm(tenantId, alarm.getId()).isSuccessful(); + } catch (Exception e) { + throw handleException(e); + } } + + @Override + public void delete(E entity, I entityId, SecurityUser user) throws ThingsboardException {} } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java index f73e44bc69..8a169b8f88 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java @@ -16,16 +16,16 @@ package org.thingsboard.server.service.entitiy.alarm; import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.service.entitiy.SimpleTbEntityService; import org.thingsboard.server.service.security.model.SecurityUser; -public interface TbAlarmService extends SimpleTbEntityService { +public interface TbAlarmService extends SimpleTbEntityService { void ack(Alarm alarm, SecurityUser user) throws ThingsboardException; void clear(Alarm alarm, SecurityUser user) throws ThingsboardException; - Boolean delete(Alarm alarm, SecurityUser user) throws ThingsboardException; + Boolean deleteAlarm(Alarm alarm, SecurityUser user) throws ThingsboardException; } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java index 3f4ce83318..559ac449b4 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java @@ -20,6 +20,7 @@ import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; @@ -28,6 +29,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; @@ -60,7 +62,8 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb try { List relatedEdgeIds = findRelatedEdgeIds(tenantId, assetId); assetService.deleteAsset(tenantId, assetId); - notificationEntityService.notifyDeleteEntity(tenantId, assetId, asset, asset.getCustomerId(), ActionType.DELETED, relatedEdgeIds, user, false, asset.toString()); + notificationEntityService.notifyDeleteEntity(tenantId, assetId, asset, asset.getCustomerId(), ActionType.DELETED, + relatedEdgeIds, user, null, asset.toString()); return removeAlarmsByEntityId(tenantId, assetId); } catch (Exception e) { @@ -70,6 +73,9 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb } } + @Override + public void delete(E entity, I entityId, SecurityUser user) throws ThingsboardException { } + @Override public Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, Customer customer, SecurityUser user) throws ThingsboardException { ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java index 3bc6dadf3d..e753093ba2 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java @@ -25,7 +25,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.service.entitiy.SimpleTbEntityService; import org.thingsboard.server.service.security.model.SecurityUser; -public interface TbAssetService extends SimpleTbEntityService { +public interface TbAssetService extends SimpleTbEntityService { ListenableFuture delete(Asset asset, SecurityUser user) throws ThingsboardException; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java index 452c71b51a..dec48a4fcb 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java @@ -19,9 +19,12 @@ import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; @@ -49,12 +52,13 @@ public class DefaultTbCustomerService extends AbstractTbEntityService implements } @Override - public void delete(Customer customer, SecurityUser user) throws ThingsboardException { - TenantId tenantId = customer.getTenantId(); + public void delete(E customer, I customerId, SecurityUser user) throws ThingsboardException { + TenantId tenantId = user.getTenantId(); try { - List relatedEdgeIds = findRelatedEdgeIds(tenantId, customer.getId()); - customerService.deleteCustomer(tenantId, customer.getId()); - notificationEntityService.notifyDeleteCustomer(customer, user, relatedEdgeIds); + List relatedEdgeIds = findRelatedEdgeIds(tenantId, customerId); + customerService.deleteCustomer(tenantId, (CustomerId) customerId); + notificationEntityService.notifyDeleteEntity(tenantId, customerId, customer, user.getCustomerId(), + ActionType.DELETED, relatedEdgeIds, user, null); } catch (Exception e) { notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.CUSTOMER), null, null, ActionType.DELETED, user, e); throw handleException(e); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java index f34d796db1..2e0d891046 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java @@ -16,12 +16,9 @@ package org.thingsboard.server.service.entitiy.customer; import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.service.entitiy.SimpleTbEntityService; -import org.thingsboard.server.service.security.model.SecurityUser; -public interface TbCustomerService extends SimpleTbEntityService { - - void delete(Customer customer, SecurityUser user) throws ThingsboardException; +public interface TbCustomerService extends SimpleTbEntityService { } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java index 11fced7790..d375c1691f 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java @@ -20,6 +20,7 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.ShortCustomerInfo; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; @@ -28,12 +29,14 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import org.thingsboard.server.service.security.model.SecurityUser; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.UUID; @@ -57,6 +60,21 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement } } + @Override + public void delete(E dashboard, I dashboardId, SecurityUser user) throws ThingsboardException { + TenantId tenantId = user.getTenantId(); + try { + List relatedEdgeIds = findRelatedEdgeIds(tenantId, dashboardId); + dashboardService.deleteDashboard(tenantId, (DashboardId) dashboardId); + notificationEntityService.notifyDeleteEntity(tenantId, dashboardId, dashboard, user.getCustomerId(), + ActionType.DELETED, relatedEdgeIds, user, null); + } catch (Exception e) { + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DASHBOARD), null, null, + ActionType.DELETED, user, e, dashboardId.toString()); + throw handleException(e); + } + } + @Override public Dashboard assignDashboardToCustomer(TenantId tenantId, DashboardId dashboardId, Customer customer, SecurityUser user) throws ThingsboardException { ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java index 0acb9a16c5..88ebc93824 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java @@ -24,7 +24,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.service.entitiy.SimpleTbEntityService; import org.thingsboard.server.service.security.model.SecurityUser; -public interface TbDashboardService extends SimpleTbEntityService { +public interface TbDashboardService extends SimpleTbEntityService { Dashboard assignDashboardToCustomer(TenantId tenantId, DashboardId dashboardId, Customer customer, SecurityUser user) throws ThingsboardException; From 122e220f4dd08c573a14e7c2c4a9fbbc891650f0 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Sat, 14 May 2022 10:55:05 +0300 Subject: [PATCH 150/276] refactoring: - fix bug delete to SimpleTbEntityService --- .../server/controller/AlarmController.java | 2 +- .../server/controller/CustomerController.java | 8 +------- .../entitiy/DefaultTbNotificationEntityService.java | 3 ++- .../service/entitiy/SimpleTbEntityService.java | 6 +++--- .../entitiy/alarm/DefaultTbAlarmService.java | 13 ++++++------- .../entitiy/asset/DefaultTbAssetService.java | 5 ++--- .../entitiy/customer/DefaultTbCustomerService.java | 5 ++--- .../dashboard/DefaultTbDashboardService.java | 5 +---- 8 files changed, 18 insertions(+), 29 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java index 939e9da8a0..e1db3c55e0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java @@ -147,7 +147,7 @@ public class AlarmController extends BaseController { try { AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); - return tbAlarmService.delete(alarm, getCurrentUser()); + return tbAlarmService.deleteAlarm(alarm, getCurrentUser()); } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java index 2fc4b7c748..8ce95eaaa5 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java @@ -32,22 +32,16 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.customer.TbCustomerService; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; -import java.util.List; - import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_SORT_PROPERTY_ALLOWABLE_VALUES; @@ -167,7 +161,7 @@ public class CustomerController extends BaseController { CustomerId customerId = new CustomerId(toUUID(strCustomerId)); Customer customer = checkCustomerId(customerId, Operation.DELETE); try { - tbCustomerService.delete(customer, getCurrentUser()); + tbCustomerService.delete(customer, customerId, getCurrentUser()); } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java index a70d068d40..6ee9531c46 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java @@ -73,7 +73,8 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS List relatedEdgeIds, SecurityUser user, String body, Object... additionalInfo) { - logEntityAction(tenantId, entityId, entity, customerId, actionType, user, additionalInfo); + EntityId entityIdForLogEntityAction = entity instanceof Alarm ? ((Alarm)entity).getOriginator() : entityId; + logEntityAction(tenantId, entityIdForLogEntityAction, entity, customerId, actionType, user, additionalInfo); sendDeleteNotificationMsg(tenantId, entityId, entity, relatedEdgeIds, body); if (entity instanceof Customer) { tbClusterService.broadcastEntityStateChangeEvent(tenantId, customerId, ComponentLifecycleEvent.DELETED); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/SimpleTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/SimpleTbEntityService.java index ea5bc92e3e..5d7f89f886 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/SimpleTbEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/SimpleTbEntityService.java @@ -20,10 +20,10 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.security.model.SecurityUser; -public interface SimpleTbEntityService { +public interface SimpleTbEntityService { - E save(E entity, SecurityUser user) throws ThingsboardException; + E save(E entity, SecurityUser user) throws ThingsboardException; - void delete (E entity, I entityId, SecurityUser user) throws ThingsboardException; + void delete (E entity, I entityId, SecurityUser user) throws ThingsboardException; } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java index ad066f2233..88cfb63e72 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java @@ -19,13 +19,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; @@ -82,16 +81,16 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb @Override public Boolean deleteAlarm(Alarm alarm, SecurityUser user) throws ThingsboardException { try { - TenantId tenantId = user.getTenantId(); - List relatedEdgeIds = findRelatedEdgeIds(tenantId, alarm.getOriginator()); - notificationEntityService.notifyDeleteEntity(tenantId, alarm.getOriginator(), alarm, user.getCustomerId(), + List relatedEdgeIds = findRelatedEdgeIds(user.getTenantId(), alarm.getOriginator()); + notificationEntityService.notifyDeleteEntity(user.getTenantId(), alarm.getId(), alarm, user.getCustomerId(), ActionType.DELETED, relatedEdgeIds, user, json.writeValueAsString(alarm)); - return alarmService.deleteAlarm(tenantId, alarm.getId()).isSuccessful(); + return alarmService.deleteAlarm(user.getTenantId(), alarm.getId()).isSuccessful(); } catch (Exception e) { throw handleException(e); } } + @Override - public void delete(E entity, I entityId, SecurityUser user) throws ThingsboardException {} + public void delete(Alarm entity, AlarmId entityId, SecurityUser user) throws ThingsboardException {} } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java index 559ac449b4..1d75e68616 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java @@ -20,7 +20,6 @@ import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; @@ -29,7 +28,6 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; @@ -73,8 +71,9 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb } } + @Override - public void delete(E entity, I entityId, SecurityUser user) throws ThingsboardException { } + public void delete(Asset entity, AssetId entityId, SecurityUser user) throws ThingsboardException {} @Override public Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, Customer customer, SecurityUser user) throws ThingsboardException { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java index dec48a4fcb..f5250c7ee9 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java @@ -19,12 +19,10 @@ import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; @@ -51,8 +49,9 @@ public class DefaultTbCustomerService extends AbstractTbEntityService implements } } + @Override - public void delete(E customer, I customerId, SecurityUser user) throws ThingsboardException { + public void delete(Customer customer, CustomerId customerId, SecurityUser user) throws ThingsboardException { TenantId tenantId = user.getTenantId(); try { List relatedEdgeIds = findRelatedEdgeIds(tenantId, customerId); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java index d375c1691f..df36680f26 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java @@ -20,7 +20,6 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.ShortCustomerInfo; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; @@ -29,7 +28,6 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.EdgeId; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; @@ -61,8 +59,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement } @Override - public void delete(E dashboard, I dashboardId, SecurityUser user) throws ThingsboardException { - TenantId tenantId = user.getTenantId(); + public void delete(Dashboard dashboard, DashboardId dashboardId, SecurityUser user) throws ThingsboardException { TenantId tenantId = user.getTenantId(); try { List relatedEdgeIds = findRelatedEdgeIds(tenantId, dashboardId); dashboardService.deleteDashboard(tenantId, (DashboardId) dashboardId); From 8bae18db71004bdc6263ac984b4112d0f70ece5b Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Sat, 14 May 2022 13:46:23 +0300 Subject: [PATCH 151/276] refactoring: - add unassignDashboard to SimpleTbEntityService --- .../controller/DashboardController.java | 85 +++---------------- .../dashboard/DefaultTbDashboardService.java | 58 ++++++++++--- .../entitiy/dashboard/TbDashboardService.java | 10 ++- 3 files changed, 66 insertions(+), 87 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index bd5938e9e5..3f7bd618a8 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -39,14 +39,11 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DashboardInfo; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HomeDashboard; import org.thingsboard.server.common.data.HomeDashboardInfo; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; @@ -198,7 +195,7 @@ public class DashboardController extends BaseController { DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); Dashboard dashboard = checkDashboardId(dashboardId, Operation.DELETE); tbDashboardService.delete(dashboard, dashboardId, getCurrentUser()); - } + } @ApiOperation(value = "Assign the Dashboard (assignDashboardToCustomer)", notes = "Assign the Dashboard to specified Customer or do nothing if the Dashboard is already assigned to that Customer. " + @@ -220,7 +217,7 @@ public class DashboardController extends BaseController { DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); checkDashboardId(dashboardId, Operation.ASSIGN_TO_CUSTOMER); - return tbDashboardService.assignDashboardToCustomer(getTenantId(), dashboardId, customer, getCurrentUser()); + return tbDashboardService.assignDashboardToCustomer(dashboardId, customer, getCurrentUser()); } @ApiOperation(value = "Unassign the Dashboard (unassignDashboardFromCustomer)", @@ -237,29 +234,11 @@ public class DashboardController extends BaseController { @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { checkParameter("customerId", strCustomerId); checkParameter(DASHBOARD_ID, strDashboardId); - try { - CustomerId customerId = new CustomerId(toUUID(strCustomerId)); - Customer customer = checkCustomerId(customerId, Operation.READ); - DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); - Dashboard dashboard = checkDashboardId(dashboardId, Operation.UNASSIGN_FROM_CUSTOMER); - - Dashboard savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(getCurrentUser().getTenantId(), dashboardId, customerId)); - - logEntityAction(dashboardId, dashboard, - customerId, - ActionType.UNASSIGNED_FROM_CUSTOMER, null, strDashboardId, customer.getId().toString(), customer.getName()); - - sendEntityAssignToCustomerNotificationMsg(savedDashboard.getTenantId(), savedDashboard.getId(), customerId, EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER); - - return savedDashboard; - } catch (Exception e) { - - logEntityAction(emptyId(EntityType.DASHBOARD), null, - null, - ActionType.UNASSIGNED_FROM_CUSTOMER, e, strDashboardId); - - throw handleException(e); - } + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + Customer customer = checkCustomerId(customerId, Operation.READ); + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + Dashboard dashboard = checkDashboardId(dashboardId, Operation.UNASSIGN_FROM_CUSTOMER); + return tbDashboardService.unassignDashboardFromCustomer(dashboard, customer, getCurrentUser()); } @ApiOperation(value = "Update the Dashboard Customers (updateDashboardCustomers)", @@ -338,8 +317,8 @@ public class DashboardController extends BaseController { checkParameter(DASHBOARD_ID, strDashboardId); DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); checkDashboardId(dashboardId, Operation.ASSIGN_TO_CUSTOMER); - return tbDashboardService.assignDashboardToPublicCustomer(getTenantId(), dashboardId, getCurrentUser()); - } + return tbDashboardService.assignDashboardToPublicCustomer(dashboardId, getCurrentUser()); + } @ApiOperation(value = "Unassign the Dashboard from Public Customer (unassignDashboardFromPublicCustomer)", notes = "Unassigns the dashboard from a special, auto-generated 'Public' Customer. Once unassigned, unauthenticated users may no longer browse the dashboard. " + @@ -352,26 +331,9 @@ public class DashboardController extends BaseController { @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION) @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { checkParameter(DASHBOARD_ID, strDashboardId); - try { - DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); - Dashboard dashboard = checkDashboardId(dashboardId, Operation.UNASSIGN_FROM_CUSTOMER); - Customer publicCustomer = customerService.findOrCreatePublicCustomer(dashboard.getTenantId()); - - Dashboard savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(getCurrentUser().getTenantId(), dashboardId, publicCustomer.getId())); - - logEntityAction(dashboardId, dashboard, - publicCustomer.getId(), - ActionType.UNASSIGNED_FROM_CUSTOMER, null, strDashboardId, publicCustomer.getId().toString(), publicCustomer.getName()); - - return savedDashboard; - } catch (Exception e) { - - logEntityAction(emptyId(EntityType.DASHBOARD), null, - null, - ActionType.UNASSIGNED_FROM_CUSTOMER, e, strDashboardId); - - throw handleException(e); - } + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + Dashboard dashboard = checkDashboardId(dashboardId, Operation.UNASSIGN_FROM_CUSTOMER); + return tbDashboardService.unassignDashboardFromPublicCustomer(dashboard, getCurrentUser()); } @ApiOperation(value = "Get Tenant Dashboards by System Administrator (getTenantDashboards)", @@ -665,27 +627,6 @@ public class DashboardController extends BaseController { DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); checkDashboardId(dashboardId, Operation.READ); return tbDashboardService.asignDashboardToEdge(getTenantId(), dashboardId, edge, getCurrentUser()); -// try { -// -// -// -// Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToEdge(getCurrentUser().getTenantId(), dashboardId, edgeId)); -// -// logEntityAction(dashboardId, savedDashboard, -// null, -// ActionType.ASSIGNED_TO_EDGE, null, strDashboardId, strEdgeId, edge.getName()); -// -// sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedDashboard.getId(), EdgeEventActionType.ASSIGNED_TO_EDGE); -// -// return savedDashboard; -// } catch (Exception e) { -// -// logEntityAction(emptyId(EntityType.DASHBOARD), null, -// null, -// ActionType.ASSIGNED_TO_EDGE, e, strDashboardId, strEdgeId); -// -// throw handleException(e); -// } } @ApiOperation(value = "Unassign dashboard from edge (unassignDashboardFromEdge)", @@ -710,7 +651,7 @@ public class DashboardController extends BaseController { DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); Dashboard dashboard = checkDashboardId(dashboardId, Operation.READ); - return tbDashboardService.unassignDeviceFromEdge(dashboard, edge, getCurrentUser()); + return tbDashboardService.unassignDashboardFromEdge(dashboard, edge, getCurrentUser()); } @ApiOperation(value = "Get Edge Dashboards (getEdgeDashboards)", diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java index df36680f26..523ae2f651 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java @@ -59,7 +59,8 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement } @Override - public void delete(Dashboard dashboard, DashboardId dashboardId, SecurityUser user) throws ThingsboardException { TenantId tenantId = user.getTenantId(); + public void delete(Dashboard dashboard, DashboardId dashboardId, SecurityUser user) throws ThingsboardException { + TenantId tenantId = user.getTenantId(); try { List relatedEdgeIds = findRelatedEdgeIds(tenantId, dashboardId); dashboardService.deleteDashboard(tenantId, (DashboardId) dashboardId); @@ -73,40 +74,56 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement } @Override - public Dashboard assignDashboardToCustomer(TenantId tenantId, DashboardId dashboardId, Customer customer, SecurityUser user) throws ThingsboardException { + public Dashboard assignDashboardToCustomer(DashboardId dashboardId, Customer customer, SecurityUser user) throws ThingsboardException { ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER; CustomerId customerId = customer.getId(); try { - Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(tenantId, dashboardId, customerId)); - notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, customerId, savedDashboard, + Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(user.getTenantId(), dashboardId, customerId)); + notificationEntityService.notifyAssignOrUnassignEntityToCustomer(user.getTenantId(), dashboardId, customerId, savedDashboard, actionType, EdgeEventActionType.ASSIGNED_TO_CUSTOMER, user, true, customerId.toString(), customer.getName()); return savedDashboard; } catch (Exception e) { - notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DASHBOARD), null, null, + notificationEntityService.notifyEntity(user.getTenantId(), emptyId(EntityType.DASHBOARD), null, null, actionType, user, e, dashboardId.toString(), customerId.toString()); throw handleException(e); } } @Override - public Dashboard assignDashboardToPublicCustomer(TenantId tenantId, DashboardId dashboardId, SecurityUser user) throws ThingsboardException { + public Dashboard assignDashboardToPublicCustomer(DashboardId dashboardId, SecurityUser user) throws ThingsboardException { ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER; try { - - Customer publicCustomer = customerService.findOrCreatePublicCustomer(tenantId); - Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(tenantId, dashboardId, publicCustomer.getId())); - notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, user.getCustomerId(), savedDashboard, + Customer publicCustomer = customerService.findOrCreatePublicCustomer(user.getTenantId()); + Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(user.getTenantId(), dashboardId, publicCustomer.getId())); + notificationEntityService.notifyAssignOrUnassignEntityToCustomer(user.getTenantId(), dashboardId, user.getCustomerId(), savedDashboard, actionType, null, user, false, dashboardId.toString(), publicCustomer.getId().toString(), publicCustomer.getName()); return savedDashboard; } catch (Exception e) { - notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DASHBOARD), null, null, + notificationEntityService.notifyEntity(user.getTenantId(), emptyId(EntityType.DASHBOARD), null, null, actionType, user, e, dashboardId.toString()); throw handleException(e); } } + @Override + public Dashboard unassignDashboardFromPublicCustomer(Dashboard dashboard, SecurityUser user) throws ThingsboardException { + ActionType actionType = ActionType.UNASSIGNED_FROM_CUSTOMER; + try { + Customer publicCustomer = customerService.findOrCreatePublicCustomer(dashboard.getTenantId()); + Dashboard savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(user.getTenantId(), dashboard.getId(), publicCustomer.getId())); + notificationEntityService.notifyAssignOrUnassignEntityToCustomer(user.getTenantId(), dashboard.getId(), user.getCustomerId(), dashboard, + actionType, null, user, false, dashboard.getId().toString(), + publicCustomer.getId().toString(), publicCustomer.getName()); + return savedDashboard; + } catch (Exception e) { + notificationEntityService.notifyEntity(user.getTenantId(), emptyId(EntityType.DASHBOARD), null, null, + actionType, user, e, dashboard.getId().toString()); + throw handleException(e); + } + } + @Override public Dashboard updateDashboardCustomers(TenantId tenantId, Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException { ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER; @@ -248,7 +265,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement } @Override - public Dashboard unassignDeviceFromEdge(Dashboard dashboard, Edge edge, SecurityUser user) throws ThingsboardException { + public Dashboard unassignDashboardFromEdge(Dashboard dashboard, Edge edge, SecurityUser user) throws ThingsboardException { ActionType actionType = ActionType.UNASSIGNED_FROM_EDGE; TenantId tenantId = dashboard.getTenantId(); DashboardId dashboardId = dashboard.getId(); @@ -266,4 +283,21 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement throw handleException(e); } } + + @Override + public Dashboard unassignDashboardFromCustomer(Dashboard dashboard, Customer customer, SecurityUser user) throws ThingsboardException { + ActionType actionType = ActionType.UNASSIGNED_FROM_CUSTOMER; + TenantId tenantId = dashboard.getTenantId(); + try { + Dashboard savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(tenantId, dashboard.getId(), customer.getId())); + notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboard.getId(), customer.getId(), savedDashboard, + actionType, EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER, user, true, customer.getId().toString(), customer.getName()); + return savedDashboard; + } catch (Exception e) { + notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DASHBOARD), null, null, + actionType, user, e, dashboard.getId().toString()); + throw handleException(e); + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java index 88ebc93824..36598f514e 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java @@ -26,9 +26,11 @@ import org.thingsboard.server.service.security.model.SecurityUser; public interface TbDashboardService extends SimpleTbEntityService { - Dashboard assignDashboardToCustomer(TenantId tenantId, DashboardId dashboardId, Customer customer, SecurityUser user) throws ThingsboardException; + Dashboard assignDashboardToCustomer(DashboardId dashboardId, Customer customer, SecurityUser user) throws ThingsboardException; - Dashboard assignDashboardToPublicCustomer(TenantId tenantId, DashboardId dashboardId, SecurityUser user) throws ThingsboardException; + Dashboard assignDashboardToPublicCustomer(DashboardId dashboardId, SecurityUser user) throws ThingsboardException; + + Dashboard unassignDashboardFromPublicCustomer(Dashboard dashboard, SecurityUser user) throws ThingsboardException; Dashboard updateDashboardCustomers(TenantId tenantId, Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException; @@ -38,6 +40,8 @@ public interface TbDashboardService extends SimpleTbEntityService Date: Sun, 15 May 2022 22:20:25 +0300 Subject: [PATCH 152/276] refactoring: - delete SimpleTbEntityService from Alarm --- .../entitiy/alarm/DefaultTbAlarmService.java | 15 +++++---------- .../service/entitiy/alarm/TbAlarmService.java | 6 +++--- .../customer/DefaultTbCustomerService.java | 2 +- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java index 88cfb63e72..870388483e 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java @@ -23,7 +23,6 @@ import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -81,16 +80,12 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb @Override public Boolean deleteAlarm(Alarm alarm, SecurityUser user) throws ThingsboardException { try { - List relatedEdgeIds = findRelatedEdgeIds(user.getTenantId(), alarm.getOriginator()); - notificationEntityService.notifyDeleteEntity(user.getTenantId(), alarm.getId(), alarm, user.getCustomerId(), - ActionType.DELETED, relatedEdgeIds, user, json.writeValueAsString(alarm)); - return alarmService.deleteAlarm(user.getTenantId(), alarm.getId()).isSuccessful(); + List relatedEdgeIds = findRelatedEdgeIds(user.getTenantId(), alarm.getOriginator()); + notificationEntityService.notifyDeleteEntity(user.getTenantId(), alarm.getId(), alarm, user.getCustomerId(), + ActionType.DELETED, relatedEdgeIds, user, json.writeValueAsString(alarm)); + return alarmService.deleteAlarm(user.getTenantId(), alarm.getId()).isSuccessful(); } catch (Exception e) { throw handleException(e); } } - - - @Override - public void delete(Alarm entity, AlarmId entityId, SecurityUser user) throws ThingsboardException {} -} +} \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java index 8a169b8f88..85bff96d47 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java @@ -17,11 +17,11 @@ package org.thingsboard.server.service.entitiy.alarm; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.common.data.id.AlarmId; -import org.thingsboard.server.service.entitiy.SimpleTbEntityService; import org.thingsboard.server.service.security.model.SecurityUser; -public interface TbAlarmService extends SimpleTbEntityService { +public interface TbAlarmService { + + Alarm save(Alarm entity, SecurityUser user) throws ThingsboardException; void ack(Alarm alarm, SecurityUser user) throws ThingsboardException; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java index f5250c7ee9..ba3141983d 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java @@ -55,7 +55,7 @@ public class DefaultTbCustomerService extends AbstractTbEntityService implements TenantId tenantId = user.getTenantId(); try { List relatedEdgeIds = findRelatedEdgeIds(tenantId, customerId); - customerService.deleteCustomer(tenantId, (CustomerId) customerId); + customerService.deleteCustomer(tenantId, customerId); notificationEntityService.notifyDeleteEntity(tenantId, customerId, customer, user.getCustomerId(), ActionType.DELETED, relatedEdgeIds, user, null); } catch (Exception e) { From f32847a8692678e23ff787d5053a62d6244c853b Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 16 May 2022 13:03:21 +0200 Subject: [PATCH 153/276] typo fix --- .../server/dao/service/validator/QueueValidator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/QueueValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/QueueValidator.java index 4875887b18..8257a6e659 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/QueueValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/QueueValidator.java @@ -57,7 +57,7 @@ public class QueueValidator extends DataValidator { if (!foundQueue.getName().equals(queue.getName())) { throw new DataValidationException("Queue name can't be changed!"); } - if (!foundQueue.getTopic().equals(queue.getTopic())) {QueueValidator + if (!foundQueue.getTopic().equals(queue.getTopic())) { throw new DataValidationException("Queue topic can't be changed!"); } return foundQueue; From 4a20d153c573ae31d95df5e0cd34048933011014 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 16 May 2022 16:35:07 +0300 Subject: [PATCH 154/276] UI: Use 'Maximum entities per datasource' parameter from widget configuration instead of hardcoded value 1024 --- ui-ngx/src/app/core/api/alias-controller.ts | 10 ++++++---- ui-ngx/src/app/core/api/widget-api.models.ts | 4 +++- ui-ngx/src/app/core/api/widget-subscription.ts | 8 +++++--- ui-ngx/src/app/core/http/entity.service.ts | 6 ++++-- .../home/components/widget/lib/maps/map-widget2.ts | 8 ++++++-- .../components/widget/lib/maps/providers/image-map.ts | 1 + .../components/widget/lib/markdown-widget.component.ts | 6 ++++-- .../components/widget/widget-config.component.html | 8 ++++++++ .../home/components/widget/widget-config.component.ts | 2 ++ .../modules/home/components/widget/widget.component.ts | 3 ++- ui-ngx/src/app/shared/models/widget.models.ts | 1 + ui-ngx/src/assets/locale/locale.constant-en_US.json | 1 + 12 files changed, 43 insertions(+), 15 deletions(-) diff --git a/ui-ngx/src/app/core/api/alias-controller.ts b/ui-ngx/src/app/core/api/alias-controller.ts index 14a26388a5..7ad7d7c715 100644 --- a/ui-ngx/src/app/core/api/alias-controller.ts +++ b/ui-ngx/src/app/core/api/alias-controller.ts @@ -17,14 +17,15 @@ import { AliasInfo, IAliasController, StateControllerHolder, StateEntityInfo } from '@core/api/widget-api.models'; import { forkJoin, Observable, of, ReplaySubject, Subject } from 'rxjs'; import { Datasource, DatasourceType, datasourceTypeTranslationMap } from '@app/shared/models/widget.models'; -import { deepClone, isEqual } from '@core/utils'; +import { deepClone, isDefinedAndNotNull, isEqual } from '@core/utils'; import { EntityService } from '@core/http/entity.service'; import { UtilsService } from '@core/services/utils.service'; import { AliasFilterType, EntityAliases, SingleEntityFilter } from '@shared/models/alias.models'; import { EntityInfo } from '@shared/models/entity.models'; import { map, mergeMap } from 'rxjs/operators'; import { - defaultEntityDataPageLink, Filter, FilterInfo, filterInfoToKeyFilters, Filters, KeyFilter, singleEntityDataPageLink, + createDefaultEntityDataPageLink, + Filter, FilterInfo, filterInfoToKeyFilters, Filters, KeyFilter, singleEntityDataPageLink, updateDatasourceFromEntityInfo } from '@shared/models/query/query.models'; import { TranslateService } from '@ngx-translate/core'; @@ -322,7 +323,7 @@ export class AliasController implements IAliasController { ); } - resolveDatasources(datasources: Array, singleEntity?: boolean): Observable> { + resolveDatasources(datasources: Array, singleEntity?: boolean, pageSize = 1024): Observable> { if (!datasources || !datasources.length) { return of([]); } @@ -360,7 +361,8 @@ export class AliasController implements IAliasController { if (singleEntity) { datasource.pageLink = deepClone(singleEntityDataPageLink); } else if (!datasource.pageLink) { - datasource.pageLink = deepClone(defaultEntityDataPageLink); + pageSize = isDefinedAndNotNull(pageSize) && pageSize > 0 ? pageSize : 1024; + datasource.pageLink = createDefaultEntityDataPageLink(pageSize); } } }); diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index 6ae258663c..6a413f4a95 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -119,7 +119,7 @@ export interface IAliasController { getEntityAliasId(aliasName: string): string; getInstantAliasInfo(aliasId: string): AliasInfo; resolveSingleEntityInfo(aliasId: string): Observable; - resolveDatasources(datasources: Array, singleEntity?: boolean): Observable>; + resolveDatasources(datasources: Array, singleEntity?: boolean, pageSize?: number): Observable>; resolveAlarmSource(alarmSource: Datasource): Observable; getEntityAliases(): EntityAliases; getFilters(): Filters; @@ -184,6 +184,7 @@ export interface SubscriptionInfo { deviceName?: string; deviceNamePrefix?: string; deviceIds?: Array; + pageSize?: number; } export class WidgetSubscriptionContext { @@ -242,6 +243,7 @@ export interface WidgetSubscriptionOptions { datasourcesOptional?: boolean; hasDataPageLink?: boolean; singleEntity?: boolean; + pageSize?: number; warnOnPageDataOverflow?: boolean; ignoreDataUpdateOnIntervalTick?: boolean; targetDeviceAliasIds?: Array; diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index bdbddffdcb..635e98d89c 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -97,6 +97,7 @@ export class WidgetSubscription implements IWidgetSubscription { hasDataPageLink: boolean; singleEntity: boolean; + pageSize: number; warnOnPageDataOverflow: boolean; ignoreDataUpdateOnIntervalTick: boolean; @@ -229,6 +230,7 @@ export class WidgetSubscription implements IWidgetSubscription { this.entityDataListeners = []; this.hasDataPageLink = options.hasDataPageLink; this.singleEntity = options.singleEntity; + this.pageSize = options.pageSize; this.warnOnPageDataOverflow = options.warnOnPageDataOverflow; this.ignoreDataUpdateOnIntervalTick = options.ignoreDataUpdateOnIntervalTick; this.datasourcePages = []; @@ -387,7 +389,7 @@ export class WidgetSubscription implements IWidgetSubscription { } ); } else { - this.ctx.aliasController.resolveDatasources(this.configuredDatasources, this.singleEntity).subscribe( + this.ctx.aliasController.resolveDatasources(this.configuredDatasources, this.singleEntity, this.pageSize).subscribe( (datasources) => { this.configuredDatasources = datasources; this.prepareDataSubscriptions().subscribe( @@ -1132,7 +1134,7 @@ export class WidgetSubscription implements IWidgetSubscription { } ); } else { - this.ctx.aliasController.resolveDatasources(this.configuredDatasources, this.singleEntity).subscribe( + this.ctx.aliasController.resolveDatasources(this.configuredDatasources, this.singleEntity, this.pageSize).subscribe( (datasources) => { this.configuredDatasources = datasources; this.prepareDataSubscriptions().subscribe( @@ -1271,7 +1273,7 @@ export class WidgetSubscription implements IWidgetSubscription { totalPages: pageData.totalPages }; if (datasource.type === DatasourceType.entity && - pageData.hasNext && pageLink.pageSize > 1) { + pageData.hasNext && !this.singleEntity) { if (this.warnOnPageDataOverflow) { const message = this.ctx.translate.instant('widget.data-overflow', {count: pageData.data.length, total: pageData.totalElements}); diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index b71f5a95ef..1b27367ece 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -1319,7 +1319,8 @@ export class EntityService { pageLink = deepClone(singleEntityDataPageLink); } else { nameFilter = subscriptionInfo.entityNamePrefix; - pageLink = deepClone(defaultEntityDataPageLink); + const pageSize = isDefinedAndNotNull(subscriptionInfo.pageSize) && subscriptionInfo.pageSize > 0 ? subscriptionInfo.pageSize : 1024; + pageLink = createDefaultEntityDataPageLink(pageSize); } datasource.entityFilter = { type: AliasFilterType.entityName, @@ -1333,7 +1334,8 @@ export class EntityService { entityType: subscriptionInfo.entityType, entityList: subscriptionInfo.entityIds }; - datasource.pageLink = deepClone(defaultEntityDataPageLink); + const pageSize = isDefinedAndNotNull(subscriptionInfo.pageSize) && subscriptionInfo.pageSize > 0 ? subscriptionInfo.pageSize : 1024; + datasource.pageLink = createDefaultEntityDataPageLink(pageSize); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts index d603848018..bccc8f394f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts @@ -45,7 +45,7 @@ import { TranslateService } from '@ngx-translate/core'; import { UtilsService } from '@core/services/utils.service'; import { EntityDataPageLink } from '@shared/models/query/query.models'; import { providerClass } from '@home/components/widget/lib/maps/providers'; -import { isDefined, parseFunction } from '@core/utils'; +import { isDefined, isDefinedAndNotNull, parseFunction } from '@core/utils'; import L from 'leaflet'; import { forkJoin, Observable, of } from 'rxjs'; import { AttributeService } from '@core/http/attribute.service'; @@ -87,9 +87,13 @@ export class MapWidgetController implements MapWidgetInterface { this.map.saveMarkerLocation = this.setMarkerLocation.bind(this); this.map.savePolygonLocation = this.savePolygonLocation.bind(this); this.map.saveLocation = this.saveLocation.bind(this); + let pageSize = this.settings.mapPageSize; + if (isDefinedAndNotNull(this.ctx.widgetConfig.pageSize)) { + pageSize = Math.max(pageSize, this.ctx.widgetConfig.pageSize); + } this.pageLink = { page: 0, - pageSize: this.settings.mapPageSize, + pageSize, textSearch: null, dynamic: true }; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts index 411258d3af..d72c2798f4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts @@ -88,6 +88,7 @@ export class ImageMap extends LeafletMap { const imageUrlSubscriptionOptions: WidgetSubscriptionOptions = { datasources, hasDataPageLink: true, + singleEntity: true, useDashboardTimewindow: false, type: widgetType.latest, callbacks: { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts index 54988775c3..daf2e40221 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts @@ -26,7 +26,7 @@ import { fillDataPattern, flatFormattedData, formattedDataFormDatasourceData, - hashCode, + hashCode, isDefinedAndNotNull, isNotEmptyStr, parseFunction, processDataPattern, safeExecute @@ -83,9 +83,11 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit { cssParser.cssPreviewNamespace = this.markdownClass; cssParser.createStyleElement(this.markdownClass, cssString); } + const pageSize = isDefinedAndNotNull(this.ctx.widgetConfig.pageSize) && + this.ctx.widgetConfig.pageSize > 0 ? this.ctx.widgetConfig.pageSize : 16384; const pageLink: EntityDataPageLink = { page: 0, - pageSize: 16384, + pageSize, textSearch: null, dynamic: true }; diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index b3df4de3be..b3ef13bbae 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -317,6 +317,14 @@ widget-config.data-settings +
+ + widget-config.data-page-size + + +
widget-config.units diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index 20ec1d6340..db593e64f2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -213,6 +213,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont widgetStyle: [null, []], widgetCss: [null, []], titleStyle: [null, []], + pageSize: [1024, [Validators.min(1), Validators.pattern(/^\d*$/)]], units: [null, []], decimals: [null, [Validators.min(0), Validators.max(15), Validators.pattern(/^\d*$/)]], noDataDisplayMessage: [null, []], @@ -420,6 +421,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont fontSize: '16px', fontWeight: 400 }, + pageSize: isDefined(config.pageSize) ? config.pageSize : 1024, units: config.units, decimals: config.decimals, noDataDisplayMessage: isDefined(config.noDataDisplayMessage) ? config.noDataDisplayMessage : '', diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index da8fa8cbd0..11f168a3b0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -977,7 +977,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI ignoreDataUpdateOnIntervalTick: this.typeParameters.ignoreDataUpdateOnIntervalTick, comparisonEnabled: comparisonSettings.comparisonEnabled, timeForComparison: comparisonSettings.timeForComparison, - comparisonCustomIntervalValue: comparisonSettings.comparisonCustomIntervalValue + comparisonCustomIntervalValue: comparisonSettings.comparisonCustomIntervalValue, + pageSize: this.widget.config.pageSize }; if (this.widget.type === widgetType.alarm) { options.alarmSource = deepClone(this.widget.config.alarmSource); diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 3a7ef4426f..12167a2f18 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -554,6 +554,7 @@ export interface WidgetConfig { units?: string; decimals?: number; noDataDisplayMessage?: string; + pageSize?: number; actions?: {[actionSourceId: string]: Array}; settings?: WidgetSettings; alarmSource?: Datasource; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 904f5e2fa0..cf576aedbf 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3246,6 +3246,7 @@ "advanced-settings": "Advanced settings", "data-settings": "Data settings", "no-data-display-message": "\"No data to display\" alternative message", + "data-page-size": "Maximum entities per datasource", "settings-component-not-found": "Settings form component not found for selector '{{selector}}'" }, "widget-type": { From f30e769ecc49e1b2c2e7e2872f0dec57e913d704 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Mon, 16 May 2022 14:39:43 +0300 Subject: [PATCH 155/276] Fix EntityViewController test --- .../BaseEntityViewControllerTest.java | 53 ++++++++++++------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java index 155c768cb0..615914c6d7 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java @@ -62,6 +62,7 @@ import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import static java.util.concurrent.TimeUnit.HOURS; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; @@ -368,7 +369,7 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes public void testTheCopyOfAttrsIntoTSForTheView() throws Exception { Set expectedActualAttributesSet = Set.of("caKey1", "caKey2", "caKey3", "caKey4"); Set actualAttributesSet = - getAttributesByKeys("{\"caKey1\":\"value1\", \"caKey2\":true, \"caKey3\":42.0, \"caKey4\":73}", expectedActualAttributesSet); + putAttributesAndWait("{\"caKey1\":\"value1\", \"caKey2\":true, \"caKey3\":42.0, \"caKey4\":73}", expectedActualAttributesSet); log.debug("got correct actualAttributesSet, saving new entity view..."); EntityView savedView = getNewSavedEntityView("Test entity view"); @@ -389,13 +390,15 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes @Test public void testTheCopyOfAttrsOutOfTSForTheView() throws Exception { + long now = System.currentTimeMillis(); Set expectedActualAttributesSet = Set.of("caKey1", "caKey2", "caKey3", "caKey4"); Set actualAttributesSet = - getAttributesByKeys("{\"caKey1\":\"value1\", \"caKey2\":true, \"caKey3\":42.0, \"caKey4\":73}", expectedActualAttributesSet); + putAttributesAndWait("{\"caKey1\":\"value1\", \"caKey2\":true, \"caKey3\":42.0, \"caKey4\":73}", expectedActualAttributesSet); - List> valueTelemetryOfDevices = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + testDevice.getId().getId().toString() + - "/values/attributes?keys=" + String.join(",", actualAttributesSet), new TypeReference<>() { + List> values = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + testDevice.getId() + + "/values/attributes?keys=" + String.join(",", expectedActualAttributesSet), new TypeReference<>() { }); + assertEquals(expectedActualAttributesSet.size(), values.size()); EntityView view = new EntityView(); view.setEntityId(testDevice.getId()); @@ -403,12 +406,12 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes view.setName("Test entity view"); view.setType("default"); view.setKeys(telemetry); - view.setStartTimeMs((long) getValue(valueTelemetryOfDevices, "lastActivityTime") * 10); - view.setEndTimeMs((long) getValue(valueTelemetryOfDevices, "lastActivityTime") / 10); + view.setStartTimeMs(now - HOURS.toMillis(1)); + view.setEndTimeMs(now - 1); EntityView savedView = doPost("/api/entityView", view, EntityView.class); - List> values = doGetAsyncTyped("/api/plugins/telemetry/ENTITY_VIEW/" + savedView.getId().getId().toString() + - "/values/attributes?keys=" + String.join(",", actualAttributesSet), new TypeReference<>() { + values = doGetAsyncTyped("/api/plugins/telemetry/ENTITY_VIEW/" + savedView.getId().getId().toString() + + "/values/attributes?keys=" + String.join(",", expectedActualAttributesSet), new TypeReference<>() { }); assertEquals(0, values.size()); } @@ -431,19 +434,19 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes uploadTelemetry("{\"tsKey1\":\"value1\", \"tsKey2\":true, \"tsKey3\":40.0}", accessToken); getWsClient().waitForUpdate(); - long startTimeMs = System.currentTimeMillis(); + long startTimeMs = getCurTsButNotPrevTs(now); getWsClient().registerWaitForUpdate(); uploadTelemetry("{\"tsKey1\":\"value2\", \"tsKey2\":false, \"tsKey3\":80.0}", accessToken); getWsClient().waitForUpdate(); - Thread.sleep(3); + long middleOfTestMs = getCurTsButNotPrevTs(startTimeMs); getWsClient().registerWaitForUpdate(); uploadTelemetry("{\"tsKey1\":\"value3\", \"tsKey2\":false, \"tsKey3\":120.0}", accessToken); getWsClient().waitForUpdate(); - long endTimeMs = System.currentTimeMillis(); + long endTimeMs = getCurTsButNotPrevTs(middleOfTestMs); getWsClient().registerWaitForUpdate(); uploadTelemetry("{\"tsKey1\":\"value4\", \"tsKey2\":true, \"tsKey3\":160.0}", accessToken); getWsClient().waitForUpdate(); @@ -455,15 +458,25 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes EntityView savedView = doPost("/api/entityView", view, EntityView.class); String entityViewId = savedView.getId().getId().toString(); - Map>> expectedValues = getTelemetryValues("DEVICE", deviceId, keys, 0L, (startTimeMs + endTimeMs) / 2); - Assert.assertEquals(2, expectedValues.get("tsKey1").size()); - Assert.assertEquals(2, expectedValues.get("tsKey2").size()); - Assert.assertEquals(2, expectedValues.get("tsKey3").size()); + Map>> actualDeviceValues = getTelemetryValues("DEVICE", deviceId, keys, 0L, middleOfTestMs); + Assert.assertEquals(2, actualDeviceValues.get("tsKey1").size()); + Assert.assertEquals(2, actualDeviceValues.get("tsKey2").size()); + Assert.assertEquals(2, actualDeviceValues.get("tsKey3").size()); + + Map>> actualEntityViewValues = getTelemetryValues("ENTITY_VIEW", entityViewId, keys, 0L, middleOfTestMs); + Assert.assertEquals(1, actualEntityViewValues.get("tsKey1").size()); + Assert.assertEquals(1, actualEntityViewValues.get("tsKey2").size()); + Assert.assertEquals(1, actualEntityViewValues.get("tsKey3").size()); + } - Map>> actualValues = getTelemetryValues("ENTITY_VIEW", entityViewId, keys, 0L, (startTimeMs + endTimeMs) / 2); - Assert.assertEquals(1, actualValues.get("tsKey1").size()); - Assert.assertEquals(1, actualValues.get("tsKey2").size()); - Assert.assertEquals(1, actualValues.get("tsKey3").size()); + private static long getCurTsButNotPrevTs(long prevTs) throws InterruptedException { + long result = System.currentTimeMillis(); + if (prevTs == result) { + Thread.sleep(1); + return getCurTsButNotPrevTs(prevTs); + } else { + return result; + } } private void uploadTelemetry(String strKvs, String accessToken) throws Exception { @@ -504,7 +517,7 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes }); } - private Set getAttributesByKeys(String stringKV, Set expectedKeySet) throws Exception { + private Set putAttributesAndWait(String stringKV, Set expectedKeySet) throws Exception { DeviceTypeFilter dtf = new DeviceTypeFilter(testDevice.getType(), testDevice.getName()); List keysToSubscribe = expectedKeySet.stream() .map(key -> new EntityKey(EntityKeyType.CLIENT_ATTRIBUTE, key)) From ec7a36cd896c2779ed23e5192afaa43a01106a8f Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Mon, 16 May 2022 16:58:29 +0300 Subject: [PATCH 156/276] Fix race conditions on WS subscribe commands processing --- ...efaultTbEntityDataSubscriptionService.java | 75 +++++++++++-------- .../subscription/TbAbstractSubCtx.java | 18 ++++- .../subscription/TbAlarmDataSubCtx.java | 8 +- .../subscription/TbEntityCountSubCtx.java | 7 +- .../subscription/TbEntityDataSubCtx.java | 10 +-- .../DefaultTelemetryWebSocketService.java | 6 +- .../controller/BaseWebsocketApiTest.java | 14 +--- .../controller/TbTestWebSocketClient.java | 4 +- 8 files changed, 78 insertions(+), 64 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java index e0eb4820fd..82d9eb779a 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -228,7 +228,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc } } else if (!theCtx.isInitialDataSent()) { EntityDataUpdate update = new EntityDataUpdate(theCtx.getCmdId(), theCtx.getData(), null, theCtx.getMaxEntitiesPerDataSubscription()); - wsService.sendWsMsg(theCtx.getSessionId(), update); + theCtx.sendWsMsg(update); theCtx.setInitialDataSent(true); } } catch (RuntimeException e) { @@ -287,7 +287,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc ctx.clearEntitySubscriptions(); if (entities.isEmpty()) { AlarmDataUpdate update = new AlarmDataUpdate(cmd.getCmdId(), new PageData<>(), null, 0, 0); - wsService.sendWsMsg(ctx.getSessionId(), update); + ctx.sendWsMsg(update); } else { ctx.fetchAlarms(); ctx.createLatestValuesSubscriptions(cmd.getQuery().getLatestValues()); @@ -420,22 +420,26 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc } } catch (InterruptedException | ExecutionException e) { log.warn("[{}][{}][{}] Failed to fetch historical data", ctx.getSessionId(), ctx.getCmdId(), entityData.getEntityId(), e); - wsService.sendWsMsg(ctx.getSessionId(), - new EntityDataUpdate(ctx.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR.getCode(), "Failed to fetch historical data!")); + ctx.sendWsMsg(new EntityDataUpdate(ctx.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR.getCode(), "Failed to fetch historical data!")); } }); - EntityDataUpdate update; - if (!ctx.isInitialDataSent()) { - update = new EntityDataUpdate(ctx.getCmdId(), ctx.getData(), null, ctx.getMaxEntitiesPerDataSubscription()); - ctx.setInitialDataSent(true); - } else { - update = new EntityDataUpdate(ctx.getCmdId(), null, ctx.getData().getData(), ctx.getMaxEntitiesPerDataSubscription()); - } - wsService.sendWsMsg(ctx.getSessionId(), update); - if (subscribe) { - ctx.createTimeseriesSubscriptions(keys.stream().map(key -> new EntityKey(EntityKeyType.TIME_SERIES, key)).collect(Collectors.toList()), cmd.getStartTs(), cmd.getEndTs()); + ctx.getWsLock().lock(); + try { + EntityDataUpdate update; + if (!ctx.isInitialDataSent()) { + update = new EntityDataUpdate(ctx.getCmdId(), ctx.getData(), null, ctx.getMaxEntitiesPerDataSubscription()); + ctx.setInitialDataSent(true); + } else { + update = new EntityDataUpdate(ctx.getCmdId(), null, ctx.getData().getData(), ctx.getMaxEntitiesPerDataSubscription()); + } + if (subscribe) { + ctx.createTimeseriesSubscriptions(keys.stream().map(key -> new EntityKey(EntityKeyType.TIME_SERIES, key)).collect(Collectors.toList()), cmd.getStartTs(), cmd.getEndTs()); + } + ctx.sendWsMsg(update); + ctx.getData().getData().forEach(ed -> ed.getTimeseries().clear()); + } finally { + ctx.getWsLock().unlock(); } - ctx.getData().getData().forEach(ed -> ed.getTimeseries().clear()); return ctx; }, wsCallBackExecutor); } @@ -464,7 +468,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc ListenableFuture> missingTsData = tsService.findLatest(ctx.getTenantId(), entityData.getEntityId(), missingTsKeys); missingTelemetryFutures.put(entityData, Futures.transform(missingTsData, this::toTsValue, MoreExecutors.directExecutor())); } - Futures.addCallback(Futures.allAsList(missingTelemetryFutures.values()), new FutureCallback>>() { + Futures.addCallback(Futures.allAsList(missingTelemetryFutures.values()), new FutureCallback<>() { @Override public void onSuccess(@Nullable List> result) { missingTelemetryFutures.forEach((key, value) -> { @@ -475,30 +479,39 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc } }); EntityDataUpdate update; - if (!ctx.isInitialDataSent()) { - update = new EntityDataUpdate(ctx.getCmdId(), ctx.getData(), null, ctx.getMaxEntitiesPerDataSubscription()); - ctx.setInitialDataSent(true); - } else { - update = new EntityDataUpdate(ctx.getCmdId(), null, ctx.getData().getData(), ctx.getMaxEntitiesPerDataSubscription()); + ctx.getWsLock().lock(); + try { + ctx.createLatestValuesSubscriptions(latestCmd.getKeys()); + if (!ctx.isInitialDataSent()) { + update = new EntityDataUpdate(ctx.getCmdId(), ctx.getData(), null, ctx.getMaxEntitiesPerDataSubscription()); + ctx.setInitialDataSent(true); + } else { + update = new EntityDataUpdate(ctx.getCmdId(), null, ctx.getData().getData(), ctx.getMaxEntitiesPerDataSubscription()); + } + ctx.sendWsMsg(update); + } finally { + ctx.getWsLock().unlock(); } - wsService.sendWsMsg(ctx.getSessionId(), update); - ctx.createLatestValuesSubscriptions(latestCmd.getKeys()); } @Override public void onFailure(Throwable t) { log.warn("[{}][{}] Failed to process websocket command: {}:{}", ctx.getSessionId(), ctx.getCmdId(), ctx.getQuery(), latestCmd, t); - wsService.sendWsMsg(ctx.getSessionId(), - new EntityDataUpdate(ctx.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR.getCode(), "Failed to process websocket command!")); + ctx.sendWsMsg(new EntityDataUpdate(ctx.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR.getCode(), "Failed to process websocket command!")); } }, wsCallBackExecutor); } else { - if (!ctx.isInitialDataSent()) { - EntityDataUpdate update = new EntityDataUpdate(ctx.getCmdId(), ctx.getData(), null, ctx.getMaxEntitiesPerDataSubscription()); - wsService.sendWsMsg(ctx.getSessionId(), update); - ctx.setInitialDataSent(true); + ctx.getWsLock().lock(); + try { + ctx.createLatestValuesSubscriptions(latestCmd.getKeys()); + if (!ctx.isInitialDataSent()) { + EntityDataUpdate update = new EntityDataUpdate(ctx.getCmdId(), ctx.getData(), null, ctx.getMaxEntitiesPerDataSubscription()); + ctx.sendWsMsg(update); + ctx.setInitialDataSent(true); + } + } finally { + ctx.getWsLock().unlock(); } - ctx.createLatestValuesSubscriptions(latestCmd.getKeys()); } } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java index 749825f644..a8d2472eb0 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -41,6 +41,7 @@ import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; +import org.thingsboard.server.service.telemetry.cmd.v2.CmdUpdate; import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; import java.util.ArrayList; @@ -52,14 +53,18 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; @Slf4j @Data public abstract class TbAbstractSubCtx { + @Getter + protected final Lock wsLock = new ReentrantLock(true); protected final String serviceId; protected final SubscriptionServiceStatistics stats; - protected final TelemetryWebSocketService wsService; + private final TelemetryWebSocketService wsService; protected final EntityService entityService; protected final TbLocalSubscriptionService localSubscriptionService; protected final AttributesService attributesService; @@ -314,4 +319,13 @@ public abstract class TbAbstractSubCtx { private final String sourceAttribute; } + public void sendWsMsg(CmdUpdate update) { + wsLock.lock(); + try { + wsService.sendWsMsg(sessionRef.getSessionId(), update); + } finally { + wsLock.unlock(); + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java index d87f1557ff..8655a91e96 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -117,7 +117,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { } else { update = new AlarmDataUpdate(cmdId, new PageData<>(), null, maxEntitiesPerAlarmSubscription, data.getTotalElements()); } - wsService.sendWsMsg(getSessionId(), update); + sendWsMsg(update); } public void fetchData() { @@ -198,7 +198,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { return alarm; }).collect(Collectors.toList()); if (!update.isEmpty()) { - wsService.sendWsMsg(sessionId, new AlarmDataUpdate(cmdId, null, update, maxEntitiesPerAlarmSubscription, data.getTotalElements())); + sendWsMsg(new AlarmDataUpdate(cmdId, null, update, maxEntitiesPerAlarmSubscription, data.getTotalElements())); } } else { log.trace("[{}][{}][{}][{}] Received stale subscription update: {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), keyType, subscriptionUpdate); @@ -222,7 +222,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { AlarmData updated = new AlarmData(alarm, current.getOriginatorName(), current.getEntityId()); updated.getLatest().putAll(current.getLatest()); alarmsMap.put(alarmId, updated); - wsService.sendWsMsg(sessionId, new AlarmDataUpdate(cmdId, null, Collections.singletonList(updated), maxEntitiesPerAlarmSubscription, data.getTotalElements())); + sendWsMsg(new AlarmDataUpdate(cmdId, null, Collections.singletonList(updated), maxEntitiesPerAlarmSubscription, data.getTotalElements())); } else { fetchAlarms(); } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityCountSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityCountSubCtx.java index 53df7c4aaf..7fc87eb17c 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityCountSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityCountSubCtx.java @@ -17,14 +17,11 @@ package org.thingsboard.server.service.subscription; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.query.EntityCountQuery; -import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountUpdate; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; -import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; @Slf4j public class TbEntityCountSubCtx extends TbAbstractSubCtx { @@ -40,7 +37,7 @@ public class TbEntityCountSubCtx extends TbAbstractSubCtx { @Override public void fetchData() { result = (int) entityService.countEntitiesByQuery(getTenantId(), getCustomerId(), query); - wsService.sendWsMsg(sessionRef.getSessionId(), new EntityCountUpdate(cmdId, result)); + sendWsMsg(new EntityCountUpdate(cmdId, result)); } @Override @@ -48,7 +45,7 @@ public class TbEntityCountSubCtx extends TbAbstractSubCtx { int newCount = (int) entityService.countEntitiesByQuery(getTenantId(), getCustomerId(), query); if (newCount != result) { result = newCount; - wsService.sendWsMsg(sessionRef.getSessionId(), new EntityCountUpdate(cmdId, result)); + sendWsMsg(new EntityCountUpdate(cmdId, result)); } } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java index b8211e50cc..6423604c4e 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -51,7 +51,7 @@ public class TbEntityDataSubCtx extends TbAbstractDataSubCtx { @Getter @Setter - private boolean initialDataSent; + private volatile boolean initialDataSent; private TimeSeriesCmd curTsCmd; private LatestValueCmd latestValueCmd; @Getter @@ -121,7 +121,7 @@ public class TbEntityDataSubCtx extends TbAbstractDataSubCtx { if (!latestUpdate.isEmpty()) { Map> latestMap = Collections.singletonMap(keyType, latestUpdate); entityData = new EntityData(entityId, latestMap, null); - wsService.sendWsMsg(sessionId, new EntityDataUpdate(cmdId, null, Collections.singletonList(entityData), maxEntitiesPerDataSubscription)); + sendWsMsg(new EntityDataUpdate(cmdId, null, Collections.singletonList(entityData), maxEntitiesPerDataSubscription)); } } @@ -162,7 +162,7 @@ public class TbEntityDataSubCtx extends TbAbstractDataSubCtx { Map tsMap = new HashMap<>(); tsUpdate.forEach((key, tsValue) -> tsMap.put(key, tsValue.toArray(new TsValue[tsValue.size()]))); EntityData entityData = new EntityData(entityId, null, tsMap); - wsService.sendWsMsg(sessionId, new EntityDataUpdate(cmdId, null, Collections.singletonList(entityData), maxEntitiesPerDataSubscription)); + sendWsMsg(new EntityDataUpdate(cmdId, null, Collections.singletonList(entityData), maxEntitiesPerDataSubscription)); } } @@ -219,9 +219,9 @@ public class TbEntityDataSubCtx extends TbAbstractDataSubCtx { } } } - wsService.sendWsMsg(sessionRef.getSessionId(), new EntityDataUpdate(cmdId, data, null, maxEntitiesPerDataSubscription)); subIdsToCancel.forEach(subId -> localSubscriptionService.cancelSubscription(getSessionId(), subId)); subsToAdd.forEach(localSubscriptionService::addSubscription); + sendWsMsg(new EntityDataUpdate(cmdId, data, null, maxEntitiesPerDataSubscription)); } public void setCurrentCmd(EntityDataCmd cmd) { diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java index 67fe9346a7..5a50a2ea22 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java @@ -442,7 +442,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi private void handleWsAttributesSubscriptionByKeys(TelemetryWebSocketSessionRef sessionRef, AttributesSubscriptionCmd cmd, String sessionId, EntityId entityId, List keys) { - FutureCallback> callback = new FutureCallback>() { + FutureCallback> callback = new FutureCallback<>() { @Override public void onSuccess(List data) { List attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList()); @@ -542,7 +542,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi private void handleWsAttributesSubscription(TelemetryWebSocketSessionRef sessionRef, AttributesSubscriptionCmd cmd, String sessionId, EntityId entityId) { - FutureCallback> callback = new FutureCallback>() { + FutureCallback> callback = new FutureCallback<>() { @Override public void onSuccess(List data) { List attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList()); @@ -666,7 +666,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi } private FutureCallback> getSubscriptionCallback(final TelemetryWebSocketSessionRef sessionRef, final TimeseriesSubscriptionCmd cmd, final String sessionId, final EntityId entityId, final long startTs, final List keys) { - return new FutureCallback>() { + return new FutureCallback<>() { @Override public void onSuccess(List data) { sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data)); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java index 90d6ac7b9d..f7838d9689 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java @@ -102,7 +102,6 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest { List tsData = Arrays.asList(dataPoint1, dataPoint2, dataPoint3); sendTelemetry(device, tsData); - Thread.sleep(100); update = getWsClient().sendHistoryCmd(keys, now, TimeUnit.HOURS.toMillis(1), dtf); @@ -136,7 +135,6 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest { List tsData = Arrays.asList(dataPoint1, dataPoint2, dataPoint3); sendTelemetry(device, tsData); - Thread.sleep(100); update = getWsClient().subscribeTsUpdate(List.of("temperature"), now, TimeUnit.HOURS.toMillis(1)); Assert.assertEquals(1, update.getCmdId()); @@ -153,7 +151,6 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest { now = System.currentTimeMillis(); TsKvEntry dataPoint4 = new BasicTsKvEntry(now, new LongDataEntry("temperature", 45L)); getWsClient().registerWaitForUpdate(); - Thread.sleep(100); sendTelemetry(device, Arrays.asList(dataPoint4)); String msg = getWsClient().waitForUpdate(); @@ -309,13 +306,12 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest { Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getTs()); Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()); + getWsClient().registerWaitForUpdate(); TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L)); List tsData = Arrays.asList(dataPoint1); sendTelemetry(device, tsData); - Thread.sleep(100); - - update = getWsClient().subscribeLatestUpdate(keys, dtf); + update = getWsClient().parseDataReply(getWsClient().waitForUpdate()); Assert.assertEquals(1, update.getCmdId()); @@ -329,7 +325,6 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest { now = System.currentTimeMillis(); TsKvEntry dataPoint2 = new BasicTsKvEntry(now, new LongDataEntry("temperature", 52L)); - getWsClient().registerWaitForUpdate(); sendTelemetry(device, Arrays.asList(dataPoint2)); update = getWsClient().parseDataReply(getWsClient().waitForUpdate()); @@ -371,7 +366,6 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest { Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getValue()); getWsClient().registerWaitForUpdate(); - Thread.sleep(500); AttributeKvEntry dataPoint1 = new BaseAttributeKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("serverAttributeKey", 42L)); List tsData = Arrays.asList(dataPoint1); @@ -394,7 +388,6 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest { AttributeKvEntry dataPoint2 = new BaseAttributeKvEntry(now, new LongDataEntry("serverAttributeKey", 52L)); getWsClient().registerWaitForUpdate(); - Thread.sleep(500); sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, Arrays.asList(dataPoint2)); msg = getWsClient().waitForUpdate(); Assert.assertNotNull(msg); @@ -411,14 +404,12 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest { //Sending update from the past, while latest value has new timestamp; getWsClient().registerWaitForUpdate(); - Thread.sleep(500); sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, Arrays.asList(dataPoint1)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); Assert.assertNull(msg); //Sending duplicate update again getWsClient().registerWaitForUpdate(); - Thread.sleep(500); sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, Arrays.asList(dataPoint2)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); Assert.assertNull(msg); @@ -456,7 +447,6 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest { getWsClient().registerWaitForUpdate(); AttributeKvEntry dataPoint1 = new BaseAttributeKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("serverAttributeKey", 42L)); List tsData = Arrays.asList(dataPoint1); - Thread.sleep(100); sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, tsData); diff --git a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java index 6af6176b06..e2031e701f 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java @@ -44,8 +44,8 @@ import java.util.concurrent.TimeUnit; public class TbTestWebSocketClient extends WebSocketClient { private volatile String lastMsg; - private CountDownLatch reply; - private CountDownLatch update; + private volatile CountDownLatch reply; + private volatile CountDownLatch update; public TbTestWebSocketClient(URI serverUri) { super(serverUri); From 097862b59f8aa0c1d92e1e97f719142d4bea1b9d Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Mon, 16 May 2022 17:00:34 +0300 Subject: [PATCH 157/276] Licnese headers --- .../subscription/DefaultTbEntityDataSubscriptionService.java | 2 +- .../server/service/subscription/TbAbstractSubCtx.java | 2 +- .../server/service/subscription/TbAlarmDataSubCtx.java | 2 +- .../server/service/subscription/TbEntityDataSubCtx.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java index 82d9eb779a..507ddb3834 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java index a8d2472eb0..15e7f754c5 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java index 8655a91e96..5804002c77 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java index 6423604c4e..385661f177 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java @@ -5,7 +5,7 @@ * 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 + * 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, From 664d337a99fac8c4aab9259a7b81bb65bb61263e Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 16 May 2022 17:21:04 +0300 Subject: [PATCH 158/276] UI: Refactoring 2FA system admin settings --- ui-ngx/src/app/core/services/menu.service.ts | 14 - .../home/pages/admin/admin-routing.module.ts | 2 +- .../two-factor-auth-settings.component.html | 304 +++++++++--------- .../two-factor-auth-settings.component.scss | 63 +++- .../two-factor-auth-settings.component.ts | 251 +++++++++------ .../shared/models/two-factor-auth.models.ts | 52 ++- .../assets/locale/locale.constant-en_US.json | 17 +- 7 files changed, 430 insertions(+), 273 deletions(-) diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index 15ab552d53..becf23ebb6 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -374,14 +374,6 @@ export class MenuService { path: '/settings/home', icon: 'settings_applications' }, - { - id: guid(), - name: 'admin.2fa.2fa', - type: 'link', - path: '/settings/2fa', - icon: 'mdi:two-factor-authentication', - isMdiIcon: true - }, { id: guid(), name: 'resource.resources-library', @@ -518,12 +510,6 @@ export class MenuService { icon: 'settings_applications', path: '/settings/home' }, - { - name: 'admin.2fa.2fa', - icon: 'mdi:two-factor-authentication', - isMdiIcon: true, - path: '/settings/2fa' - }, { name: 'resource.resources-library', icon: 'folder', diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts index e63eec7b52..b381d39b3c 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts @@ -190,7 +190,7 @@ const routes: Routes = [ component: TwoFactorAuthSettingsComponent, canDeactivate: [ConfirmOnExitGuard], data: { - auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], + auth: [Authority.SYS_ADMIN], title: 'admin.2fa.2fa', breadcrumb: { label: 'admin.2fa.2fa', diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html index fccec76b54..bcc5a695e2 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html @@ -27,159 +27,171 @@
- +
- - {{ 'admin.2fa.use-system-two-factor-auth-settings' | translate }} - - - - admin.2fa.total-allowed-time-for-verification - - - {{ 'admin.2fa.total-allowed-time-for-verification-required' | translate }} - - - {{ 'admin.2fa.total-allowed-time-for-verification-pattern' | translate }} - - - - admin.2fa.max-verification-failures-before-user-lockout - - - {{ 'admin.2fa.max-verification-failures-before-user-lockout-required' | translate }} - - - {{ 'admin.2fa.max-verification-failures-before-user-lockout-pattern' | translate }} - - - - admin.2fa.verification-code-send-rate-limit - - - {{ 'admin.2fa.verification-code-send-rate-limit-required' | translate }} - - - {{ 'admin.2fa.verification-code-send-rate-limit-pattern' | translate }} - - - - admin.2fa.verification-code-check-rate-limit - - - {{ 'admin.2fa.verification-code-check-rate-limit-required' | translate }} - - - {{ 'admin.2fa.verification-code-check-rate-limit-pattern' | translate }} - - -
admin.2fa.available-providers
- -
- - - - - {{ provider.value.providerType }} - - - - - + +
+ admin.2fa.general-setting +
+ + admin.2fa.total-allowed-time-for-verification + + + {{ 'admin.2fa.total-allowed-time-for-verification-required' | translate }} + + + {{ 'admin.2fa.total-allowed-time-for-verification-pattern' | translate }} + + + + admin.2fa.max-verification-failures-before-user-lockout + + + {{ 'admin.2fa.max-verification-failures-before-user-lockout-required' | translate }} + + + {{ 'admin.2fa.max-verification-failures-before-user-lockout-pattern' | translate }} + + +
+ + {{ 'admin.2fa.verification-code-send-rate-limit' | translate }} + +
+ + admin.2fa.number-of-attempts + + + {{ 'admin.2fa.number-of-attempts-required' | translate }} + + + {{ 'admin.2fa.number-of-attempts-pattern' | translate }} + + + + admin.2fa.within-time + + + {{ 'admin.2fa.within-time-required' | translate }} + + + {{ 'admin.2fa.within-time-pattern' | translate }} + + +
+ + {{ 'admin.2fa.verification-code-check-rate-limit' | translate }} + +
+ + admin.2fa.number-of-attempts + + + {{ 'admin.2fa.number-of-attempts-required' | translate }} + + + {{ 'admin.2fa.number-of-attempts-pattern' | translate }} + + + + admin.2fa.within-time + + + {{ 'admin.2fa.within-time-required' | translate }} + + + {{ 'admin.2fa.within-time-pattern' | translate }} + + +
+
+
+ admin.2fa.available-providers + + + + + + + {{ provider.value.providerType }} + + - -
+ + + - admin.2fa.provider - - - {{ provider }} - - + admin.2fa.issuer-name + + + {{ "admin.2fa.issuer-name-required" | translate }} + + + +
+ + admin.2fa.verification-message-template + + + {{ "admin.2fa.verification-message-template-required" | translate }} + + + {{ "admin.2fa.verification-message-template-pattern" | translate }} + - - - - admin.2fa.issuer-name - - - {{ "admin.2fa.issuer-name-required" | translate }} - - - -
- - admin.2fa.verification-message-template - - - {{ "admin.2fa.verification-message-template-required" | translate }} - - - {{ "admin.2fa.verification-message-template-pattern" | translate }} - - - - - admin.2fa.verification-code-lifetime - - - {{ "admin.2fa.verification-code-lifetime-required" | translate }} - - - {{ "admin.2fa.verification-code-lifetime-pattern" | translate }} - - -
-
- - admin.2fa.verification-code-lifetime - - - {{ "admin.2fa.verification-code-lifetime-required" | translate }} - - - {{ "admin.2fa.verification-code-lifetime-pattern" | translate }} - - -
-
-
-
-
- -
-
+ + admin.2fa.verification-code-lifetime + + + {{ "admin.2fa.verification-code-lifetime-required" | translate }} + + + {{ "admin.2fa.verification-code-lifetime-pattern" | translate }} + + +
+
+ + admin.2fa.verification-code-lifetime + + + {{ "admin.2fa.verification-code-lifetime-required" | translate }} + + + {{ "admin.2fa.verification-code-lifetime-pattern" | translate }} + + +
+ +
+ + + + +
+ +
-
- - -
diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.scss b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.scss index 8fb412f648..2ae3981717 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.scss +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.scss @@ -15,7 +15,66 @@ */ :host{ - .container { - margin-bottom: 1em; + + .fields-group { + margin: 8px 0; + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + position: relative; + padding-bottom: 8px; + + legend { + color: rgba(0, 0, 0, .7); + width: fit-content; + } + + &:not(:last-of-type) { + padding: 8px; + } + + &:last-of-type { + margin-bottom: 24px; + legend { + margin: 0 8px; + } + } + + .rate-limit-toggle { + display: block; + margin-bottom: 8px; + } + } + + .mat-expansion-panel { + box-shadow: none; + margin: 1px 0 0; + &.provider { + overflow: inherit; + .mat-expansion-panel-header { + padding: 0 24px 0 8px; + &.mat-expanded { + height: 48px; + } + .mat-slide-toggle { + margin-right: 8px; + } + } + .mat-expansion-panel-header-title { + height: 40px; + } + } + } +} + +:host ::ng-deep { + .mat-expansion-panel { + &.provider { + .mat-expansion-panel-header > .mat-content { + overflow: inherit; + } + .mat-expansion-panel-body { + padding: 0 16px 8px 8px; + } + } } } diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts index 5caf1533ed..b23576f549 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts @@ -14,21 +14,21 @@ /// limitations under the License. /// -import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { ActivatedRoute } from '@angular/router'; -import { AbstractControl, FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { DialogService } from '@core/services/dialog.service'; -import { TranslateService } from '@ngx-translate/core'; -import { WINDOW } from '@core/services/window.service'; +import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; -import { AuthState } from '@core/auth/auth.models'; -import { getCurrentAuthState } from '@core/auth/auth.selectors'; -import { Authority } from '@shared/models/authority.enum'; -import { TwoFactorAuthProviderType, TwoFactorAuthSettings } from '@shared/models/two-factor-auth.models'; +import { + TwoFactorAuthProviderType, + TwoFactorAuthSettings, + TwoFactorAuthSettingsForm +} from '@shared/models/two-factor-auth.models'; +import { deepClone, isNotEmptyStr } from '@core/utils'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; @Component({ selector: 'tb-2fa-settings', @@ -37,56 +37,72 @@ import { TwoFactorAuthProviderType, TwoFactorAuthSettings } from '@shared/models }) export class TwoFactorAuthSettingsComponent extends PageComponent implements OnInit, HasConfirmForm, OnDestroy { - private authState: AuthState = getCurrentAuthState(this.store); - private authUser = this.authState.authUser; + private readonly destroy$ = new Subject(); twoFaFormGroup: FormGroup; - twoFactorAuthProviderTypes = Object.keys(TwoFactorAuthProviderType); twoFactorAuthProviderType = TwoFactorAuthProviderType; constructor(protected store: Store, - private route: ActivatedRoute, private twoFaService: TwoFactorAuthenticationService, - private fb: FormBuilder, - private dialogService: DialogService, - private translate: TranslateService, - @Inject(WINDOW) private window: Window) { + private fb: FormBuilder) { super(store); } ngOnInit() { this.build2faSettingsForm(); this.twoFaService.getTwoFaSettings().subscribe((setting) => { - this.initTwoFactorAuthForm(setting); + this.setAuthConfigFormValue(setting); }); } ngOnDestroy() { super.ngOnDestroy(); + this.destroy$.next(); + this.destroy$.complete(); } confirmForm(): FormGroup { return this.twoFaFormGroup; } - isTenantAdmin(): boolean { - return this.authUser.authority === Authority.TENANT_ADMIN; + save() { + if (this.twoFaFormGroup.valid) { + const setting = this.twoFaFormGroup.value as TwoFactorAuthSettingsForm; + this.joinRateLimit(setting, 'verificationCodeCheckRateLimit'); + this.joinRateLimit(setting, 'verificationCodeSendRateLimit'); + const providers = setting.providers.filter(provider => provider.enable); + providers.forEach(provider => delete provider.enable); + const config = Object.assign(setting, {providers}); + this.twoFaService.saveTwoFaSettings(config).subscribe( + () => { + this.twoFaFormGroup.markAsUntouched(); + this.twoFaFormGroup.markAsPristine(); + } + ); + } else { + Object.keys(this.twoFaFormGroup.controls).forEach(field => { + const control = this.twoFaFormGroup.get(field); + control.markAsTouched({onlySelf: true}); + }); + } } - save() { - const setting = this.twoFaFormGroup.value; - this.twoFaService.saveTwoFaSettings(setting).subscribe( - (twoFactorAuthSettings) => { - this.twoFaFormGroup.patchValue(twoFactorAuthSettings, {emitEvent: false}); - this.twoFaFormGroup.markAsUntouched(); - this.twoFaFormGroup.markAsPristine(); - } - ); + toggleProviders($event: Event): void { + if ($event) { + $event.stopPropagation(); + } + } + + trackByElement(i: number, item: any) { + return item; + } + + get providersForm(): FormArray { + return this.twoFaFormGroup.get('providers') as FormArray; } private build2faSettingsForm(): void { this.twoFaFormGroup = this.fb.group({ - useSystemTwoFactorAuthSettings: [this.isTenantAdmin()], maxVerificationFailuresBeforeUserLockout: [30, [ Validators.required, Validators.pattern(/^\d*$/), @@ -98,91 +114,122 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI Validators.min(1), Validators.pattern(/^\d*$/) ]], - verificationCodeCheckRateLimit: ['3:900', [Validators.required, Validators.pattern(/^[1-9]\d*:[1-9]\d*$/)]], - verificationCodeSendRateLimit: ['1:60', [Validators.required, Validators.pattern(/^[1-9]\d*:[1-9]\d*$/)]], + verificationCodeCheckRateLimitEnable: [false], + verificationCodeCheckRateLimitNumber: ['3', [Validators.required, Validators.min(1), Validators.pattern(/^\d*$/)]], + verificationCodeCheckRateLimitTime: ['900', [Validators.required, Validators.min(1), Validators.pattern(/^\d*$/)]], + verificationCodeSendRateLimitEnable: [false], + verificationCodeSendRateLimitNumber: ['1', [Validators.required, Validators.min(1), Validators.pattern(/^\d*$/)]], + verificationCodeSendRateLimitTime: ['60', [Validators.required, Validators.min(1), Validators.pattern(/^\d*$/)]], providers: this.fb.array([]) }); - } - - private initTwoFactorAuthForm(settings: TwoFactorAuthSettings) { - settings.providers.forEach(() => { - this.addProvider(); + Object.values(TwoFactorAuthProviderType).forEach(provider => { + this.buildProvidersSettingsForm(provider); + }); + this.twoFaFormGroup.get('verificationCodeCheckRateLimitEnable').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(value => { + if (value) { + this.twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').enable({emitEvent: false}); + this.twoFaFormGroup.get('verificationCodeCheckRateLimitTime').enable({emitEvent: false}); + } else { + this.twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').disable({emitEvent: false}); + this.twoFaFormGroup.get('verificationCodeCheckRateLimitTime').disable({emitEvent: false}); + } + }); + this.twoFaFormGroup.get('verificationCodeSendRateLimitEnable').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(value => { + if (value) { + this.twoFaFormGroup.get('verificationCodeSendRateLimitNumber').enable({emitEvent: false}); + this.twoFaFormGroup.get('verificationCodeSendRateLimitTime').enable({emitEvent: false}); + } else { + this.twoFaFormGroup.get('verificationCodeSendRateLimitNumber').disable({emitEvent: false}); + this.twoFaFormGroup.get('verificationCodeSendRateLimitTime').disable({emitEvent: false}); + } }); - this.twoFaFormGroup.patchValue(settings); - this.twoFaFormGroup.markAsPristine(); } - addProvider() { - const newProviders = this.fb.group({ - providerType: [TwoFactorAuthProviderType.TOTP], - issuerName: ['ThingsBoard', Validators.required], - smsVerificationMessageTemplate: [{ - value: 'Verification code: ${verificationCode}', - disabled: true - }, [ - Validators.required, - Validators.pattern(/\${verificationCode}/) - ]], - verificationCodeLifetime: [{ - value: 120, - disabled: true - }, [ - Validators.required, - Validators.min(1), - Validators.pattern(/^\d*$/) - ]] + private setAuthConfigFormValue(settings: TwoFactorAuthSettings) { + const [checkRateLimitNumber, checkRateLimitTime] = this.splitRateLimit(settings.verificationCodeCheckRateLimit); + const [sendRateLimitNumber, sendRateLimitTime] = this.splitRateLimit(settings.verificationCodeSendRateLimit); + const allowProvidersConfig = settings.providers.map(provider => provider.providerType); + const processFormValue: TwoFactorAuthSettingsForm = Object.assign(deepClone(settings), { + verificationCodeCheckRateLimitEnable: checkRateLimitNumber > 0, + verificationCodeCheckRateLimitNumber: checkRateLimitNumber || 3, + verificationCodeCheckRateLimitTime: checkRateLimitTime || 900, + verificationCodeSendRateLimitEnable: sendRateLimitNumber > 0, + verificationCodeSendRateLimitNumber: sendRateLimitNumber || 1, + verificationCodeSendRateLimitTime: sendRateLimitTime || 60, + providers: [] }); - newProviders.get('providerType').valueChanges.subscribe(type => { - switch (type) { - case TwoFactorAuthProviderType.SMS: - newProviders.get('issuerName').disable({emitEvent: false}); - newProviders.get('smsVerificationMessageTemplate').enable({emitEvent: false}); - newProviders.get('verificationCodeLifetime').enable({emitEvent: false}); - break; - case TwoFactorAuthProviderType.TOTP: - newProviders.get('issuerName').enable({emitEvent: false}); - newProviders.get('smsVerificationMessageTemplate').disable({emitEvent: false}); - newProviders.get('verificationCodeLifetime').disable({emitEvent: false}); - break; - case TwoFactorAuthProviderType.EMAIL: - newProviders.get('issuerName').disable({emitEvent: false}); - newProviders.get('smsVerificationMessageTemplate').disable({emitEvent: false}); - newProviders.get('verificationCodeLifetime').enable({emitEvent: false}); - break; + Object.values(TwoFactorAuthProviderType).forEach(provider => { + const index = allowProvidersConfig.indexOf(provider); + if (index > -1) { + processFormValue.providers.push(Object.assign(settings.providers[index], {enable: true})); + } else { + processFormValue.providers.push({enable: false}); } }); - if (this.providersForm.length) { - const selectedProviderTypes = this.providersForm.value.map(providers => providers.providerType); - const allowProviders = this.twoFactorAuthProviderTypes.filter(provider => !selectedProviderTypes.includes(provider)); - newProviders.get('providerType').setValue(allowProviders[0]); - newProviders.updateValueAndValidity(); - } - this.providersForm.push(newProviders); - this.providersForm.markAsDirty(); + this.twoFaFormGroup.patchValue(processFormValue); } - removeProviders($event: Event, index: number): void { - if ($event) { - $event.stopPropagation(); - $event.preventDefault(); + private buildProvidersSettingsForm(provider: TwoFactorAuthProviderType) { + const formControlConfig: {[key: string]: any} = { + providerType: [provider], + enable: [false] + }; + switch (provider) { + case TwoFactorAuthProviderType.TOTP: + formControlConfig.issuerName = [{value: 'ThingsBoard', disabled: true}, Validators.required]; + break; + case TwoFactorAuthProviderType.SMS: + formControlConfig.smsVerificationMessageTemplate = [{value: 'Verification code: ${verificationCode}', disabled: true}, [ + Validators.required, + Validators.pattern(/\${verificationCode}/) + ]]; + formControlConfig.verificationCodeLifetime = [{value: 120, disabled: true}, [ + Validators.required, + Validators.min(1), + Validators.pattern(/^\d*$/) + ]]; + break; + case TwoFactorAuthProviderType.EMAIL: + formControlConfig.verificationCodeLifetime = [{value: 120, disabled: true}, [ + Validators.required, + Validators.min(1), + Validators.pattern(/^\d*$/) + ]]; + break; } - this.providersForm.removeAt(index); - this.providersForm.markAsTouched(); - this.providersForm.markAsDirty(); - } - - get providersForm(): FormArray { - return this.twoFaFormGroup.get('providers') as FormArray; + const newProviders = this.fb.group(formControlConfig); + newProviders.get('enable').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(value => { + if (value) { + newProviders.enable({emitEvent: false}); + } else { + newProviders.disable({emitEvent: false}); + newProviders.get('enable').enable({emitEvent: false}); + newProviders.get('providerType').enable({emitEvent: false}); + } + }); + this.providersForm.push(newProviders); } - trackByElement(i: number, item: any) { - return item; + private splitRateLimit(setting: string): [number, number] { + if (isNotEmptyStr(setting)) { + const [attemptNumber, time] = setting.split(':'); + return [parseInt(attemptNumber, 10), parseInt(time, 10)]; + } + return [0, 0]; } - selectedTypes(type: TwoFactorAuthProviderType, index: number): boolean { - const selectedProviderTypes: TwoFactorAuthProviderType[] = this.providersForm.value.map(providers => providers.providerType); - selectedProviderTypes.splice(index, 1); - return selectedProviderTypes.includes(type); + private joinRateLimit(processFormValue: TwoFactorAuthSettingsForm, property: string) { + if (processFormValue[`${property}Enable`]) { + processFormValue[property] = [processFormValue[`${property}Number`], processFormValue[`${property}Time`]].join(':'); + } + delete processFormValue[`${property}Enable`]; + delete processFormValue[`${property}Number`]; + delete processFormValue[`${property}Time`]; } - } diff --git a/ui-ngx/src/app/shared/models/two-factor-auth.models.ts b/ui-ngx/src/app/shared/models/two-factor-auth.models.ts index 731c1212e9..57e4e6b409 100644 --- a/ui-ngx/src/app/shared/models/two-factor-auth.models.ts +++ b/ui-ngx/src/app/shared/models/two-factor-auth.models.ts @@ -23,9 +23,22 @@ export interface TwoFactorAuthSettings { verificationCodeSendRateLimit: string; } +export interface TwoFactorAuthSettingsForm extends TwoFactorAuthSettings{ + providers: Array; + verificationCodeCheckRateLimitEnable: boolean; + verificationCodeCheckRateLimitNumber: number; + verificationCodeCheckRateLimitTime: number; + verificationCodeSendRateLimitEnable: boolean; + verificationCodeSendRateLimitNumber: number; + verificationCodeSendRateLimitTime: number; +} + export type TwoFactorAuthProviderConfig = Partial; +export type TwoFactorAuthProviderConfigForm = Partial & TwoFactorAuthProviderFormConfig; + export interface TotpTwoFactorAuthProviderConfig { providerType: TwoFactorAuthProviderType; issuerName: string; @@ -42,6 +55,10 @@ export interface EmailTwoFactorAuthProviderConfig { verificationCodeLifetime: number; } +export interface TwoFactorAuthProviderFormConfig { + enable: boolean; +} + export enum TwoFactorAuthProviderType{ TOTP = 'TOTP', SMS = 'SMS', @@ -69,5 +86,38 @@ export type TwoFactorAuthAccountConfig = TotpTwoFactorAuthAccountConfig | SmsTwo export interface AccountTwoFaSettings { - configs?: {TwoFactorAuthProviderType: TwoFactorAuthAccountConfig}; + configs: {TwoFactorAuthProviderType: TwoFactorAuthAccountConfig}; } + +export interface TwoFaProviderInfo { + type: TwoFactorAuthProviderType; + default: boolean; +} + +export interface TwoFactorAuthProviderData { + name: string; + description: string; +} + +export const twoFactorAuthProvidersData = new Map( + [ + [ + TwoFactorAuthProviderType.TOTP, { + name: 'Authentication app', + description: 'Use apps like Google Authenticator, Authy, or Duo on your phone to authenticate. It will generate a security code for logging in.' + } + ], + [ + TwoFactorAuthProviderType.SMS, { + name: 'SMS', + description: 'Use your phone to authenticate. We\'ll send you a security code via SMS message when you log in.' + } + ], + [ + TwoFactorAuthProviderType.EMAIL, { + name: 'Email', + description: 'Use a security code sent to your email address to authenticate.' + } + ], + ] +); diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 72bd583ef8..28ea3e1f7e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -123,6 +123,7 @@ "number-from-required": "Phone Number From is required.", "number-to": "Phone Number To", "number-to-required": "Phone Number To is required.", + "phone-number": "Phone Number", "phone-number-hint": "Phone Number in E.164 format, ex. +19995550123", "phone-number-hint-twilio": "Phone Number in E.164 format/Phone Number's SID/Messaging Service SID, ex. +19995550123/PNXXX/MGXXX", "phone-number-pattern": "Invalid phone number. Should be in E.164 format, ex. +19995550123.", @@ -314,30 +315,32 @@ }, "2fa": { "2fa": "Two-factor authentication", - "add-provider": "Add provider", - "available-providers": "Available providers:", + "available-providers": "Available providers", + "general-setting": "General setting", "issuer-name": "Issuer name", "issuer-name-required": "Issuer name is required.", "max-verification-failures-before-user-lockout": "Max verification failures before user lockout", "max-verification-failures-before-user-lockout-pattern": "Max verification failures must be a positive integer.", "max-verification-failures-before-user-lockout-required": "Max verification failures is required.", + "number-of-attempts": "Number of attempts", + "number-of-attempts-pattern": "Number of attempts must be a positive integer.", + "number-of-attempts-required": "Number of attempts is required.", "provider": "Provider", "total-allowed-time-for-verification": "Total allowed time for verification (sec)", "total-allowed-time-for-verification-pattern": "Total allowed time must be a positive integer.", "total-allowed-time-for-verification-required": "Total allowed time is required.", "use-system-two-factor-auth-settings": "Use system two factor auth settings", "verification-code-check-rate-limit": "Verification code check rate limit", - "verification-code-check-rate-limit-pattern": "Verification code check limit has invalid format", - "verification-code-check-rate-limit-required": "Verification code check rate limit is required.", "verification-code-lifetime": "Verification code lifetime (sec)", "verification-code-lifetime-pattern": "Verification code lifetime must be a positive integer.", "verification-code-lifetime-required": "Verification code lifetime is required.", "verification-code-send-rate-limit": "Verification code send rate limit", - "verification-code-send-rate-limit-pattern": "Verification code send limit has invalid format", - "verification-code-send-rate-limit-required": "Verification code send rate limit is required.", "verification-message-template": "Verification message template", "verification-message-template-pattern": "Verification message need to contains pattern: ${verificationCode}", - "verification-message-template-required": "Verification message template is required." + "verification-message-template-required": "Verification message template is required.", + "within-time": "Within time (sec)", + "within-time-pattern": "Time must be a positive integer.", + "within-time-required": "Time is required." } }, "alarm": { From 87a2f66a71f08fc2191d5665c633e40171e5b329 Mon Sep 17 00:00:00 2001 From: ShvaykaD Date: Mon, 16 May 2022 17:23:12 +0300 Subject: [PATCH 159/276] MQTT RPC tests speed up --- .../mqtt/AbstractMqttIntegrationTest.java | 17 +- .../transport/mqtt/MqttTestCallback.java | 39 +- .../server/transport/mqtt/MqttTestClient.java | 8 + ...AbstractMqttAttributesIntegrationTest.java | 3 - ...tractMqttServerSideRpcIntegrationTest.java | 396 ++++++++---------- ...cBackwardCompatibilityIntegrationTest.java | 23 +- ...ttServerSideRpcDefaultIntegrationTest.java | 16 +- .../MqttServerSideRpcJsonIntegrationTest.java | 23 +- ...MqttServerSideRpcProtoIntegrationTest.java | 17 +- 9 files changed, 261 insertions(+), 281 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java index deb0574704..9fbdff3fa5 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java @@ -52,6 +52,7 @@ import static org.junit.Assert.assertNotNull; @TestPropertySource(properties = { "transport.mqtt.enabled=true", + "js.evaluator=mock", }) @Slf4j public abstract class AbstractMqttIntegrationTest extends AbstractTransportIntegrationTest { @@ -83,22 +84,6 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg } } - protected MqttAsyncClient getMqttAsyncClient(String accessToken) throws MqttException { - String clientId = MqttAsyncClient.generateClientId(); - MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId, new MemoryPersistence()); - - MqttConnectOptions options = new MqttConnectOptions(); - options.setUserName(accessToken); - client.connect(options).waitForCompletion(); - return client; - } - - protected void publishMqttMsg(MqttAsyncClient client, byte[] payload, String topic) throws MqttException { - MqttMessage message = new MqttMessage(); - message.setPayload(payload); - client.publish(topic, message); - } - protected DeviceProfile createMqttDeviceProfile(MqttTestConfigProperties config) throws Exception { TransportPayloadType transportPayloadType = config.getTransportPayloadType(); if (transportPayloadType == null) { diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestCallback.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestCallback.java index c8c5b2560c..18815d1c38 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestCallback.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestCallback.java @@ -28,22 +28,27 @@ import java.util.concurrent.CountDownLatch; @Data public class MqttTestCallback implements MqttCallback { - private final CountDownLatch subscribeLatch; - private final CountDownLatch deliveryLatch; - private int qoS; - private byte[] payloadBytes; - private String awaitSubTopic; - private boolean pubAckReceived; + protected CountDownLatch subscribeLatch; + protected final CountDownLatch deliveryLatch; + protected int qoS; + protected byte[] payloadBytes; + protected String awaitSubTopic; + protected boolean pubAckReceived; - public MqttTestCallback(String awaitSubTopic) { + public MqttTestCallback() { this.subscribeLatch = new CountDownLatch(1); this.deliveryLatch = new CountDownLatch(1); - this.awaitSubTopic = awaitSubTopic; } - public MqttTestCallback() { + public MqttTestCallback(int subscribeCount) { + this.subscribeLatch = new CountDownLatch(subscribeCount); + this.deliveryLatch = new CountDownLatch(1); + } + + public MqttTestCallback(String awaitSubTopic) { this.subscribeLatch = new CountDownLatch(1); this.deliveryLatch = new CountDownLatch(1); + this.awaitSubTopic = awaitSubTopic; } @Override @@ -60,12 +65,16 @@ public class MqttTestCallback implements MqttCallback { payloadBytes = mqttMessage.getPayload(); subscribeLatch.countDown(); } else { - log.warn("messageArrived on topic: {}, awaitSubTopic: {}", requestTopic, awaitSubTopic); - if (awaitSubTopic.equals(requestTopic)) { - qoS = mqttMessage.getQos(); - payloadBytes = mqttMessage.getPayload(); - subscribeLatch.countDown(); - } + messageArrivedOnAwaitSubTopic(requestTopic, mqttMessage); + } + } + + protected void messageArrivedOnAwaitSubTopic(String requestTopic, MqttMessage mqttMessage) { + log.warn("messageArrived on topic: {}, awaitSubTopic: {}", requestTopic, awaitSubTopic); + if (awaitSubTopic.equals(requestTopic)) { + qoS = mqttMessage.getQos(); + payloadBytes = mqttMessage.getPayload(); + subscribeLatch.countDown(); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestClient.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestClient.java index 8afa41b160..96846f8678 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestClient.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestClient.java @@ -104,6 +104,14 @@ public class MqttTestClient { return client.subscribe(topic, qoS.value()); } + public void enableManualAcks() { + client.setManualAcks(true); + } + + public void messageArrivedComplete(MqttMessage mqttMessage) throws MqttException { + client.messageArrivedComplete(mqttMessage.getId(), mqttMessage.getQos()); + } + private MqttAsyncClient createClient(String clientId) throws MqttException { if (StringUtils.isEmpty(clientId)) { clientId = MqttAsyncClient.generateClientId(); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java index 261f904e2d..556af34371 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java @@ -60,9 +60,6 @@ import static org.thingsboard.server.common.data.device.profile.MqttTopics.GATEW import static org.thingsboard.server.common.data.query.EntityKeyType.CLIENT_ATTRIBUTE; import static org.thingsboard.server.common.data.query.EntityKeyType.SHARED_ATTRIBUTE; -@TestPropertySource(properties = { - "js.evaluator=mock", -}) @Slf4j public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqttIntegrationTest { diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java index 33256bbf2f..0abd0f1d6c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java @@ -26,33 +26,34 @@ import com.squareup.wire.schema.internal.parser.ProtoFileElement; import io.netty.handler.codec.mqtt.MqttQoS; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; -import org.eclipse.paho.client.mqttv3.MqttAsyncClient; -import org.eclipse.paho.client.mqttv3.MqttCallback; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; -import org.junit.Assert; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; -import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; import org.thingsboard.server.gen.transport.TransportApiProtos; import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; +import org.thingsboard.server.transport.mqtt.MqttTestCallback; +import org.thingsboard.server.transport.mqtt.MqttTestClient; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.BASE_DEVICE_API_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.BASE_DEVICE_API_TOPIC_V2; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.GATEWAY_CONNECT_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.GATEWAY_RPC_TOPIC; @Slf4j public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractMqttIntegrationTest { @@ -76,81 +77,93 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM protected static final Long asyncContextTimeoutToUseRpcPlugin = 10000L; protected void processOneWayRpcTest(String rpcSubTopic) throws Exception { - MqttAsyncClient client = getMqttAsyncClient(accessToken); - - CountDownLatch latch = new CountDownLatch(1); - TestOneWayMqttCallback callback = new TestOneWayMqttCallback(client, latch); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(accessToken); + MqttTestCallback callback = new MqttTestCallback(rpcSubTopic.replace("+", "0")); client.setCallback(callback); - - client.subscribe(rpcSubTopic, MqttQoS.AT_MOST_ONCE.value()); - - Thread.sleep(1000); + client.subscribeAndWait(rpcSubTopic, MqttQoS.AT_MOST_ONCE); String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}"; - String deviceId = savedDevice.getId().getId().toString(); - String result = doPostAsync("/api/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().isOk()); - Assert.assertTrue(StringUtils.isEmpty(result)); - latch.await(3, TimeUnit.SECONDS); + String result = doPostAsync("/api/rpc/oneway/" + savedDevice.getId(), setGpioRequest, String.class, status().isOk()); + assertTrue(StringUtils.isEmpty(result)); + callback.getSubscribeLatch().await(3, TimeUnit.SECONDS); + DeviceTransportType deviceTransportType = deviceProfile.getTransportType(); + if (deviceTransportType.equals(DeviceTransportType.MQTT)) { + DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); + assertTrue(transportConfiguration instanceof MqttDeviceProfileTransportConfiguration); + MqttDeviceProfileTransportConfiguration configuration = (MqttDeviceProfileTransportConfiguration) transportConfiguration; + TransportPayloadType transportPayloadType = configuration.getTransportPayloadTypeConfiguration().getTransportPayloadType(); + if (transportPayloadType.equals(TransportPayloadType.PROTOBUF)) { + // TODO: add correct validation of proto requests to device + assertTrue(callback.getPayloadBytes().length > 0); + } else { + assertEquals(JacksonUtil.toJsonNode(setGpioRequest), JacksonUtil.fromBytes(callback.getPayloadBytes())); + } + } else { + assertEquals(JacksonUtil.toJsonNode(setGpioRequest), JacksonUtil.fromBytes(callback.getPayloadBytes())); + } assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); + client.disconnect(); } protected void processJsonOneWayRpcTestGateway(String deviceName) throws Exception { - MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(gatewayAccessToken); String payload = "{\"device\":\"" + deviceName + "\"}"; byte[] payloadBytes = payload.getBytes(); validateOneWayRpcGatewayResponse(deviceName, client, payloadBytes); + client.disconnect(); } protected void processJsonTwoWayRpcTest(String rpcSubTopic) throws Exception { - MqttAsyncClient client = getMqttAsyncClient(accessToken); - client.subscribe(rpcSubTopic, 1); - - CountDownLatch latch = new CountDownLatch(1); - TestJsonMqttCallback callback = new TestJsonMqttCallback(client, latch); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(accessToken); + client.subscribeAndWait(rpcSubTopic, MqttQoS.AT_LEAST_ONCE); + MqttTestRpcJsonCallback callback = new MqttTestRpcJsonCallback(client, rpcSubTopic.replace("+", "0")); client.setCallback(callback); - - Thread.sleep(1000); - String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"26\",\"value\": 1}}"; - String deviceId = savedDevice.getId().getId().toString(); - - String result = doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); - String expected = "{\"value1\":\"A\",\"value2\":\"B\"}"; - latch.await(3, TimeUnit.SECONDS); - Assert.assertEquals(expected, result); + String actualRpcResponse = doPostAsync("/api/rpc/twoway/" + savedDevice.getId(), setGpioRequest, String.class, status().isOk()); + callback.getSubscribeLatch().await(3, TimeUnit.SECONDS); + assertEquals(JacksonUtil.toJsonNode(setGpioRequest), JacksonUtil.fromBytes(callback.getPayloadBytes())); + assertEquals("{\"value1\":\"A\",\"value2\":\"B\"}", actualRpcResponse); + client.disconnect(); } protected void processProtoTwoWayRpcTest(String rpcSubTopic) throws Exception { - MqttAsyncClient client = getMqttAsyncClient(accessToken); - client.subscribe(rpcSubTopic, 1); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(accessToken); + client.subscribeAndWait(rpcSubTopic, MqttQoS.AT_LEAST_ONCE); - CountDownLatch latch = new CountDownLatch(1); - TestProtoMqttCallback callback = new TestProtoMqttCallback(client, latch); + MqttTestRpcProtoCallback callback = new MqttTestRpcProtoCallback(client, rpcSubTopic.replace("+", "0")); client.setCallback(callback); - Thread.sleep(1000); - String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"26\",\"value\": 1}}"; String deviceId = savedDevice.getId().getId().toString(); - String result = doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); - String expected = "{\"payload\":\"{\\\"value1\\\":\\\"A\\\",\\\"value2\\\":\\\"B\\\"}\"}"; - latch.await(3, TimeUnit.SECONDS); - Assert.assertEquals(expected, result); + String actualRpcResponse = doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); + callback.getSubscribeLatch().await(3, TimeUnit.SECONDS); + // TODO: add correct validation of proto requests to device + assertTrue(callback.getPayloadBytes().length > 0); + assertEquals("{\"payload\":\"{\\\"value1\\\":\\\"A\\\",\\\"value2\\\":\\\"B\\\"}\"}", actualRpcResponse); + client.disconnect(); } protected void processProtoTwoWayRpcTestGateway(String deviceName) throws Exception { - MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(gatewayAccessToken); TransportApiProtos.ConnectMsg connectMsgProto = getConnectProto(deviceName); byte[] payloadBytes = connectMsgProto.toByteArray(); validateProtoTwoWayRpcGatewayResponse(deviceName, client, payloadBytes); + client.disconnect(); } protected void processProtoOneWayRpcTestGateway(String deviceName) throws Exception { - MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(gatewayAccessToken); TransportApiProtos.ConnectMsg connectMsgProto = getConnectProto(deviceName); byte[] payloadBytes = connectMsgProto.toByteArray(); validateOneWayRpcGatewayResponse(deviceName, client, payloadBytes); + client.disconnect(); } private TransportApiProtos.ConnectMsg getConnectProto(String deviceName) { @@ -175,29 +188,30 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM doPostAsync("/api/rpc/twoway/" + deviceId, JacksonUtil.toString(request), String.class, status().isOk()); } - MqttAsyncClient client = getMqttAsyncClient(accessToken); - client.setManualAcks(true); - CountDownLatch latch = new CountDownLatch(10); - TestSequenceMqttCallback callback = new TestSequenceMqttCallback(client, latch, result); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(accessToken); + client.enableManualAcks(); + MqttTestSequenceCallback callback = new MqttTestSequenceCallback(client, 10, result); client.setCallback(callback); - client.subscribe(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC, 1); + client.subscribeAndWait(DEVICE_RPC_REQUESTS_SUB_TOPIC, MqttQoS.AT_LEAST_ONCE); - latch.await(10, TimeUnit.SECONDS); - Assert.assertEquals(expected, result); + callback.getSubscribeLatch().await(10, TimeUnit.SECONDS); + assertEquals(expected, result); } protected void processJsonTwoWayRpcTestGateway(String deviceName) throws Exception { - MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(gatewayAccessToken); String payload = "{\"device\":\"" + deviceName + "\"}"; byte[] payloadBytes = payload.getBytes(); validateJsonTwoWayRpcGatewayResponse(deviceName, client, payloadBytes); + client.disconnect(); } - protected void validateOneWayRpcGatewayResponse(String deviceName, MqttAsyncClient client, byte[] payloadBytes) throws Exception { - publishMqttMsg(client, payloadBytes, MqttTopics.GATEWAY_CONNECT_TOPIC); - + protected void validateOneWayRpcGatewayResponse(String deviceName, MqttTestClient client, byte[] connectPayloadBytes) throws Exception { + client.publish(GATEWAY_CONNECT_TOPIC, connectPayloadBytes); Device savedDevice = doExecuteWithRetriesAndInterval( () -> getDeviceByName(deviceName), 20, @@ -205,24 +219,45 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM ); assertNotNull(savedDevice); - CountDownLatch latch = new CountDownLatch(1); - TestOneWayMqttCallback callback = new TestOneWayMqttCallback(client, latch); + MqttTestCallback callback = new MqttTestCallback(GATEWAY_RPC_TOPIC); client.setCallback(callback); - - client.subscribe(MqttTopics.GATEWAY_RPC_TOPIC, MqttQoS.AT_MOST_ONCE.value()); - - Thread.sleep(1000); - + client.subscribeAndWait(GATEWAY_RPC_TOPIC, MqttQoS.AT_MOST_ONCE); String setGpioRequest = "{\"method\": \"toggle_gpio\", \"params\": {\"pin\":1}}"; String deviceId = savedDevice.getId().getId().toString(); String result = doPostAsync("/api/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().isOk()); - Assert.assertTrue(StringUtils.isEmpty(result)); - latch.await(3, TimeUnit.SECONDS); + assertTrue(StringUtils.isEmpty(result)); + callback.getSubscribeLatch().await(3, TimeUnit.SECONDS); + DeviceTransportType deviceTransportType = deviceProfile.getTransportType(); + if (deviceTransportType.equals(DeviceTransportType.MQTT)) { + DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); + assertTrue(transportConfiguration instanceof MqttDeviceProfileTransportConfiguration); + MqttDeviceProfileTransportConfiguration configuration = (MqttDeviceProfileTransportConfiguration) transportConfiguration; + TransportPayloadType transportPayloadType = configuration.getTransportPayloadTypeConfiguration().getTransportPayloadType(); + if (transportPayloadType.equals(TransportPayloadType.PROTOBUF)) { + // TODO: add correct validation of proto requests to device + assertTrue(callback.getPayloadBytes().length > 0); + } else { + JsonNode expectedJsonRequestData = getExpectedGatewayJsonRequestData(deviceName, setGpioRequest); + assertEquals(expectedJsonRequestData, JacksonUtil.fromBytes(callback.getPayloadBytes())); + } + } else { + JsonNode expectedJsonRequestData = getExpectedGatewayJsonRequestData(deviceName, setGpioRequest); + assertEquals(expectedJsonRequestData, JacksonUtil.fromBytes(callback.getPayloadBytes())); + } assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); } - protected void validateJsonTwoWayRpcGatewayResponse(String deviceName, MqttAsyncClient client, byte[] payloadBytes) throws Exception { - publishMqttMsg(client, payloadBytes, MqttTopics.GATEWAY_CONNECT_TOPIC); + private JsonNode getExpectedGatewayJsonRequestData(String deviceName, String requestStr) { + ObjectNode deviceData = (ObjectNode) JacksonUtil.toJsonNode(requestStr); + deviceData.put("id", 0); + ObjectNode expectedRequest = JacksonUtil.newObjectNode(); + expectedRequest.put("device", deviceName); + expectedRequest.set("data", deviceData); + return expectedRequest; + } + + protected void validateJsonTwoWayRpcGatewayResponse(String deviceName, MqttTestClient client, byte[] connectPayloadBytes) throws Exception { + client.publish(GATEWAY_CONNECT_TOPIC, connectPayloadBytes); Device savedDevice = doExecuteWithRetriesAndInterval( () -> getDeviceByName(deviceName), @@ -231,25 +266,21 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM ); assertNotNull(savedDevice); - CountDownLatch latch = new CountDownLatch(1); - TestJsonMqttCallback callback = new TestJsonMqttCallback(client, latch); + MqttTestRpcJsonCallback callback = new MqttTestRpcJsonCallback(client, GATEWAY_RPC_TOPIC); client.setCallback(callback); - - client.subscribe(MqttTopics.GATEWAY_RPC_TOPIC, MqttQoS.AT_MOST_ONCE.value()); - - Thread.sleep(1000); + client.subscribeAndWait(GATEWAY_RPC_TOPIC, MqttQoS.AT_MOST_ONCE); String setGpioRequest = "{\"method\": \"toggle_gpio\", \"params\": {\"pin\":1}}"; String deviceId = savedDevice.getId().getId().toString(); - String result = doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); - latch.await(3, TimeUnit.SECONDS); - String expected = "{\"success\":true}"; - assertEquals(expected, result); + String actualRpcResponse = doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); + callback.getSubscribeLatch().await(3, TimeUnit.SECONDS); + log.warn("request payload: {}", JacksonUtil.fromBytes(callback.getPayloadBytes())); + assertEquals("{\"success\":true}", actualRpcResponse); assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); } - protected void validateProtoTwoWayRpcGatewayResponse(String deviceName, MqttAsyncClient client, byte[] payloadBytes) throws Exception { - publishMqttMsg(client, payloadBytes, MqttTopics.GATEWAY_CONNECT_TOPIC); + protected void validateProtoTwoWayRpcGatewayResponse(String deviceName, MqttTestClient client, byte[] connectPayloadBytes) throws Exception { + client.publish(GATEWAY_CONNECT_TOPIC, connectPayloadBytes); Device savedDevice = doExecuteWithRetriesAndInterval( () -> getDeviceByName(deviceName), @@ -258,20 +289,15 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM ); assertNotNull(savedDevice); - CountDownLatch latch = new CountDownLatch(1); - TestProtoMqttCallback callback = new TestProtoMqttCallback(client, latch); + MqttTestRpcProtoCallback callback = new MqttTestRpcProtoCallback(client, GATEWAY_RPC_TOPIC); client.setCallback(callback); - - client.subscribe(MqttTopics.GATEWAY_RPC_TOPIC, MqttQoS.AT_MOST_ONCE.value()); - - Thread.sleep(1000); + client.subscribeAndWait(GATEWAY_RPC_TOPIC, MqttQoS.AT_MOST_ONCE); String setGpioRequest = "{\"method\": \"toggle_gpio\", \"params\": {\"pin\":1}}"; String deviceId = savedDevice.getId().getId().toString(); - String result = doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); - latch.await(3, TimeUnit.SECONDS); - String expected = "{\"success\":true}"; - assertEquals(expected, result); + String actualRpcResponse = doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); + callback.getSubscribeLatch().await(3, TimeUnit.SECONDS); + assertEquals("{\"success\":true}", actualRpcResponse); assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); } @@ -279,132 +305,82 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM return doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class); } - protected MqttMessage processJsonMessageArrived(String requestTopic, MqttMessage mqttMessage) throws MqttException, InvalidProtocolBufferException { - MqttMessage message = new MqttMessage(); - if (requestTopic.startsWith(MqttTopics.BASE_DEVICE_API_TOPIC) || requestTopic.startsWith(MqttTopics.BASE_DEVICE_API_TOPIC_V2)) { - message.setPayload(DEVICE_RESPONSE.getBytes(StandardCharset.UTF_8)); + protected byte[] processJsonMessageArrived(String requestTopic, MqttMessage mqttMessage) { + if (requestTopic.startsWith(BASE_DEVICE_API_TOPIC) || requestTopic.startsWith(BASE_DEVICE_API_TOPIC_V2)) { + return DEVICE_RESPONSE.getBytes(StandardCharset.UTF_8); } else { JsonNode requestMsgNode = JacksonUtil.toJsonNode(new String(mqttMessage.getPayload(), StandardCharset.UTF_8)); String deviceName = requestMsgNode.get("device").asText(); int requestId = requestMsgNode.get("data").get("id").asInt(); - message.setPayload(("{\"device\": \"" + deviceName + "\", \"id\": " + requestId + ", \"data\": {\"success\": true}}").getBytes(StandardCharset.UTF_8)); - } - return message; - } - - protected class TestOneWayMqttCallback implements MqttCallback { - - private final MqttAsyncClient client; - private final CountDownLatch latch; - private Integer qoS; - - TestOneWayMqttCallback(MqttAsyncClient client, CountDownLatch latch) { - this.client = client; - this.latch = latch; - } - - int getQoS() { - return qoS; - } - - @Override - public void connectionLost(Throwable throwable) { - } - - @Override - public void messageArrived(String requestTopic, MqttMessage mqttMessage) throws Exception { - log.info("Message Arrived: " + Arrays.toString(mqttMessage.getPayload())); - qoS = mqttMessage.getQos(); - latch.countDown(); - } - - @Override - public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { - + String response = "{\"device\": \"" + deviceName + "\", \"id\": " + requestId + ", \"data\": {\"success\": true}}"; + return response.getBytes(StandardCharset.UTF_8); } } - protected class TestJsonMqttCallback implements MqttCallback { + protected class MqttTestRpcJsonCallback extends MqttTestCallback { - private final MqttAsyncClient client; - private final CountDownLatch latch; - private Integer qoS; + private final MqttTestClient client; - TestJsonMqttCallback(MqttAsyncClient client, CountDownLatch latch) { + public MqttTestRpcJsonCallback(MqttTestClient client, String awaitSubTopic) { + super(awaitSubTopic); this.client = client; - this.latch = latch; - } - - int getQoS() { - return qoS; - } - - @Override - public void connectionLost(Throwable throwable) { } @Override - public void messageArrived(String requestTopic, MqttMessage mqttMessage) throws Exception { - log.info("Message Arrived: " + Arrays.toString(mqttMessage.getPayload())); - String responseTopic; - if (requestTopic.startsWith(MqttTopics.BASE_DEVICE_API_TOPIC_V2)) { - responseTopic = requestTopic.replace("req", "res"); - } else { - responseTopic = requestTopic.replace("request", "response"); + protected void messageArrivedOnAwaitSubTopic(String requestTopic, MqttMessage mqttMessage) { + log.warn("messageArrived on topic: {}, awaitSubTopic: {}", requestTopic, awaitSubTopic); + if (awaitSubTopic.equals(requestTopic)) { + qoS = mqttMessage.getQos(); + payloadBytes = mqttMessage.getPayload(); + String responseTopic; + if (requestTopic.startsWith(BASE_DEVICE_API_TOPIC_V2)) { + responseTopic = requestTopic.replace("req", "res"); + } else { + responseTopic = requestTopic.replace("request", "response"); + } + try { + client.publish(responseTopic, processJsonMessageArrived(requestTopic, mqttMessage)); + } catch (MqttException e) { + log.warn("Failed to publish response on topic: {} due to: ", responseTopic, e); + } + subscribeLatch.countDown(); } - qoS = mqttMessage.getQos(); - client.publish(responseTopic, processJsonMessageArrived(requestTopic, mqttMessage)); - latch.countDown(); - } - - @Override - public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { - } } - protected class TestProtoMqttCallback implements MqttCallback { + protected class MqttTestRpcProtoCallback extends MqttTestCallback { - private final MqttAsyncClient client; - private final CountDownLatch latch; - private Integer qoS; + private final MqttTestClient client; - TestProtoMqttCallback(MqttAsyncClient client, CountDownLatch latch) { + public MqttTestRpcProtoCallback(MqttTestClient client, String awaitSubTopic) { + super(awaitSubTopic); this.client = client; - this.latch = latch; - } - - int getQoS() { - return qoS; } @Override - public void connectionLost(Throwable throwable) { - } - - @Override - public void messageArrived(String requestTopic, MqttMessage mqttMessage) throws Exception { - log.info("Message Arrived: " + Arrays.toString(mqttMessage.getPayload())); - String responseTopic; - if (requestTopic.startsWith(MqttTopics.BASE_DEVICE_API_TOPIC_V2)) { - responseTopic = requestTopic.replace("req", "res"); - } else { - responseTopic = requestTopic.replace("request", "response"); + protected void messageArrivedOnAwaitSubTopic(String requestTopic, MqttMessage mqttMessage) { + log.warn("messageArrived on topic: {}, awaitSubTopic: {}", requestTopic, awaitSubTopic); + if (awaitSubTopic.equals(requestTopic)) { + qoS = mqttMessage.getQos(); + payloadBytes = mqttMessage.getPayload(); + String responseTopic; + if (requestTopic.startsWith(BASE_DEVICE_API_TOPIC_V2)) { + responseTopic = requestTopic.replace("req", "res"); + } else { + responseTopic = requestTopic.replace("request", "response"); + } + try { + client.publish(responseTopic, processProtoMessageArrived(requestTopic, mqttMessage)); + } catch (Exception e) { + log.warn("Failed to publish response on topic: {} due to: ", responseTopic, e); + } + subscribeLatch.countDown(); } - qoS = mqttMessage.getQos(); - client.publish(responseTopic, processProtoMessageArrived(requestTopic, mqttMessage)); - latch.countDown(); - } - - @Override - public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { - } } - protected MqttMessage processProtoMessageArrived(String requestTopic, MqttMessage mqttMessage) throws MqttException, InvalidProtocolBufferException { - MqttMessage message = new MqttMessage(); - if (requestTopic.startsWith(MqttTopics.BASE_DEVICE_API_TOPIC) || requestTopic.startsWith(MqttTopics.BASE_DEVICE_API_TOPIC_V2)) { + protected byte[] processProtoMessageArrived(String requestTopic, MqttMessage mqttMessage) throws MqttException, InvalidProtocolBufferException { + if (requestTopic.startsWith(BASE_DEVICE_API_TOPIC) || requestTopic.startsWith(BASE_DEVICE_API_TOPIC_V2)) { ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = getProtoTransportPayloadConfiguration(); ProtoFileElement rpcRequestProtoSchemaFile = protoTransportPayloadConfiguration.getTransportProtoSchema(RPC_REQUEST_PROTO_SCHEMA); DynamicSchema rpcRequestProtoSchema = protoTransportPayloadConfiguration.getDynamicSchema(rpcRequestProtoSchemaFile, ProtoTransportPayloadConfiguration.RPC_REQUEST_PROTO_SCHEMA); @@ -428,9 +404,9 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM DynamicMessage rpcResponseMsg = rpcResponseBuilder .setField(rpcResponseMsgDescriptor.findFieldByName("payload"), DEVICE_RESPONSE) .build(); - message.setPayload(rpcResponseMsg.toByteArray()); + return rpcResponseMsg.toByteArray(); } catch (InvalidProtocolBufferException e) { - log.warn("Command Response Ack Error, Invalid response received: ", e); + throw new RuntimeException("Command Response Ack Error, Invalid response received: ", e); } } else { TransportApiProtos.GatewayDeviceRpcRequestMsg msg = TransportApiProtos.GatewayDeviceRpcRequestMsg.parseFrom(mqttMessage.getPayload()); @@ -441,9 +417,8 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM .setId(requestId) .setData("{\"success\": true}") .build(); - message.setPayload(gatewayRpcResponseMsg.toByteArray()); + return gatewayRpcResponseMsg.toByteArray(); } - return message; } private ProtoTransportPayloadConfiguration getProtoTransportPayloadConfiguration() { @@ -455,37 +430,30 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM return (ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration; } - protected class TestSequenceMqttCallback implements MqttCallback { + protected class MqttTestSequenceCallback extends MqttTestCallback { - private final MqttAsyncClient client; - private final CountDownLatch latch; + private final MqttTestClient client; private final List expected; - TestSequenceMqttCallback(MqttAsyncClient client, CountDownLatch latch, List expected) { + MqttTestSequenceCallback(MqttTestClient client, int subscribeCount, List expected) { + super(subscribeCount); this.client = client; - this.latch = latch; this.expected = expected; } @Override - public void connectionLost(Throwable throwable) { - } - - @Override - public void messageArrived(String requestTopic, MqttMessage mqttMessage) throws Exception { - log.info("Message Arrived: " + Arrays.toString(mqttMessage.getPayload())); + public void messageArrived(String requestTopic, MqttMessage mqttMessage) { + log.warn("messageArrived on topic: {}, awaitSubTopic: {}", requestTopic, awaitSubTopic); expected.add(new String(mqttMessage.getPayload())); String responseTopic = requestTopic.replace("request", "response"); - var qoS = mqttMessage.getQos(); - - client.messageArrivedComplete(mqttMessage.getId(), qoS); - client.publish(responseTopic, processJsonMessageArrived(requestTopic, mqttMessage)); - latch.countDown(); - } - - @Override - public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { - + qoS = mqttMessage.getQos(); + try { + client.messageArrivedComplete(mqttMessage); + client.publish(responseTopic, processJsonMessageArrived(requestTopic, mqttMessage)); + } catch (MqttException e) { + log.warn("Failed to publish response on topic: {} due to: ", responseTopic, e); + } + subscribeLatch.countDown(); } } } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/MqttServerSideRpcBackwardCompatibilityIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/MqttServerSideRpcBackwardCompatibilityIntegrationTest.java index 465063004e..5fb123666f 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/MqttServerSideRpcBackwardCompatibilityIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/MqttServerSideRpcBackwardCompatibilityIntegrationTest.java @@ -16,13 +16,16 @@ package org.thingsboard.server.transport.mqtt.rpc; import lombok.extern.slf4j.Slf4j; -import org.junit.After; import org.junit.Test; import org.thingsboard.server.common.data.TransportPayloadType; -import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_PROTO_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC; + @Slf4j @DaoSqlTest public class MqttServerSideRpcBackwardCompatibilityIntegrationTest extends AbstractMqttServerSideRpcIntegrationTest { @@ -37,7 +40,7 @@ public class MqttServerSideRpcBackwardCompatibilityIntegrationTest extends Abstr .useJsonPayloadFormatForDefaultDownlinkTopics(true) .build(); processBeforeTest(configProperties); - processOneWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC); + processOneWayRpcTest(DEVICE_RPC_REQUESTS_SUB_TOPIC); } @Test @@ -50,7 +53,7 @@ public class MqttServerSideRpcBackwardCompatibilityIntegrationTest extends Abstr .useJsonPayloadFormatForDefaultDownlinkTopics(true) .build(); super.processBeforeTest(configProperties); - processOneWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); + processOneWayRpcTest(DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); } @Test @@ -63,7 +66,7 @@ public class MqttServerSideRpcBackwardCompatibilityIntegrationTest extends Abstr .useJsonPayloadFormatForDefaultDownlinkTopics(true) .build(); super.processBeforeTest(configProperties); - processOneWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_PROTO_TOPIC); + processOneWayRpcTest(DEVICE_RPC_REQUESTS_SUB_SHORT_PROTO_TOPIC); } @Test @@ -75,7 +78,7 @@ public class MqttServerSideRpcBackwardCompatibilityIntegrationTest extends Abstr .enableCompatibilityWithJsonPayloadFormat(true) .build(); super.processBeforeTest(configProperties); - processProtoTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC); + processProtoTwoWayRpcTest(DEVICE_RPC_REQUESTS_SUB_TOPIC); } @Test @@ -88,7 +91,7 @@ public class MqttServerSideRpcBackwardCompatibilityIntegrationTest extends Abstr .useJsonPayloadFormatForDefaultDownlinkTopics(true) .build(); super.processBeforeTest(configProperties); - processJsonTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC); + processJsonTwoWayRpcTest(DEVICE_RPC_REQUESTS_SUB_TOPIC); } @Test @@ -99,7 +102,7 @@ public class MqttServerSideRpcBackwardCompatibilityIntegrationTest extends Abstr .rpcRequestProtoSchema(RPC_REQUEST_PROTO_SCHEMA) .build(); super.processBeforeTest(configProperties); - processProtoTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); + processProtoTwoWayRpcTest(DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); } @Test @@ -112,7 +115,7 @@ public class MqttServerSideRpcBackwardCompatibilityIntegrationTest extends Abstr .useJsonPayloadFormatForDefaultDownlinkTopics(true) .build(); super.processBeforeTest(configProperties); - processProtoTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_PROTO_TOPIC); + processProtoTwoWayRpcTest(DEVICE_RPC_REQUESTS_SUB_SHORT_PROTO_TOPIC); } @Test @@ -125,7 +128,7 @@ public class MqttServerSideRpcBackwardCompatibilityIntegrationTest extends Abstr .useJsonPayloadFormatForDefaultDownlinkTopics(true) .build(); super.processBeforeTest(configProperties); - processJsonTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC); + processJsonTwoWayRpcTest(DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC); } @Test diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/MqttServerSideRpcDefaultIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/MqttServerSideRpcDefaultIntegrationTest.java index 542843eb2c..95e3b7fde3 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/MqttServerSideRpcDefaultIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/MqttServerSideRpcDefaultIntegrationTest.java @@ -20,12 +20,14 @@ import lombok.extern.slf4j.Slf4j; import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.service.security.AccessValidator; import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC; @Slf4j @DaoSqlTest @@ -80,32 +82,32 @@ public class MqttServerSideRpcDefaultIntegrationTest extends AbstractMqttServerS @Test public void testServerMqttOneWayRpc() throws Exception { - processOneWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC); + processOneWayRpcTest(DEVICE_RPC_REQUESTS_SUB_TOPIC); } @Test public void testServerMqttOneWayRpcOnShortTopic() throws Exception { - processOneWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); + processOneWayRpcTest(DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); } @Test public void testServerMqttOneWayRpcOnShortJsonTopic() throws Exception { - processOneWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC); + processOneWayRpcTest(DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC); } @Test public void testServerMqttTwoWayRpc() throws Exception { - processJsonTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC); + processJsonTwoWayRpcTest(DEVICE_RPC_REQUESTS_SUB_TOPIC); } @Test public void testServerMqttTwoWayRpcOnShortTopic() throws Exception { - processJsonTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); + processJsonTwoWayRpcTest(DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); } @Test public void testServerMqttTwoWayRpcOnShortJsonTopic() throws Exception { - processJsonTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC); + processJsonTwoWayRpcTest(DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC); } @Test diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/MqttServerSideRpcJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/MqttServerSideRpcJsonIntegrationTest.java index 2fc95e5f6e..b3c3a994d1 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/MqttServerSideRpcJsonIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/MqttServerSideRpcJsonIntegrationTest.java @@ -16,14 +16,17 @@ package org.thingsboard.server.transport.mqtt.rpc; import lombok.extern.slf4j.Slf4j; -import org.eclipse.paho.client.mqttv3.MqttAsyncClient; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.TransportPayloadType; -import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.transport.mqtt.MqttTestClient; import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC; + @Slf4j @DaoSqlTest public class MqttServerSideRpcJsonIntegrationTest extends AbstractMqttServerSideRpcIntegrationTest { @@ -40,32 +43,32 @@ public class MqttServerSideRpcJsonIntegrationTest extends AbstractMqttServerSide @Test public void testServerMqttOneWayRpc() throws Exception { - processOneWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC); + processOneWayRpcTest(DEVICE_RPC_REQUESTS_SUB_TOPIC); } @Test public void testServerMqttOneWayRpcOnShortTopic() throws Exception { - processOneWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); + processOneWayRpcTest(DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); } @Test public void testServerMqttOneWayRpcOnShortJsonTopic() throws Exception { - processOneWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC); + processOneWayRpcTest(DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC); } @Test public void testServerMqttTwoWayRpc() throws Exception { - processJsonTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC); + processJsonTwoWayRpcTest(DEVICE_RPC_REQUESTS_SUB_TOPIC); } @Test public void testServerMqttTwoWayRpcOnShortTopic() throws Exception { - processJsonTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); + processJsonTwoWayRpcTest(DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); } @Test public void testServerMqttTwoWayRpcOnShortJsonTopic() throws Exception { - processJsonTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC); + processJsonTwoWayRpcTest(DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC); } @Test @@ -79,10 +82,12 @@ public class MqttServerSideRpcJsonIntegrationTest extends AbstractMqttServerSide } protected void processJsonOneWayRpcTestGateway(String deviceName) throws Exception { - MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(gatewayAccessToken); String payload = "{\"device\": \"" + deviceName + "\", \"type\": \"" + TransportPayloadType.JSON.name() + "\"}"; byte[] payloadBytes = payload.getBytes(); validateOneWayRpcGatewayResponse(deviceName, client, payloadBytes); + client.disconnect(); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/MqttServerSideRpcProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/MqttServerSideRpcProtoIntegrationTest.java index dcd06f5a52..e888f49ff4 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/MqttServerSideRpcProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/MqttServerSideRpcProtoIntegrationTest.java @@ -19,10 +19,13 @@ import lombok.extern.slf4j.Slf4j; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.TransportPayloadType; -import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_PROTO_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC; +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC; + @Slf4j @DaoSqlTest public class MqttServerSideRpcProtoIntegrationTest extends AbstractMqttServerSideRpcIntegrationTest { @@ -40,32 +43,32 @@ public class MqttServerSideRpcProtoIntegrationTest extends AbstractMqttServerSid @Test public void testServerMqttOneWayRpc() throws Exception { - processOneWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC); + processOneWayRpcTest(DEVICE_RPC_REQUESTS_SUB_TOPIC); } @Test public void testServerMqttOneWayRpcOnShortTopic() throws Exception { - processOneWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); + processOneWayRpcTest(DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); } @Test public void testServerMqttOneWayRpcOnShortProtoTopic() throws Exception { - processOneWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_PROTO_TOPIC); + processOneWayRpcTest(DEVICE_RPC_REQUESTS_SUB_SHORT_PROTO_TOPIC); } @Test public void testServerMqttTwoWayRpc() throws Exception { - processProtoTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC); + processProtoTwoWayRpcTest(DEVICE_RPC_REQUESTS_SUB_TOPIC); } @Test public void testServerMqttTwoWayRpcOnShortTopic() throws Exception { - processProtoTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); + processProtoTwoWayRpcTest(DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); } @Test public void testServerMqttTwoWayRpcOnShortProtoTopic() throws Exception { - processProtoTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_PROTO_TOPIC); + processProtoTwoWayRpcTest(DEVICE_RPC_REQUESTS_SUB_SHORT_PROTO_TOPIC); } @Test From 1e973825b74c1eecb38366b054aa0a1729128ca7 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Mon, 16 May 2022 17:54:48 +0300 Subject: [PATCH 160/276] refactoring: - dashboard commits --- .../server/controller/AssetController.java | 4 ++-- .../server/controller/CustomerController.java | 2 +- .../server/controller/DashboardController.java | 2 +- .../service/asset/AssetBulkImportService.java | 2 +- .../DefaultTbNotificationEntityService.java | 10 +++------- .../service/entitiy/SimpleTbEntityService.java | 8 +++----- .../entitiy/TbNotificationEntityService.java | 2 +- .../entitiy/alarm/DefaultTbAlarmService.java | 7 +++---- .../entitiy/asset/DefaultTbAssetService.java | 11 ++++------- .../service/entitiy/asset/TbAssetService.java | 7 ++++--- .../entitiy/customer/DefaultTbCustomerService.java | 12 +++++++++--- .../entitiy/customer/TbCustomerService.java | 3 +-- .../dashboard/DefaultTbDashboardService.java | 14 +++++--------- .../entitiy/dashboard/TbDashboardService.java | 2 +- 14 files changed, 39 insertions(+), 47 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java index 4718feb774..9edd328b68 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java @@ -149,7 +149,7 @@ public class AssetController extends BaseController { } asset.setTenantId(getCurrentUser().getTenantId()); checkEntity(asset.getId(), asset, Resource.ASSET); - return tbAssetService.save(asset, getCurrentUser()); + return tbAssetService.saveAsset(asset, getCurrentUser()); } @ApiOperation(value = "Delete asset (deleteAsset)", @@ -162,7 +162,7 @@ public class AssetController extends BaseController { try { AssetId assetId = new AssetId(toUUID(strAssetId)); Asset asset = checkAssetId(assetId, Operation.DELETE); - tbAssetService.delete(asset, getCurrentUser()).get(); + tbAssetService.deleteAsset(asset, getCurrentUser()).get(); } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java index 8ce95eaaa5..a824690653 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java @@ -161,7 +161,7 @@ public class CustomerController extends BaseController { CustomerId customerId = new CustomerId(toUUID(strCustomerId)); Customer customer = checkCustomerId(customerId, Operation.DELETE); try { - tbCustomerService.delete(customer, customerId, getCurrentUser()); + tbCustomerService.delete(customer, getCurrentUser()); } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index 3f7bd618a8..7ff16baae0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -194,7 +194,7 @@ public class DashboardController extends BaseController { checkParameter(DASHBOARD_ID, strDashboardId); DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); Dashboard dashboard = checkDashboardId(dashboardId, Operation.DELETE); - tbDashboardService.delete(dashboard, dashboardId, getCurrentUser()); + tbDashboardService.delete(dashboard, getCurrentUser()); } @ApiOperation(value = "Assign the Dashboard (assignDashboardToCustomer)", diff --git a/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java index e9999d2dc1..834eeb555d 100644 --- a/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java @@ -66,7 +66,7 @@ public class AssetBulkImportService extends AbstractBulkImportService { @Override @SneakyThrows protected Asset saveEntity(SecurityUser user, Asset entity, Map fields) { - return tbAssetService.save(entity, user); + return tbAssetService.saveAsset(entity, user); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java index 6ee9531c46..2cd1ae1acc 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java @@ -22,7 +22,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.rule.engine.api.msg.DeviceCredentialsUpdateNotificationMsg; import org.thingsboard.server.cluster.TbClusterService; -import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.HasName; @@ -68,17 +67,14 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS } @Override - public void notifyDeleteEntity(TenantId tenantId, I entityId, E entity, + public void notifyDeleteEntity(TenantId tenantId, I entityId, E entity, EntityId originatorId, CustomerId customerId, ActionType actionType, List relatedEdgeIds, SecurityUser user, String body, Object... additionalInfo) { - EntityId entityIdForLogEntityAction = entity instanceof Alarm ? ((Alarm)entity).getOriginator() : entityId; + EntityId entityIdForLogEntityAction = originatorId != null ? originatorId : entityId; logEntityAction(tenantId, entityIdForLogEntityAction, entity, customerId, actionType, user, additionalInfo); sendDeleteNotificationMsg(tenantId, entityId, entity, relatedEdgeIds, body); - if (entity instanceof Customer) { - tbClusterService.broadcastEntityStateChangeEvent(tenantId, customerId, ComponentLifecycleEvent.DELETED); - } } @Override @@ -131,7 +127,7 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS gatewayNotificationsService.onDeviceDeleted(device); tbClusterService.onDeviceDeleted(device, null); - notifyDeleteEntity(tenantId, deviceId, device, customerId, ActionType.DELETED, relatedEdgeIds, user,null, additionalInfo); + notifyDeleteEntity(tenantId, deviceId, device, customerId, null, ActionType.DELETED, relatedEdgeIds, user,null, additionalInfo); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/SimpleTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/SimpleTbEntityService.java index 5d7f89f886..ce22e8d787 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/SimpleTbEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/SimpleTbEntityService.java @@ -15,15 +15,13 @@ */ package org.thingsboard.server.service.entitiy; -import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.security.model.SecurityUser; -public interface SimpleTbEntityService { +public interface SimpleTbEntityService { - E save(E entity, SecurityUser user) throws ThingsboardException; + T save(T entity, SecurityUser user) throws ThingsboardException; - void delete (E entity, I entityId, SecurityUser user) throws ThingsboardException; + void delete (T entity, SecurityUser user) throws ThingsboardException; } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java index 70cac320d7..1e0bcf1124 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java @@ -43,7 +43,7 @@ public interface TbNotificationEntityService { CustomerId customerId, ActionType actionType, SecurityUser user, Object... additionalInfo); - void notifyDeleteEntity(TenantId tenantId, I entityId, E entity, CustomerId customerId, + void notifyDeleteEntity(TenantId tenantId, I entityId, E entity, EntityId originatorId, CustomerId customerId, ActionType actionType, List relatedEdgeIds, SecurityUser user, String body, Object... additionalInfo); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java index 870388483e..809b8b4e8c 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java @@ -15,9 +15,9 @@ */ package org.thingsboard.server.service.entitiy.alarm; -import com.fasterxml.jackson.databind.ObjectMapper; import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmStatus; @@ -35,7 +35,6 @@ import java.util.List; @TbCoreComponent @AllArgsConstructor public class DefaultTbAlarmService extends AbstractTbEntityService implements TbAlarmService { - private static final ObjectMapper json = new ObjectMapper(); @Override public Alarm save(Alarm alarm, SecurityUser user) throws ThingsboardException { @@ -81,8 +80,8 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb public Boolean deleteAlarm(Alarm alarm, SecurityUser user) throws ThingsboardException { try { List relatedEdgeIds = findRelatedEdgeIds(user.getTenantId(), alarm.getOriginator()); - notificationEntityService.notifyDeleteEntity(user.getTenantId(), alarm.getId(), alarm, user.getCustomerId(), - ActionType.DELETED, relatedEdgeIds, user, json.writeValueAsString(alarm)); + notificationEntityService.notifyDeleteEntity(user.getTenantId(), alarm.getId(), alarm, alarm.getOriginator(), user.getCustomerId(), + ActionType.DELETED, relatedEdgeIds, user, JacksonUtil.OBJECT_MAPPER.writeValueAsString(alarm)); return alarmService.deleteAlarm(user.getTenantId(), alarm.getId()).isSuccessful(); } catch (Exception e) { throw handleException(e); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java index 1d75e68616..e44e405273 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java @@ -39,8 +39,9 @@ import java.util.List; @TbCoreComponent @AllArgsConstructor public class DefaultTbAssetService extends AbstractTbEntityService implements TbAssetService { + @Override - public Asset save(Asset asset, SecurityUser user) throws ThingsboardException { + public Asset saveAsset(Asset asset, SecurityUser user) throws ThingsboardException { ActionType actionType = asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED; TenantId tenantId = asset.getTenantId(); try { @@ -54,13 +55,13 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb } @Override - public ListenableFuture delete(Asset asset, SecurityUser user) throws ThingsboardException { + public ListenableFuture deleteAsset(Asset asset, SecurityUser user) throws ThingsboardException { TenantId tenantId = asset.getTenantId(); AssetId assetId = asset.getId(); try { List relatedEdgeIds = findRelatedEdgeIds(tenantId, assetId); assetService.deleteAsset(tenantId, assetId); - notificationEntityService.notifyDeleteEntity(tenantId, assetId, asset, asset.getCustomerId(), ActionType.DELETED, + notificationEntityService.notifyDeleteEntity(tenantId, assetId, asset, null, asset.getCustomerId(), ActionType.DELETED, relatedEdgeIds, user, null, asset.toString()); return removeAlarmsByEntityId(tenantId, assetId); @@ -71,10 +72,6 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb } } - - @Override - public void delete(Asset entity, AssetId entityId, SecurityUser user) throws ThingsboardException {} - @Override public Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, Customer customer, SecurityUser user) throws ThingsboardException { ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java index e753093ba2..ccbd9e715e 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java @@ -22,12 +22,13 @@ import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.service.entitiy.SimpleTbEntityService; import org.thingsboard.server.service.security.model.SecurityUser; -public interface TbAssetService extends SimpleTbEntityService { +public interface TbAssetService { - ListenableFuture delete(Asset asset, SecurityUser user) throws ThingsboardException; + Asset saveAsset(Asset asset, SecurityUser user) throws ThingsboardException; + + ListenableFuture deleteAsset (Asset asset, SecurityUser user) throws ThingsboardException; Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, Customer customer, SecurityUser user) throws ThingsboardException; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java index ba3141983d..b3abdd1fc6 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.entitiy.customer; import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; +import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.audit.ActionType; @@ -24,6 +25,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import org.thingsboard.server.service.security.model.SecurityUser; @@ -35,6 +37,8 @@ import java.util.List; @AllArgsConstructor public class DefaultTbCustomerService extends AbstractTbEntityService implements TbCustomerService { + private final TbClusterService tbClusterService; + @Override public Customer save(Customer customer, SecurityUser user) throws ThingsboardException { ActionType actionType = customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED; @@ -51,13 +55,15 @@ public class DefaultTbCustomerService extends AbstractTbEntityService implements @Override - public void delete(Customer customer, CustomerId customerId, SecurityUser user) throws ThingsboardException { - TenantId tenantId = user.getTenantId(); + public void delete(Customer customer, SecurityUser user) throws ThingsboardException { + TenantId tenantId = customer.getTenantId(); + CustomerId customerId = customer.getId(); try { List relatedEdgeIds = findRelatedEdgeIds(tenantId, customerId); customerService.deleteCustomer(tenantId, customerId); - notificationEntityService.notifyDeleteEntity(tenantId, customerId, customer, user.getCustomerId(), + notificationEntityService.notifyDeleteEntity(tenantId, customerId, customer, null, customerId, ActionType.DELETED, relatedEdgeIds, user, null); + tbClusterService.broadcastEntityStateChangeEvent(tenantId, customerId, ComponentLifecycleEvent.DELETED); } catch (Exception e) { notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.CUSTOMER), null, null, ActionType.DELETED, user, e); throw handleException(e); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java index 2e0d891046..870de10efc 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java @@ -16,9 +16,8 @@ package org.thingsboard.server.service.entitiy.customer; import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.service.entitiy.SimpleTbEntityService; -public interface TbCustomerService extends SimpleTbEntityService { +public interface TbCustomerService extends SimpleTbEntityService { } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java index 523ae2f651..cf7a4d9413 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java @@ -59,12 +59,13 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement } @Override - public void delete(Dashboard dashboard, DashboardId dashboardId, SecurityUser user) throws ThingsboardException { - TenantId tenantId = user.getTenantId(); + public void delete(Dashboard dashboard, SecurityUser user) throws ThingsboardException { + TenantId tenantId = dashboard.getTenantId(); + DashboardId dashboardId = dashboard.getId(); try { List relatedEdgeIds = findRelatedEdgeIds(tenantId, dashboardId); - dashboardService.deleteDashboard(tenantId, (DashboardId) dashboardId); - notificationEntityService.notifyDeleteEntity(tenantId, dashboardId, dashboard, user.getCustomerId(), + dashboardService.deleteDashboard(tenantId, dashboardId); + notificationEntityService.notifyDeleteEntity(tenantId, dashboardId, dashboard, null, user.getCustomerId(), ActionType.DELETED, relatedEdgeIds, user, null); } catch (Exception e) { notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DASHBOARD), null, null, @@ -104,7 +105,6 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement actionType, user, e, dashboardId.toString()); throw handleException(e); } - } @Override @@ -128,8 +128,6 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement public Dashboard updateDashboardCustomers(TenantId tenantId, Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException { ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER; try { - - Set customerIds = new HashSet<>(); if (strCustomerIds != null) { for (String strCustomerId : strCustomerIds) { @@ -216,8 +214,6 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement public Dashboard removeDashboardCustomers(TenantId tenantId, Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException { ActionType actionType = ActionType.UNASSIGNED_FROM_CUSTOMER; try { - - Set customerIds = new HashSet<>(); if (strCustomerIds != null) { for (String strCustomerId : strCustomerIds) { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java index 36598f514e..5daceb5510 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java @@ -24,7 +24,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.service.entitiy.SimpleTbEntityService; import org.thingsboard.server.service.security.model.SecurityUser; -public interface TbDashboardService extends SimpleTbEntityService { +public interface TbDashboardService extends SimpleTbEntityService { Dashboard assignDashboardToCustomer(DashboardId dashboardId, Customer customer, SecurityUser user) throws ThingsboardException; From 8852fc8f8e2524ade77fc99cbd21b0489309dd96 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 17 May 2022 08:11:19 +0200 Subject: [PATCH 161/276] enabled autoconfiguration for MeterRegistry --- .../server/coap/ThingsboardCoapTransportApplication.java | 2 ++ .../server/http/ThingsboardHttpTransportApplication.java | 2 ++ .../server/lwm2m/ThingsboardLwm2mTransportApplication.java | 2 ++ .../server/mqtt/ThingsboardMqttTransportApplication.java | 6 +++++- .../server/snmp/ThingsboardSnmpTransportApplication.java | 2 ++ 5 files changed, 13 insertions(+), 1 deletion(-) diff --git a/transport/coap/src/main/java/org/thingsboard/server/coap/ThingsboardCoapTransportApplication.java b/transport/coap/src/main/java/org/thingsboard/server/coap/ThingsboardCoapTransportApplication.java index 49e0ef1553..2478502bcc 100644 --- a/transport/coap/src/main/java/org/thingsboard/server/coap/ThingsboardCoapTransportApplication.java +++ b/transport/coap/src/main/java/org/thingsboard/server/coap/ThingsboardCoapTransportApplication.java @@ -17,6 +17,7 @@ package org.thingsboard.server.coap; import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.ComponentScan; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; @@ -26,6 +27,7 @@ import java.util.Arrays; @SpringBootConfiguration @EnableAsync @EnableScheduling +@EnableAutoConfiguration @ComponentScan({"org.thingsboard.server.coap", "org.thingsboard.server.common", "org.thingsboard.server.coapserver", "org.thingsboard.server.transport.coap", "org.thingsboard.server.queue", "org.thingsboard.server.cache"}) public class ThingsboardCoapTransportApplication { diff --git a/transport/http/src/main/java/org/thingsboard/server/http/ThingsboardHttpTransportApplication.java b/transport/http/src/main/java/org/thingsboard/server/http/ThingsboardHttpTransportApplication.java index 1ef3acd298..4d51a5a65a 100644 --- a/transport/http/src/main/java/org/thingsboard/server/http/ThingsboardHttpTransportApplication.java +++ b/transport/http/src/main/java/org/thingsboard/server/http/ThingsboardHttpTransportApplication.java @@ -16,6 +16,7 @@ package org.thingsboard.server.http; import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.ComponentScan; import org.springframework.scheduling.annotation.EnableAsync; @@ -24,6 +25,7 @@ import java.util.Arrays; @SpringBootApplication @EnableAsync +@EnableAutoConfiguration @ComponentScan({"org.thingsboard.server.http", "org.thingsboard.server.common", "org.thingsboard.server.transport.http", "org.thingsboard.server.queue", "org.thingsboard.server.cache"}) public class ThingsboardHttpTransportApplication { diff --git a/transport/lwm2m/src/main/java/org/thingsboard/server/lwm2m/ThingsboardLwm2mTransportApplication.java b/transport/lwm2m/src/main/java/org/thingsboard/server/lwm2m/ThingsboardLwm2mTransportApplication.java index 6889d7c593..62406bb8b0 100644 --- a/transport/lwm2m/src/main/java/org/thingsboard/server/lwm2m/ThingsboardLwm2mTransportApplication.java +++ b/transport/lwm2m/src/main/java/org/thingsboard/server/lwm2m/ThingsboardLwm2mTransportApplication.java @@ -17,6 +17,7 @@ package org.thingsboard.server.lwm2m; import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.ComponentScan; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; @@ -26,6 +27,7 @@ import java.util.Arrays; @SpringBootConfiguration @EnableAsync @EnableScheduling +@EnableAutoConfiguration @ComponentScan({"org.thingsboard.server.lwm2m", "org.thingsboard.server.common", "org.thingsboard.server.transport.lwm2m", "org.thingsboard.server.queue", "org.thingsboard.server.cache"}) public class ThingsboardLwm2mTransportApplication { diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/mqtt/ThingsboardMqttTransportApplication.java b/transport/mqtt/src/main/java/org/thingsboard/server/mqtt/ThingsboardMqttTransportApplication.java index 08d3c2b1f9..1f64be1efe 100644 --- a/transport/mqtt/src/main/java/org/thingsboard/server/mqtt/ThingsboardMqttTransportApplication.java +++ b/transport/mqtt/src/main/java/org/thingsboard/server/mqtt/ThingsboardMqttTransportApplication.java @@ -17,6 +17,10 @@ package org.thingsboard.server.mqtt; import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; +import org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.context.annotation.ComponentScan; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; @@ -25,7 +29,7 @@ import java.util.Arrays; @SpringBootConfiguration @EnableAsync -@EnableScheduling +@EnableAutoConfiguration @ComponentScan({"org.thingsboard.server.mqtt", "org.thingsboard.server.common", "org.thingsboard.server.transport.mqtt", "org.thingsboard.server.queue", "org.thingsboard.server.cache"}) public class ThingsboardMqttTransportApplication { diff --git a/transport/snmp/src/main/java/org/thingsboard/server/snmp/ThingsboardSnmpTransportApplication.java b/transport/snmp/src/main/java/org/thingsboard/server/snmp/ThingsboardSnmpTransportApplication.java index 4cfa81770c..10f8341bfc 100644 --- a/transport/snmp/src/main/java/org/thingsboard/server/snmp/ThingsboardSnmpTransportApplication.java +++ b/transport/snmp/src/main/java/org/thingsboard/server/snmp/ThingsboardSnmpTransportApplication.java @@ -17,6 +17,7 @@ package org.thingsboard.server.snmp; import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.ComponentScan; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; @@ -26,6 +27,7 @@ import java.util.Arrays; @SpringBootConfiguration @EnableAsync @EnableScheduling +@EnableAutoConfiguration @ComponentScan({"org.thingsboard.server.snmp", "org.thingsboard.server.common", "org.thingsboard.server.transport.snmp", "org.thingsboard.server.queue", "org.thingsboard.server.cache"}) public class ThingsboardSnmpTransportApplication { From 2831c58aed05606685ead912c85aa1691b4d19e8 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 17 May 2022 08:17:18 +0200 Subject: [PATCH 162/276] removed queues config from yml --- .../src/main/resources/tb-coap-transport.yml | 46 ------------------- .../src/main/resources/tb-http-transport.yml | 46 ------------------- .../src/main/resources/tb-lwm2m-transport.yml | 46 ------------------- .../src/main/resources/tb-mqtt-transport.yml | 46 ------------------- .../src/main/resources/tb-snmp-transport.yml | 46 ------------------- 5 files changed, 230 deletions(-) diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index f4717c418d..39cb9b35d0 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -260,52 +260,6 @@ queue: stats: enabled: "${TB_QUEUE_RULE_ENGINE_STATS_ENABLED:true}" print-interval-ms: "${TB_QUEUE_RULE_ENGINE_STATS_PRINT_INTERVAL_MS:60000}" - queues: - - name: "${TB_QUEUE_RE_MAIN_QUEUE_NAME:Main}" - topic: "${TB_QUEUE_RE_MAIN_TOPIC:tb_rule_engine.main}" - poll-interval: "${TB_QUEUE_RE_MAIN_POLL_INTERVAL_MS:25}" - partitions: "${TB_QUEUE_RE_MAIN_PARTITIONS:10}" - pack-processing-timeout: "${TB_QUEUE_RE_MAIN_PACK_PROCESSING_TIMEOUT_MS:60000}" - submit-strategy: - type: "${TB_QUEUE_RE_MAIN_SUBMIT_STRATEGY_TYPE:BURST}" # BURST, BATCH, SEQUENTIAL_BY_ORIGINATOR, SEQUENTIAL_BY_TENANT, SEQUENTIAL - # For BATCH only - batch-size: "${TB_QUEUE_RE_MAIN_SUBMIT_STRATEGY_BATCH_SIZE:1000}" # Maximum number of messages in batch - processing-strategy: - type: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_TYPE:SKIP_ALL_FAILURES}" # SKIP_ALL_FAILURES, SKIP_ALL_FAILURES_AND_TIMED_OUT, RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - # For RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - retries: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_RETRIES:3}" # Number of retries, 0 is unlimited - failure-percentage: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; - pause-between-retries: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_RETRY_PAUSE:3}"# Time in seconds to wait in consumer thread before retries; - - name: "${TB_QUEUE_RE_HP_QUEUE_NAME:HighPriority}" - topic: "${TB_QUEUE_RE_HP_TOPIC:tb_rule_engine.hp}" - poll-interval: "${TB_QUEUE_RE_HP_POLL_INTERVAL_MS:25}" - partitions: "${TB_QUEUE_RE_HP_PARTITIONS:10}" - pack-processing-timeout: "${TB_QUEUE_RE_HP_PACK_PROCESSING_TIMEOUT_MS:60000}" - submit-strategy: - type: "${TB_QUEUE_RE_HP_SUBMIT_STRATEGY_TYPE:BURST}" # BURST, BATCH, SEQUENTIAL_BY_ORIGINATOR, SEQUENTIAL_BY_TENANT, SEQUENTIAL - # For BATCH only - batch-size: "${TB_QUEUE_RE_HP_SUBMIT_STRATEGY_BATCH_SIZE:100}" # Maximum number of messages in batch - processing-strategy: - type: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_TYPE:RETRY_FAILED_AND_TIMED_OUT}" # SKIP_ALL_FAILURES, SKIP_ALL_FAILURES_AND_TIMED_OUT, RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - # For RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - retries: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_RETRIES:0}" # Number of retries, 0 is unlimited - failure-percentage: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; - pause-between-retries: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_RETRY_PAUSE:5}"# Time in seconds to wait in consumer thread before retries; - - name: "${TB_QUEUE_RE_SQ_QUEUE_NAME:SequentialByOriginator}" - topic: "${TB_QUEUE_RE_SQ_TOPIC:tb_rule_engine.sq}" - poll-interval: "${TB_QUEUE_RE_SQ_POLL_INTERVAL_MS:25}" - partitions: "${TB_QUEUE_RE_SQ_PARTITIONS:10}" - pack-processing-timeout: "${TB_QUEUE_RE_SQ_PACK_PROCESSING_TIMEOUT_MS:60000}" - submit-strategy: - type: "${TB_QUEUE_RE_SQ_SUBMIT_STRATEGY_TYPE:SEQUENTIAL_BY_ORIGINATOR}" # BURST, BATCH, SEQUENTIAL_BY_ORIGINATOR, SEQUENTIAL_BY_TENANT, SEQUENTIAL - # For BATCH only - batch-size: "${TB_QUEUE_RE_SQ_SUBMIT_STRATEGY_BATCH_SIZE:100}" # Maximum number of messages in batch - processing-strategy: - type: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_TYPE:RETRY_FAILED_AND_TIMED_OUT}" # SKIP_ALL_FAILURES, SKIP_ALL_FAILURES_AND_TIMED_OUT, RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - # For RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - retries: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_RETRIES:3}" # Number of retries, 0 is unlimited - failure-percentage: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; - pause-between-retries: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_RETRY_PAUSE:5}"# Time in seconds to wait in consumer thread before retries; transport: # For high priority notifications that require minimum latency and processing time notifications_topic: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_TOPIC:tb_transport.notifications}" diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index 76e78118af..29d8a1d381 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -248,52 +248,6 @@ queue: stats: enabled: "${TB_QUEUE_RULE_ENGINE_STATS_ENABLED:true}" print-interval-ms: "${TB_QUEUE_RULE_ENGINE_STATS_PRINT_INTERVAL_MS:60000}" - queues: - - name: "${TB_QUEUE_RE_MAIN_QUEUE_NAME:Main}" - topic: "${TB_QUEUE_RE_MAIN_TOPIC:tb_rule_engine.main}" - poll-interval: "${TB_QUEUE_RE_MAIN_POLL_INTERVAL_MS:25}" - partitions: "${TB_QUEUE_RE_MAIN_PARTITIONS:10}" - pack-processing-timeout: "${TB_QUEUE_RE_MAIN_PACK_PROCESSING_TIMEOUT_MS:60000}" - submit-strategy: - type: "${TB_QUEUE_RE_MAIN_SUBMIT_STRATEGY_TYPE:BURST}" # BURST, BATCH, SEQUENTIAL_BY_ORIGINATOR, SEQUENTIAL_BY_TENANT, SEQUENTIAL - # For BATCH only - batch-size: "${TB_QUEUE_RE_MAIN_SUBMIT_STRATEGY_BATCH_SIZE:1000}" # Maximum number of messages in batch - processing-strategy: - type: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_TYPE:SKIP_ALL_FAILURES}" # SKIP_ALL_FAILURES, SKIP_ALL_FAILURES_AND_TIMED_OUT, RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - # For RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - retries: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_RETRIES:3}" # Number of retries, 0 is unlimited - failure-percentage: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; - pause-between-retries: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_RETRY_PAUSE:3}"# Time in seconds to wait in consumer thread before retries; - - name: "${TB_QUEUE_RE_HP_QUEUE_NAME:HighPriority}" - topic: "${TB_QUEUE_RE_HP_TOPIC:tb_rule_engine.hp}" - poll-interval: "${TB_QUEUE_RE_HP_POLL_INTERVAL_MS:25}" - partitions: "${TB_QUEUE_RE_HP_PARTITIONS:10}" - pack-processing-timeout: "${TB_QUEUE_RE_HP_PACK_PROCESSING_TIMEOUT_MS:60000}" - submit-strategy: - type: "${TB_QUEUE_RE_HP_SUBMIT_STRATEGY_TYPE:BURST}" # BURST, BATCH, SEQUENTIAL_BY_ORIGINATOR, SEQUENTIAL_BY_TENANT, SEQUENTIAL - # For BATCH only - batch-size: "${TB_QUEUE_RE_HP_SUBMIT_STRATEGY_BATCH_SIZE:100}" # Maximum number of messages in batch - processing-strategy: - type: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_TYPE:RETRY_FAILED_AND_TIMED_OUT}" # SKIP_ALL_FAILURES, SKIP_ALL_FAILURES_AND_TIMED_OUT, RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - # For RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - retries: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_RETRIES:0}" # Number of retries, 0 is unlimited - failure-percentage: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; - pause-between-retries: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_RETRY_PAUSE:5}"# Time in seconds to wait in consumer thread before retries; - - name: "${TB_QUEUE_RE_SQ_QUEUE_NAME:SequentialByOriginator}" - topic: "${TB_QUEUE_RE_SQ_TOPIC:tb_rule_engine.sq}" - poll-interval: "${TB_QUEUE_RE_SQ_POLL_INTERVAL_MS:25}" - partitions: "${TB_QUEUE_RE_SQ_PARTITIONS:10}" - pack-processing-timeout: "${TB_QUEUE_RE_SQ_PACK_PROCESSING_TIMEOUT_MS:60000}" - submit-strategy: - type: "${TB_QUEUE_RE_SQ_SUBMIT_STRATEGY_TYPE:SEQUENTIAL_BY_ORIGINATOR}" # BURST, BATCH, SEQUENTIAL_BY_ORIGINATOR, SEQUENTIAL_BY_TENANT, SEQUENTIAL - # For BATCH only - batch-size: "${TB_QUEUE_RE_SQ_SUBMIT_STRATEGY_BATCH_SIZE:100}" # Maximum number of messages in batch - processing-strategy: - type: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_TYPE:RETRY_FAILED_AND_TIMED_OUT}" # SKIP_ALL_FAILURES, SKIP_ALL_FAILURES_AND_TIMED_OUT, RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - # For RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - retries: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_RETRIES:3}" # Number of retries, 0 is unlimited - failure-percentage: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; - pause-between-retries: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_RETRY_PAUSE:5}"# Time in seconds to wait in consumer thread before retries; transport: # For high priority notifications that require minimum latency and processing time notifications_topic: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_TOPIC:tb_transport.notifications}" diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index c9ab9c5542..361ac7c651 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -327,52 +327,6 @@ queue: stats: enabled: "${TB_QUEUE_RULE_ENGINE_STATS_ENABLED:true}" print-interval-ms: "${TB_QUEUE_RULE_ENGINE_STATS_PRINT_INTERVAL_MS:60000}" - queues: - - name: "${TB_QUEUE_RE_MAIN_QUEUE_NAME:Main}" - topic: "${TB_QUEUE_RE_MAIN_TOPIC:tb_rule_engine.main}" - poll-interval: "${TB_QUEUE_RE_MAIN_POLL_INTERVAL_MS:25}" - partitions: "${TB_QUEUE_RE_MAIN_PARTITIONS:10}" - pack-processing-timeout: "${TB_QUEUE_RE_MAIN_PACK_PROCESSING_TIMEOUT_MS:60000}" - submit-strategy: - type: "${TB_QUEUE_RE_MAIN_SUBMIT_STRATEGY_TYPE:BURST}" # BURST, BATCH, SEQUENTIAL_BY_ORIGINATOR, SEQUENTIAL_BY_TENANT, SEQUENTIAL - # For BATCH only - batch-size: "${TB_QUEUE_RE_MAIN_SUBMIT_STRATEGY_BATCH_SIZE:1000}" # Maximum number of messages in batch - processing-strategy: - type: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_TYPE:SKIP_ALL_FAILURES}" # SKIP_ALL_FAILURES, SKIP_ALL_FAILURES_AND_TIMED_OUT, RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - # For RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - retries: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_RETRIES:3}" # Number of retries, 0 is unlimited - failure-percentage: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; - pause-between-retries: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_RETRY_PAUSE:3}"# Time in seconds to wait in consumer thread before retries; - - name: "${TB_QUEUE_RE_HP_QUEUE_NAME:HighPriority}" - topic: "${TB_QUEUE_RE_HP_TOPIC:tb_rule_engine.hp}" - poll-interval: "${TB_QUEUE_RE_HP_POLL_INTERVAL_MS:25}" - partitions: "${TB_QUEUE_RE_HP_PARTITIONS:10}" - pack-processing-timeout: "${TB_QUEUE_RE_HP_PACK_PROCESSING_TIMEOUT_MS:60000}" - submit-strategy: - type: "${TB_QUEUE_RE_HP_SUBMIT_STRATEGY_TYPE:BURST}" # BURST, BATCH, SEQUENTIAL_BY_ORIGINATOR, SEQUENTIAL_BY_TENANT, SEQUENTIAL - # For BATCH only - batch-size: "${TB_QUEUE_RE_HP_SUBMIT_STRATEGY_BATCH_SIZE:100}" # Maximum number of messages in batch - processing-strategy: - type: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_TYPE:RETRY_FAILED_AND_TIMED_OUT}" # SKIP_ALL_FAILURES, SKIP_ALL_FAILURES_AND_TIMED_OUT, RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - # For RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - retries: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_RETRIES:0}" # Number of retries, 0 is unlimited - failure-percentage: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; - pause-between-retries: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_RETRY_PAUSE:5}"# Time in seconds to wait in consumer thread before retries; - - name: "${TB_QUEUE_RE_SQ_QUEUE_NAME:SequentialByOriginator}" - topic: "${TB_QUEUE_RE_SQ_TOPIC:tb_rule_engine.sq}" - poll-interval: "${TB_QUEUE_RE_SQ_POLL_INTERVAL_MS:25}" - partitions: "${TB_QUEUE_RE_SQ_PARTITIONS:10}" - pack-processing-timeout: "${TB_QUEUE_RE_SQ_PACK_PROCESSING_TIMEOUT_MS:60000}" - submit-strategy: - type: "${TB_QUEUE_RE_SQ_SUBMIT_STRATEGY_TYPE:SEQUENTIAL_BY_ORIGINATOR}" # BURST, BATCH, SEQUENTIAL_BY_ORIGINATOR, SEQUENTIAL_BY_TENANT, SEQUENTIAL - # For BATCH only - batch-size: "${TB_QUEUE_RE_SQ_SUBMIT_STRATEGY_BATCH_SIZE:100}" # Maximum number of messages in batch - processing-strategy: - type: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_TYPE:RETRY_FAILED_AND_TIMED_OUT}" # SKIP_ALL_FAILURES, SKIP_ALL_FAILURES_AND_TIMED_OUT, RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - # For RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - retries: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_RETRIES:3}" # Number of retries, 0 is unlimited - failure-percentage: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; - pause-between-retries: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_RETRY_PAUSE:5}"# Time in seconds to wait in consumer thread before retries; transport: # For high priority notifications that require minimum latency and processing time notifications_topic: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_TOPIC:tb_transport.notifications}" diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index 17858eeb1a..1b84701ccf 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -278,52 +278,6 @@ queue: stats: enabled: "${TB_QUEUE_RULE_ENGINE_STATS_ENABLED:true}" print-interval-ms: "${TB_QUEUE_RULE_ENGINE_STATS_PRINT_INTERVAL_MS:60000}" - queues: - - name: "${TB_QUEUE_RE_MAIN_QUEUE_NAME:Main}" - topic: "${TB_QUEUE_RE_MAIN_TOPIC:tb_rule_engine.main}" - poll-interval: "${TB_QUEUE_RE_MAIN_POLL_INTERVAL_MS:25}" - partitions: "${TB_QUEUE_RE_MAIN_PARTITIONS:10}" - pack-processing-timeout: "${TB_QUEUE_RE_MAIN_PACK_PROCESSING_TIMEOUT_MS:60000}" - submit-strategy: - type: "${TB_QUEUE_RE_MAIN_SUBMIT_STRATEGY_TYPE:BURST}" # BURST, BATCH, SEQUENTIAL_BY_ORIGINATOR, SEQUENTIAL_BY_TENANT, SEQUENTIAL - # For BATCH only - batch-size: "${TB_QUEUE_RE_MAIN_SUBMIT_STRATEGY_BATCH_SIZE:1000}" # Maximum number of messages in batch - processing-strategy: - type: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_TYPE:SKIP_ALL_FAILURES}" # SKIP_ALL_FAILURES, SKIP_ALL_FAILURES_AND_TIMED_OUT, RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - # For RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - retries: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_RETRIES:3}" # Number of retries, 0 is unlimited - failure-percentage: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; - pause-between-retries: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_RETRY_PAUSE:3}"# Time in seconds to wait in consumer thread before retries; - - name: "${TB_QUEUE_RE_HP_QUEUE_NAME:HighPriority}" - topic: "${TB_QUEUE_RE_HP_TOPIC:tb_rule_engine.hp}" - poll-interval: "${TB_QUEUE_RE_HP_POLL_INTERVAL_MS:25}" - partitions: "${TB_QUEUE_RE_HP_PARTITIONS:10}" - pack-processing-timeout: "${TB_QUEUE_RE_HP_PACK_PROCESSING_TIMEOUT_MS:60000}" - submit-strategy: - type: "${TB_QUEUE_RE_HP_SUBMIT_STRATEGY_TYPE:BURST}" # BURST, BATCH, SEQUENTIAL_BY_ORIGINATOR, SEQUENTIAL_BY_TENANT, SEQUENTIAL - # For BATCH only - batch-size: "${TB_QUEUE_RE_HP_SUBMIT_STRATEGY_BATCH_SIZE:100}" # Maximum number of messages in batch - processing-strategy: - type: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_TYPE:RETRY_FAILED_AND_TIMED_OUT}" # SKIP_ALL_FAILURES, SKIP_ALL_FAILURES_AND_TIMED_OUT, RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - # For RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - retries: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_RETRIES:0}" # Number of retries, 0 is unlimited - failure-percentage: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; - pause-between-retries: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_RETRY_PAUSE:5}"# Time in seconds to wait in consumer thread before retries; - - name: "${TB_QUEUE_RE_SQ_QUEUE_NAME:SequentialByOriginator}" - topic: "${TB_QUEUE_RE_SQ_TOPIC:tb_rule_engine.sq}" - poll-interval: "${TB_QUEUE_RE_SQ_POLL_INTERVAL_MS:25}" - partitions: "${TB_QUEUE_RE_SQ_PARTITIONS:10}" - pack-processing-timeout: "${TB_QUEUE_RE_SQ_PACK_PROCESSING_TIMEOUT_MS:60000}" - submit-strategy: - type: "${TB_QUEUE_RE_SQ_SUBMIT_STRATEGY_TYPE:SEQUENTIAL_BY_ORIGINATOR}" # BURST, BATCH, SEQUENTIAL_BY_ORIGINATOR, SEQUENTIAL_BY_TENANT, SEQUENTIAL - # For BATCH only - batch-size: "${TB_QUEUE_RE_SQ_SUBMIT_STRATEGY_BATCH_SIZE:100}" # Maximum number of messages in batch - processing-strategy: - type: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_TYPE:RETRY_FAILED_AND_TIMED_OUT}" # SKIP_ALL_FAILURES, SKIP_ALL_FAILURES_AND_TIMED_OUT, RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - # For RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - retries: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_RETRIES:3}" # Number of retries, 0 is unlimited - failure-percentage: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; - pause-between-retries: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_RETRY_PAUSE:5}"# Time in seconds to wait in consumer thread before retries; transport: # For high priority notifications that require minimum latency and processing time notifications_topic: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_TOPIC:tb_transport.notifications}" diff --git a/transport/snmp/src/main/resources/tb-snmp-transport.yml b/transport/snmp/src/main/resources/tb-snmp-transport.yml index 34dfbddcdc..7562af359b 100644 --- a/transport/snmp/src/main/resources/tb-snmp-transport.yml +++ b/transport/snmp/src/main/resources/tb-snmp-transport.yml @@ -228,52 +228,6 @@ queue: stats: enabled: "${TB_QUEUE_RULE_ENGINE_STATS_ENABLED:true}" print-interval-ms: "${TB_QUEUE_RULE_ENGINE_STATS_PRINT_INTERVAL_MS:60000}" - queues: - - name: "${TB_QUEUE_RE_MAIN_QUEUE_NAME:Main}" - topic: "${TB_QUEUE_RE_MAIN_TOPIC:tb_rule_engine.main}" - poll-interval: "${TB_QUEUE_RE_MAIN_POLL_INTERVAL_MS:25}" - partitions: "${TB_QUEUE_RE_MAIN_PARTITIONS:10}" - pack-processing-timeout: "${TB_QUEUE_RE_MAIN_PACK_PROCESSING_TIMEOUT_MS:60000}" - submit-strategy: - type: "${TB_QUEUE_RE_MAIN_SUBMIT_STRATEGY_TYPE:BURST}" # BURST, BATCH, SEQUENTIAL_BY_ORIGINATOR, SEQUENTIAL_BY_TENANT, SEQUENTIAL - # For BATCH only - batch-size: "${TB_QUEUE_RE_MAIN_SUBMIT_STRATEGY_BATCH_SIZE:1000}" # Maximum number of messages in batch - processing-strategy: - type: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_TYPE:SKIP_ALL_FAILURES}" # SKIP_ALL_FAILURES, SKIP_ALL_FAILURES_AND_TIMED_OUT, RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - # For RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - retries: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_RETRIES:3}" # Number of retries, 0 is unlimited - failure-percentage: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; - pause-between-retries: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_RETRY_PAUSE:3}"# Time in seconds to wait in consumer thread before retries; - - name: "${TB_QUEUE_RE_HP_QUEUE_NAME:HighPriority}" - topic: "${TB_QUEUE_RE_HP_TOPIC:tb_rule_engine.hp}" - poll-interval: "${TB_QUEUE_RE_HP_POLL_INTERVAL_MS:25}" - partitions: "${TB_QUEUE_RE_HP_PARTITIONS:10}" - pack-processing-timeout: "${TB_QUEUE_RE_HP_PACK_PROCESSING_TIMEOUT_MS:60000}" - submit-strategy: - type: "${TB_QUEUE_RE_HP_SUBMIT_STRATEGY_TYPE:BURST}" # BURST, BATCH, SEQUENTIAL_BY_ORIGINATOR, SEQUENTIAL_BY_TENANT, SEQUENTIAL - # For BATCH only - batch-size: "${TB_QUEUE_RE_HP_SUBMIT_STRATEGY_BATCH_SIZE:100}" # Maximum number of messages in batch - processing-strategy: - type: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_TYPE:RETRY_FAILED_AND_TIMED_OUT}" # SKIP_ALL_FAILURES, SKIP_ALL_FAILURES_AND_TIMED_OUT, RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - # For RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - retries: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_RETRIES:0}" # Number of retries, 0 is unlimited - failure-percentage: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; - pause-between-retries: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_RETRY_PAUSE:5}"# Time in seconds to wait in consumer thread before retries; - - name: "${TB_QUEUE_RE_SQ_QUEUE_NAME:SequentialByOriginator}" - topic: "${TB_QUEUE_RE_SQ_TOPIC:tb_rule_engine.sq}" - poll-interval: "${TB_QUEUE_RE_SQ_POLL_INTERVAL_MS:25}" - partitions: "${TB_QUEUE_RE_SQ_PARTITIONS:10}" - pack-processing-timeout: "${TB_QUEUE_RE_SQ_PACK_PROCESSING_TIMEOUT_MS:60000}" - submit-strategy: - type: "${TB_QUEUE_RE_SQ_SUBMIT_STRATEGY_TYPE:SEQUENTIAL_BY_ORIGINATOR}" # BURST, BATCH, SEQUENTIAL_BY_ORIGINATOR, SEQUENTIAL_BY_TENANT, SEQUENTIAL - # For BATCH only - batch-size: "${TB_QUEUE_RE_SQ_SUBMIT_STRATEGY_BATCH_SIZE:100}" # Maximum number of messages in batch - processing-strategy: - type: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_TYPE:RETRY_FAILED_AND_TIMED_OUT}" # SKIP_ALL_FAILURES, SKIP_ALL_FAILURES_AND_TIMED_OUT, RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - # For RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT - retries: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_RETRIES:3}" # Number of retries, 0 is unlimited - failure-percentage: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; - pause-between-retries: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_RETRY_PAUSE:5}"# Time in seconds to wait in consumer thread before retries; transport: # For high priority notifications that require minimum latency and processing time notifications_topic: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_TOPIC:tb_transport.notifications}" From a0a0d5dcf9863c001507c4df13bc6c96d0a08bfe Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 17 May 2022 08:19:22 +0200 Subject: [PATCH 163/276] changed log level for transports --- transport/coap/src/main/resources/logback.xml | 2 +- transport/http/src/main/resources/logback.xml | 2 +- transport/lwm2m/src/main/resources/logback.xml | 2 +- transport/mqtt/src/main/resources/logback.xml | 2 +- transport/snmp/src/main/resources/logback.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/transport/coap/src/main/resources/logback.xml b/transport/coap/src/main/resources/logback.xml index 6149e004d9..240eccaa64 100644 --- a/transport/coap/src/main/resources/logback.xml +++ b/transport/coap/src/main/resources/logback.xml @@ -25,7 +25,7 @@ - + diff --git a/transport/http/src/main/resources/logback.xml b/transport/http/src/main/resources/logback.xml index 6149e004d9..240eccaa64 100644 --- a/transport/http/src/main/resources/logback.xml +++ b/transport/http/src/main/resources/logback.xml @@ -25,7 +25,7 @@ - + diff --git a/transport/lwm2m/src/main/resources/logback.xml b/transport/lwm2m/src/main/resources/logback.xml index 6149e004d9..240eccaa64 100644 --- a/transport/lwm2m/src/main/resources/logback.xml +++ b/transport/lwm2m/src/main/resources/logback.xml @@ -25,7 +25,7 @@ - + diff --git a/transport/mqtt/src/main/resources/logback.xml b/transport/mqtt/src/main/resources/logback.xml index 6149e004d9..240eccaa64 100644 --- a/transport/mqtt/src/main/resources/logback.xml +++ b/transport/mqtt/src/main/resources/logback.xml @@ -25,7 +25,7 @@ - + diff --git a/transport/snmp/src/main/resources/logback.xml b/transport/snmp/src/main/resources/logback.xml index 6149e004d9..240eccaa64 100644 --- a/transport/snmp/src/main/resources/logback.xml +++ b/transport/snmp/src/main/resources/logback.xml @@ -25,7 +25,7 @@ - + From f9aae60a529829916cd043f722b68c0cabba5bd1 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Tue, 17 May 2022 09:55:07 +0300 Subject: [PATCH 164/276] Sync relation creation for device creation through gateway --- .../server/service/transport/DefaultTransportApiService.java | 2 +- .../server/msa/connectivity/MqttGatewayClientTest.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 5fda933234..ae42fb878c 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -292,7 +292,7 @@ public class DefaultTransportApiService implements TransportApiService { tbClusterService.onDeviceUpdated(savedDevice, null); device = savedDevice; - relationService.saveRelationAsync(TenantId.SYS_TENANT_ID, new EntityRelation(gateway.getId(), device.getId(), "Created")); + relationService.saveRelation(TenantId.SYS_TENANT_ID, new EntityRelation(gateway.getId(), device.getId(), "Created")); TbMsgMetaData metaData = new TbMsgMetaData(); CustomerId customerId = gateway.getCustomerId(); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java index 951efd6dfe..ec77cbec36 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java @@ -408,9 +408,8 @@ public class MqttGatewayClientTest extends AbstractContainerTest { private Device createDeviceThroughGateway(MqttClient mqttClient, Device gatewayDevice) throws Exception { String deviceName = "mqtt_device"; - mqttClient.publish("v1/gateway/connect", Unpooled.wrappedBuffer(createGatewayConnectPayload(deviceName).toString().getBytes())).get(); + mqttClient.publish("v1/gateway/connect", Unpooled.wrappedBuffer(createGatewayConnectPayload(deviceName).toString().getBytes()), MqttQoS.AT_LEAST_ONCE).get(); - TimeUnit.SECONDS.sleep(3); List relations = restClient.findByFrom(gatewayDevice.getId(), RelationTypeGroup.COMMON); Assert.assertEquals(1, relations.size()); From e59ffcdba6a4eed9c3760d6cb1146b5d6fa6f7b5 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Tue, 17 May 2022 12:07:11 +0300 Subject: [PATCH 165/276] refactoring: - dashboard delete tenantId from interface --- .../server/controller/DashboardController.java | 8 ++++---- .../entitiy/dashboard/DefaultTbDashboardService.java | 12 ++++++++---- .../entitiy/dashboard/TbDashboardService.java | 8 ++++---- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index 7ff16baae0..7a101f3571 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -258,7 +258,7 @@ public class DashboardController extends BaseController { checkParameter(DASHBOARD_ID, strDashboardId); DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); Dashboard dashboard = checkDashboardId(dashboardId, Operation.ASSIGN_TO_CUSTOMER); - return tbDashboardService.updateDashboardCustomers(getTenantId(), dashboard, strCustomerIds, getCurrentUser()); + return tbDashboardService.updateDashboardCustomers(dashboard, strCustomerIds, getCurrentUser()); } @ApiOperation(value = "Adds the Dashboard Customers (addDashboardCustomers)", @@ -277,7 +277,7 @@ public class DashboardController extends BaseController { checkParameter(DASHBOARD_ID, strDashboardId); DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); Dashboard dashboard = checkDashboardId(dashboardId, Operation.ASSIGN_TO_CUSTOMER); - return tbDashboardService.addDashboardCustomers(getTenantId(), dashboard, strCustomerIds, getCurrentUser()); + return tbDashboardService.addDashboardCustomers(dashboard, strCustomerIds, getCurrentUser()); } @ApiOperation(value = "Remove the Dashboard Customers (removeDashboardCustomers)", @@ -296,7 +296,7 @@ public class DashboardController extends BaseController { checkParameter(DASHBOARD_ID, strDashboardId); DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); Dashboard dashboard = checkDashboardId(dashboardId, Operation.UNASSIGN_FROM_CUSTOMER); - return tbDashboardService.removeDashboardCustomers(getTenantId(), dashboard, strCustomerIds, getCurrentUser()); + return tbDashboardService.removeDashboardCustomers(dashboard, strCustomerIds, getCurrentUser()); } @@ -626,7 +626,7 @@ public class DashboardController extends BaseController { DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); checkDashboardId(dashboardId, Operation.READ); - return tbDashboardService.asignDashboardToEdge(getTenantId(), dashboardId, edge, getCurrentUser()); + return tbDashboardService.asignDashboardToEdge(dashboardId, edge, getCurrentUser()); } @ApiOperation(value = "Unassign dashboard from edge (unassignDashboardFromEdge)", diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java index cf7a4d9413..a9c6e7dde5 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java @@ -125,8 +125,9 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement } @Override - public Dashboard updateDashboardCustomers(TenantId tenantId, Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException { + public Dashboard updateDashboardCustomers(Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException { ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER; + TenantId tenantId = user.getTenantId(); try { Set customerIds = new HashSet<>(); if (strCustomerIds != null) { @@ -178,8 +179,9 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement } @Override - public Dashboard addDashboardCustomers(TenantId tenantId, Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException { + public Dashboard addDashboardCustomers(Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException { ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER; + TenantId tenantId = user.getTenantId(); try { Set customerIds = new HashSet<>(); if (strCustomerIds != null) { @@ -211,8 +213,9 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement } @Override - public Dashboard removeDashboardCustomers(TenantId tenantId, Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException { + public Dashboard removeDashboardCustomers(Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException { ActionType actionType = ActionType.UNASSIGNED_FROM_CUSTOMER; + TenantId tenantId = user.getTenantId(); try { Set customerIds = new HashSet<>(); if (strCustomerIds != null) { @@ -244,8 +247,9 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement } @Override - public Dashboard asignDashboardToEdge(TenantId tenantId, DashboardId dashboardId, Edge edge, SecurityUser user) throws ThingsboardException { + public Dashboard asignDashboardToEdge(DashboardId dashboardId, Edge edge, SecurityUser user) throws ThingsboardException { ActionType actionType = ActionType.ASSIGNED_TO_EDGE; + TenantId tenantId = user.getTenantId(); EdgeId edgeId = edge.getId(); try { Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToEdge(tenantId, dashboardId, edgeId)); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java index 5daceb5510..2930b1c10d 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java @@ -32,13 +32,13 @@ public interface TbDashboardService extends SimpleTbEntityService { Dashboard unassignDashboardFromPublicCustomer(Dashboard dashboard, SecurityUser user) throws ThingsboardException; - Dashboard updateDashboardCustomers(TenantId tenantId, Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException; + Dashboard updateDashboardCustomers(Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException; - Dashboard addDashboardCustomers(TenantId tenantId, Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException; + Dashboard addDashboardCustomers(Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException; - Dashboard removeDashboardCustomers(TenantId tenantId, Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException; + Dashboard removeDashboardCustomers(Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException; - Dashboard asignDashboardToEdge(TenantId tenantId, DashboardId dashboardId, Edge edge, SecurityUser user) throws ThingsboardException; + Dashboard asignDashboardToEdge(DashboardId dashboardId, Edge edge, SecurityUser user) throws ThingsboardException; Dashboard unassignDashboardFromEdge(Dashboard dashboard, Edge edge, SecurityUser user) throws ThingsboardException; From 4f6ef91d33ce97bcb166c3fe22d8b876ed08cbc6 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Tue, 17 May 2022 12:49:54 +0300 Subject: [PATCH 166/276] Fix race conditions in WS test client --- .../server/controller/TbTestWebSocketClient.java | 8 ++++---- .../AbstractMqttAttributesIntegrationTest.java | 14 +++++++++----- .../dao/attributes/CachedAttributesService.java | 5 +++++ 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java index 2b5dabb3b9..a306294237 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -60,12 +60,12 @@ public class TbTestWebSocketClient extends WebSocketClient { public void onMessage(String s) { log.info("RECEIVED: {}", s); lastMsg = s; - if (reply != null) { - reply.countDown(); - } if (update != null) { update.countDown(); } + if (reply != null) { + reply.countDown(); + } } @Override diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java index 556af34371..4502e79ff2 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.query.DeviceTypeFilter; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.SingleEntityFilter; import org.thingsboard.server.gen.transport.TransportApiProtos; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; @@ -319,7 +320,8 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt protected void processJsonTestRequestAttributesValuesFromTheServer(String attrPubTopic, String attrSubTopic, String attrReqTopicPrefix) throws Exception { MqttTestClient client = new MqttTestClient(); client.connectAndWait(accessToken); - DeviceTypeFilter dtf = new DeviceTypeFilter(savedDevice.getType(), savedDevice.getName()); + SingleEntityFilter dtf = new SingleEntityFilter(); + dtf.setSingleEntity(savedDevice.getId()); String clientKeysStr = "clientStr,clientBool,clientDbl,clientLong,clientJson"; String sharedKeysStr = "sharedStr,sharedBool,sharedDbl,sharedLong,sharedJson"; List clientKeysList = List.of(clientKeysStr.split(",")); @@ -389,7 +391,8 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt 100); assertNotNull(device); - DeviceTypeFilter dtf = new DeviceTypeFilter(device.getType(), device.getName()); + SingleEntityFilter dtf = new SingleEntityFilter(); + dtf.setSingleEntity(device.getId()); String clientKeysStr = "clientStr,clientBool,clientDbl,clientLong,clientJson"; String sharedKeysStr = "sharedStr,sharedBool,sharedDbl,sharedLong,sharedJson"; List clientKeysList = List.of(clientKeysStr.split(",")); @@ -443,7 +446,8 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt 100); assertNotNull(device); - DeviceTypeFilter dtf = new DeviceTypeFilter(device.getType(), device.getName()); + SingleEntityFilter dtf = new SingleEntityFilter(); + dtf.setSingleEntity(device.getId()); String sharedKeysStr = "sharedStr,sharedBool,sharedDbl,sharedLong,sharedJson"; List sharedKeysList = List.of(sharedKeysStr.split(",")); List csKeys = getEntityKeys(clientKeysList, CLIENT_ATTRIBUTE); @@ -569,7 +573,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt assertEquals(JacksonUtil.toJsonNode(expectedRequestPayload), JacksonUtil.fromBytes(callback.getPayloadBytes())); } - protected void validateProtoClientResponseGateway(MqttTestCallback callback, String deviceName) throws InterruptedException, InvalidProtocolBufferException { + protected void validateProtoClientResponseGateway(MqttTestCallback callback, String deviceName) throws InterruptedException, InvalidProtocolBufferException { callback.getSubscribeLatch().await(3, TimeUnit.SECONDS); assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS()); TransportApiProtos.GatewayAttributeResponseMsg expectedGatewayAttributeResponseMsg = getExpectedGatewayAttributeResponseMsg(deviceName, true); diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index 3ce56dfbf3..802e746122 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -141,6 +141,7 @@ public class CachedAttributesService implements AttributesService { .filter(Objects::nonNull) .collect(Collectors.toList()); if (wrappedCachedAttributes.size() == attributeKeys.size()) { + log.trace("[{}][{}] Found all attributes from cache: {}", entityId, scope, attributeKeys); return Futures.immediateFuture(cachedAttributes); } @@ -152,6 +153,7 @@ public class CachedAttributesService implements AttributesService { return cacheExecutor.submit(() -> { var cacheTransaction = cache.newTransactionForKeys(notFoundKeys); try { + log.trace("[{}][{}] Lookup attributes from db: {}", entityId, scope, notFoundAttributeKeys); List result = attributesDao.find(tenantId, entityId, scope, notFoundAttributeKeys); for (AttributeKvEntry foundInDbAttribute : result) { AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, foundInDbAttribute.getKey()); @@ -164,6 +166,7 @@ public class CachedAttributesService implements AttributesService { List mergedAttributes = new ArrayList<>(cachedAttributes); mergedAttributes.addAll(result); cacheTransaction.commit(); + log.trace("[{}][{}] Commit cache transaction: {}", entityId, scope, notFoundAttributeKeys); return mergedAttributes; } catch (Throwable e) { cacheTransaction.rollback(); @@ -211,7 +214,9 @@ public class CachedAttributesService implements AttributesService { for (var attribute : attributes) { ListenableFuture future = attributesDao.save(tenantId, entityId, scope, attribute); futures.add(Futures.transform(future, key -> { + log.trace("[{}][{}][{}] Before cache evict: {}", entityId, scope, key, attribute); cache.evictOrPut(new AttributeCacheKey(scope, entityId, key), attribute); + log.trace("[{}][{}][{}] after cache evict.", entityId, scope, key); return key; }, cacheExecutor)); } From 24db25e3b7923cf10b258dcd4e55870aed23dfad Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Tue, 17 May 2022 12:51:01 +0300 Subject: [PATCH 167/276] WS Client race-condition fix --- .../thingsboard/server/controller/TbTestWebSocketClient.java | 2 +- .../mqtt/attributes/AbstractMqttAttributesIntegrationTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java index a306294237..be7973d517 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java index 4502e79ff2..6d030aca9f 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java @@ -5,7 +5,7 @@ * 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 + * 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, From 8232fc4b7018654fd0ccc723f7cd9b5f9421392b Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Tue, 17 May 2022 13:58:18 +0300 Subject: [PATCH 168/276] refactoring: - dashboard comments2 --- .../server/controller/AlarmController.java | 2 +- .../server/controller/AssetController.java | 4 +- .../controller/DashboardController.java | 27 +++++++++++-- .../server/controller/DeviceController.java | 2 +- .../server/controller/EdgeController.java | 4 +- .../service/asset/AssetBulkImportService.java | 2 +- .../service/edge/EdgeBulkImportService.java | 2 +- .../DefaultTbNotificationEntityService.java | 34 ++++++++++++----- .../entitiy/TbNotificationEntityService.java | 12 ++++-- .../entitiy/alarm/DefaultTbAlarmService.java | 4 +- .../service/entitiy/alarm/TbAlarmService.java | 2 +- .../entitiy/asset/DefaultTbAssetService.java | 6 +-- .../service/entitiy/asset/TbAssetService.java | 4 +- .../customer/DefaultTbCustomerService.java | 2 +- .../dashboard/DefaultTbDashboardService.java | 38 +++---------------- .../entitiy/dashboard/TbDashboardService.java | 10 +++-- .../device/DefaultTbDeviceService.java | 2 +- .../entitiy/device/TbDeviceService.java | 2 +- .../entitiy/edge/DefaultTbEdgeService.java | 4 +- .../service/entitiy/edge/TbEdgeService.java | 4 +- 20 files changed, 91 insertions(+), 76 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java index e1db3c55e0..939e9da8a0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java @@ -147,7 +147,7 @@ public class AlarmController extends BaseController { try { AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); - return tbAlarmService.deleteAlarm(alarm, getCurrentUser()); + return tbAlarmService.delete(alarm, getCurrentUser()); } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java index 9edd328b68..4718feb774 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java @@ -149,7 +149,7 @@ public class AssetController extends BaseController { } asset.setTenantId(getCurrentUser().getTenantId()); checkEntity(asset.getId(), asset, Resource.ASSET); - return tbAssetService.saveAsset(asset, getCurrentUser()); + return tbAssetService.save(asset, getCurrentUser()); } @ApiOperation(value = "Delete asset (deleteAsset)", @@ -162,7 +162,7 @@ public class AssetController extends BaseController { try { AssetId assetId = new AssetId(toUUID(strAssetId)); Asset asset = checkAssetId(assetId, Operation.DELETE); - tbAssetService.deleteAsset(asset, getCurrentUser()).get(); + tbAssetService.delete(asset, getCurrentUser()).get(); } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index 7a101f3571..044c61001d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -39,6 +39,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DashboardInfo; +import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HomeDashboard; import org.thingsboard.server.common.data.HomeDashboardInfo; import org.thingsboard.server.common.data.Tenant; @@ -57,7 +58,10 @@ import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID; @@ -258,7 +262,8 @@ public class DashboardController extends BaseController { checkParameter(DASHBOARD_ID, strDashboardId); DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); Dashboard dashboard = checkDashboardId(dashboardId, Operation.ASSIGN_TO_CUSTOMER); - return tbDashboardService.updateDashboardCustomers(dashboard, strCustomerIds, getCurrentUser()); + Set customerIds = customerIdFromStr(strCustomerIds, dashboard); + return tbDashboardService.updateDashboardCustomers(dashboard, customerIds, getCurrentUser()); } @ApiOperation(value = "Adds the Dashboard Customers (addDashboardCustomers)", @@ -277,7 +282,8 @@ public class DashboardController extends BaseController { checkParameter(DASHBOARD_ID, strDashboardId); DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); Dashboard dashboard = checkDashboardId(dashboardId, Operation.ASSIGN_TO_CUSTOMER); - return tbDashboardService.addDashboardCustomers(dashboard, strCustomerIds, getCurrentUser()); + Set customerIds = customerIdFromStr(strCustomerIds, dashboard); + return tbDashboardService.addDashboardCustomers(dashboard, customerIds, getCurrentUser()); } @ApiOperation(value = "Remove the Dashboard Customers (removeDashboardCustomers)", @@ -296,8 +302,8 @@ public class DashboardController extends BaseController { checkParameter(DASHBOARD_ID, strDashboardId); DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); Dashboard dashboard = checkDashboardId(dashboardId, Operation.UNASSIGN_FROM_CUSTOMER); - return tbDashboardService.removeDashboardCustomers(dashboard, strCustomerIds, getCurrentUser()); - + Set customerIds = customerIdFromStr(strCustomerIds, dashboard); + return tbDashboardService.removeDashboardCustomers(dashboard, customerIds, getCurrentUser()); } @ApiOperation(value = "Assign the Dashboard to Public Customer (assignDashboardToPublicCustomer)", @@ -698,4 +704,17 @@ public class DashboardController extends BaseController { throw handleException(e); } } + + private Set customerIdFromStr(String [] strCustomerIds, Dashboard dashboard) { + Set customerIds = new HashSet<>(); + if (strCustomerIds != null) { + for (String strCustomerId : strCustomerIds) { + CustomerId customerId = new CustomerId(UUID.fromString(strCustomerId)); + if (dashboard.isAssignedToCustomer(customerId)) { + customerIds.add(customerId); + } + } + } + return customerIds; + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index f7b3908010..793ea64336 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -206,7 +206,7 @@ public class DeviceController extends BaseController { DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); Device device = checkDeviceId(deviceId, Operation.DELETE); try { - tbDeviceService.deleteDevice(device, getCurrentUser()).get(); + tbDeviceService.delete(device, getCurrentUser()).get(); } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java index 8a01e9ef02..5d9737db23 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java @@ -164,7 +164,7 @@ public class EdgeController extends BaseController { accessControlService.checkPermission(getCurrentUser(), Resource.EDGE, operation, edge.getId(), edge); - return tbEdgeService.saveEdge(edge, edgeTemplateRootRuleChain, getCurrentUser()); + return tbEdgeService.save(edge, edgeTemplateRootRuleChain, getCurrentUser()); } @ApiOperation(value = "Delete edge (deleteEdge)", @@ -177,7 +177,7 @@ public class EdgeController extends BaseController { checkParameter(EDGE_ID, strEdgeId); EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); Edge edge = checkEdgeId(edgeId, Operation.DELETE); - tbEdgeService.deleteEdge(edge, getCurrentUser()); + tbEdgeService.delete(edge, getCurrentUser()); } @ApiOperation(value = "Get Tenant Edges (getEdges)", diff --git a/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java index 834eeb555d..e9999d2dc1 100644 --- a/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java @@ -66,7 +66,7 @@ public class AssetBulkImportService extends AbstractBulkImportService { @Override @SneakyThrows protected Asset saveEntity(SecurityUser user, Asset entity, Map fields) { - return tbAssetService.saveAsset(entity, user); + return tbAssetService.save(entity, user); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java index 038dcadfc8..fd55e9b321 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java @@ -76,7 +76,7 @@ public class EdgeBulkImportService extends AbstractBulkImportService { @Override protected Edge saveEntity(SecurityUser user, Edge entity, Map fields) { RuleChain edgeTemplateRootRuleChain = ruleChainService.getEdgeTemplateRootRuleChain(user.getTenantId()); - return tbEdgeService.saveEdge(entity, edgeTemplateRootRuleChain, user); + return tbEdgeService.save(entity, edgeTemplateRootRuleChain, user); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java index 2cd1ae1acc..1f32063299 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java @@ -67,14 +67,21 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS } @Override - public void notifyDeleteEntity(TenantId tenantId, I entityId, E entity, EntityId originatorId, + public void notifyDeleteEntity(TenantId tenantId, I entityId, E entity, CustomerId customerId, ActionType actionType, List relatedEdgeIds, - SecurityUser user, - String body, Object... additionalInfo) { - EntityId entityIdForLogEntityAction = originatorId != null ? originatorId : entityId; - logEntityAction(tenantId, entityIdForLogEntityAction, entity, customerId, actionType, user, additionalInfo); - sendDeleteNotificationMsg(tenantId, entityId, entity, relatedEdgeIds, body); + SecurityUser user, Object... additionalInfo) { + logEntityAction(tenantId, entityId, entity, customerId, actionType, user, additionalInfo); + sendDeleteNotificationMsg(tenantId, entityId, entity, relatedEdgeIds); + } + + public void notifyDeleteEntityAlarm(TenantId tenantId, I entityId, E entity, EntityId originatorId, + CustomerId customerId, ActionType actionType, + List relatedEdgeIds, + SecurityUser user, + String body, Object... additionalInfo) { + logEntityAction(tenantId, originatorId, entity, customerId, actionType, user, additionalInfo); + sendAlarmDeleteNotificationMsg(tenantId, entityId, entity, relatedEdgeIds, body); } @Override @@ -127,7 +134,7 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS gatewayNotificationsService.onDeviceDeleted(device); tbClusterService.onDeviceDeleted(device, null); - notifyDeleteEntity(tenantId, deviceId, device, customerId, null, ActionType.DELETED, relatedEdgeIds, user,null, additionalInfo); + notifyDeleteEntity(tenantId, deviceId, device, customerId, ActionType.DELETED, relatedEdgeIds, user,null, additionalInfo); } @Override @@ -221,8 +228,8 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS } } - protected void sendDeleteNotificationMsg(TenantId tenantId, I entityId, E entity, - List edgeIds, String body) { + protected void sendAlarmDeleteNotificationMsg(TenantId tenantId, I entityId, E entity, + List edgeIds, String body) { try { sendDeleteNotificationMsg(tenantId, entityId, edgeIds, body); } catch (Exception e) { @@ -230,6 +237,15 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS } } + protected void sendDeleteNotificationMsg(TenantId tenantId, I entityId, E entity, + List edgeIds) { + try { + sendDeleteNotificationMsg(tenantId, entityId, edgeIds, null); + } catch (Exception e) { + log.warn("Failed to push delete " + entity.getClass().getName() + " msg to core: {}", entity, e); + } + } + private void sendDeleteNotificationMsg(TenantId tenantId, EntityId entityId, List edgeIds, String body) { if (edgeIds != null && !edgeIds.isEmpty()) { for (EdgeId edgeId : edgeIds) { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java index 1e0bcf1124..93a8b66749 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java @@ -43,9 +43,15 @@ public interface TbNotificationEntityService { CustomerId customerId, ActionType actionType, SecurityUser user, Object... additionalInfo); - void notifyDeleteEntity(TenantId tenantId, I entityId, E entity, EntityId originatorId, CustomerId customerId, - ActionType actionType, List relatedEdgeIds, SecurityUser user, - String body, Object... additionalInfo); + void notifyDeleteEntity(TenantId tenantId, I entityId, E entity, + CustomerId customerId, ActionType actionType, + List relatedEdgeIds, + SecurityUser user, Object... additionalInfo); + + void notifyDeleteEntityAlarm(TenantId tenantId, I entityId, E entity, EntityId originatorId, + CustomerId customerId, ActionType actionType, + List relatedEdgeIds, + SecurityUser user, String body, Object... additionalInfo); void notifyAssignOrUnassignEntityToCustomer(TenantId tenantId, I entityId, CustomerId customerId, E entity, diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java index 809b8b4e8c..44f84a5e3b 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java @@ -77,10 +77,10 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb } @Override - public Boolean deleteAlarm(Alarm alarm, SecurityUser user) throws ThingsboardException { + public Boolean delete(Alarm alarm, SecurityUser user) throws ThingsboardException { try { List relatedEdgeIds = findRelatedEdgeIds(user.getTenantId(), alarm.getOriginator()); - notificationEntityService.notifyDeleteEntity(user.getTenantId(), alarm.getId(), alarm, alarm.getOriginator(), user.getCustomerId(), + notificationEntityService.notifyDeleteEntityAlarm(user.getTenantId(), alarm.getId(), alarm, alarm.getOriginator(), user.getCustomerId(), ActionType.DELETED, relatedEdgeIds, user, JacksonUtil.OBJECT_MAPPER.writeValueAsString(alarm)); return alarmService.deleteAlarm(user.getTenantId(), alarm.getId()).isSuccessful(); } catch (Exception e) { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java index 85bff96d47..7a6d7f6a81 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java @@ -27,5 +27,5 @@ public interface TbAlarmService { void clear(Alarm alarm, SecurityUser user) throws ThingsboardException; - Boolean deleteAlarm(Alarm alarm, SecurityUser user) throws ThingsboardException; + Boolean delete(Alarm alarm, SecurityUser user) throws ThingsboardException; } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java index e44e405273..5d1556fd7d 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java @@ -41,7 +41,7 @@ import java.util.List; public class DefaultTbAssetService extends AbstractTbEntityService implements TbAssetService { @Override - public Asset saveAsset(Asset asset, SecurityUser user) throws ThingsboardException { + public Asset save(Asset asset, SecurityUser user) throws ThingsboardException { ActionType actionType = asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED; TenantId tenantId = asset.getTenantId(); try { @@ -55,13 +55,13 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb } @Override - public ListenableFuture deleteAsset(Asset asset, SecurityUser user) throws ThingsboardException { + public ListenableFuture delete(Asset asset, SecurityUser user) throws ThingsboardException { TenantId tenantId = asset.getTenantId(); AssetId assetId = asset.getId(); try { List relatedEdgeIds = findRelatedEdgeIds(tenantId, assetId); assetService.deleteAsset(tenantId, assetId); - notificationEntityService.notifyDeleteEntity(tenantId, assetId, asset, null, asset.getCustomerId(), ActionType.DELETED, + notificationEntityService.notifyDeleteEntity(tenantId, assetId, asset, asset.getCustomerId(), ActionType.DELETED, relatedEdgeIds, user, null, asset.toString()); return removeAlarmsByEntityId(tenantId, assetId); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java index ccbd9e715e..3ffb03cb7b 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java @@ -26,9 +26,9 @@ import org.thingsboard.server.service.security.model.SecurityUser; public interface TbAssetService { - Asset saveAsset(Asset asset, SecurityUser user) throws ThingsboardException; + Asset save(Asset asset, SecurityUser user) throws ThingsboardException; - ListenableFuture deleteAsset (Asset asset, SecurityUser user) throws ThingsboardException; + ListenableFuture delete(Asset asset, SecurityUser user) throws ThingsboardException; Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, Customer customer, SecurityUser user) throws ThingsboardException; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java index b3abdd1fc6..af27daa5db 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java @@ -61,7 +61,7 @@ public class DefaultTbCustomerService extends AbstractTbEntityService implements try { List relatedEdgeIds = findRelatedEdgeIds(tenantId, customerId); customerService.deleteCustomer(tenantId, customerId); - notificationEntityService.notifyDeleteEntity(tenantId, customerId, customer, null, customerId, + notificationEntityService.notifyDeleteEntity(tenantId, customerId, customer, customerId, ActionType.DELETED, relatedEdgeIds, user, null); tbClusterService.broadcastEntityStateChangeEvent(tenantId, customerId, ComponentLifecycleEvent.DELETED); } catch (Exception e) { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java index a9c6e7dde5..5ea27f9edc 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java @@ -36,7 +36,6 @@ import org.thingsboard.server.service.security.model.SecurityUser; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.UUID; @Service @TbCoreComponent @@ -65,7 +64,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement try { List relatedEdgeIds = findRelatedEdgeIds(tenantId, dashboardId); dashboardService.deleteDashboard(tenantId, dashboardId); - notificationEntityService.notifyDeleteEntity(tenantId, dashboardId, dashboard, null, user.getCustomerId(), + notificationEntityService.notifyDeleteEntity(tenantId, dashboardId, dashboard, user.getCustomerId(), ActionType.DELETED, relatedEdgeIds, user, null); } catch (Exception e) { notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DASHBOARD), null, null, @@ -125,17 +124,10 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement } @Override - public Dashboard updateDashboardCustomers(Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException { + public Dashboard updateDashboardCustomers(Dashboard dashboard, Set customerIds, SecurityUser user) throws ThingsboardException { ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER; TenantId tenantId = user.getTenantId(); try { - Set customerIds = new HashSet<>(); - if (strCustomerIds != null) { - for (String strCustomerId : strCustomerIds) { - customerIds.add(new CustomerId(UUID.fromString(strCustomerId))); - } - } - Set addedCustomerIds = new HashSet<>(); Set removedCustomerIds = new HashSet<>(); for (CustomerId customerId : customerIds) { @@ -179,20 +171,10 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement } @Override - public Dashboard addDashboardCustomers(Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException { + public Dashboard addDashboardCustomers(Dashboard dashboard, Set customerIds, SecurityUser user) throws ThingsboardException { ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER; TenantId tenantId = user.getTenantId(); try { - Set customerIds = new HashSet<>(); - if (strCustomerIds != null) { - for (String strCustomerId : strCustomerIds) { - CustomerId customerId = new CustomerId(UUID.fromString(strCustomerId)); - if (!dashboard.isAssignedToCustomer(customerId)) { - customerIds.add(customerId); - } - } - } - if (customerIds.isEmpty()) { return dashboard; } else { @@ -213,21 +195,11 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement } @Override - public Dashboard removeDashboardCustomers(Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException { + public Dashboard removeDashboardCustomers(Dashboard dashboard, Set customerIds, SecurityUser user) throws ThingsboardException { ActionType actionType = ActionType.UNASSIGNED_FROM_CUSTOMER; TenantId tenantId = user.getTenantId(); try { - Set customerIds = new HashSet<>(); - if (strCustomerIds != null) { - for (String strCustomerId : strCustomerIds) { - CustomerId customerId = new CustomerId(UUID.fromString(strCustomerId)); - if (dashboard.isAssignedToCustomer(customerId)) { - customerIds.add(customerId); - } - } - } - - if (customerIds.isEmpty()) { + if (customerIds.isEmpty()) { return dashboard; } else { Dashboard savedDashboard = null; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java index 2930b1c10d..84dba247ff 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java @@ -19,11 +19,13 @@ import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.service.entitiy.SimpleTbEntityService; import org.thingsboard.server.service.security.model.SecurityUser; +import java.util.Set; + public interface TbDashboardService extends SimpleTbEntityService { Dashboard assignDashboardToCustomer(DashboardId dashboardId, Customer customer, SecurityUser user) throws ThingsboardException; @@ -32,11 +34,11 @@ public interface TbDashboardService extends SimpleTbEntityService { Dashboard unassignDashboardFromPublicCustomer(Dashboard dashboard, SecurityUser user) throws ThingsboardException; - Dashboard updateDashboardCustomers(Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException; + Dashboard updateDashboardCustomers(Dashboard dashboard, Set customerIds, SecurityUser user) throws ThingsboardException; - Dashboard addDashboardCustomers(Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException; + Dashboard addDashboardCustomers(Dashboard dashboard, Set customerIds, SecurityUser user) throws ThingsboardException; - Dashboard removeDashboardCustomers(Dashboard dashboard, String[] strCustomerIds, SecurityUser user) throws ThingsboardException; + Dashboard removeDashboardCustomers(Dashboard dashboard, Set customerIds, SecurityUser user) throws ThingsboardException; Dashboard asignDashboardToEdge(DashboardId dashboardId, Edge edge, SecurityUser user) throws ThingsboardException; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java index 1bed17f941..08e3a6f4cd 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java @@ -80,7 +80,7 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T } @Override - public ListenableFuture deleteDevice(Device device, SecurityUser user) throws ThingsboardException { + public ListenableFuture delete(Device device, SecurityUser user) throws ThingsboardException { TenantId tenantId = device.getTenantId(); DeviceId deviceId = device.getId(); try { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java index 16fd0c7448..74e0bcfbe0 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java @@ -35,7 +35,7 @@ public interface TbDeviceService { Device saveDeviceWithCredentials(TenantId tenantId, Device device, DeviceCredentials deviceCredentials, SecurityUser user) throws ThingsboardException; - ListenableFuture deleteDevice(Device device, SecurityUser user) throws ThingsboardException; + ListenableFuture delete(Device device, SecurityUser user) throws ThingsboardException; Device assignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, Customer customer, SecurityUser user) throws ThingsboardException; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java index 93487ac5cd..5547c63375 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java @@ -39,7 +39,7 @@ import org.thingsboard.server.service.security.model.SecurityUser; public class DefaultTbEdgeService extends AbstractTbEntityService implements TbEdgeService { @Override - public Edge saveEdge(Edge edge, RuleChain edgeTemplateRootRuleChain, SecurityUser user) throws ThingsboardException { + public Edge save(Edge edge, RuleChain edgeTemplateRootRuleChain, SecurityUser user) throws ThingsboardException { ActionType actionType = edge.getId() == null ? ActionType.ADDED : ActionType.UPDATED; TenantId tenantId = edge.getTenantId(); try { @@ -62,7 +62,7 @@ public class DefaultTbEdgeService extends AbstractTbEntityService implements TbE } @Override - public void deleteEdge(Edge edge, SecurityUser user) throws ThingsboardException { + public void delete(Edge edge, SecurityUser user) throws ThingsboardException { ActionType actionType = ActionType.DELETED; EdgeId edgeId = edge.getId(); TenantId tenantId = edge.getTenantId(); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/TbEdgeService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/TbEdgeService.java index 5ed27fbbb3..3b4fc28283 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/TbEdgeService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/TbEdgeService.java @@ -25,9 +25,9 @@ import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.service.security.model.SecurityUser; public interface TbEdgeService { - Edge saveEdge(Edge edge, RuleChain edgeTemplateRootRuleChain, SecurityUser user) throws ThingsboardException; + Edge save(Edge edge, RuleChain edgeTemplateRootRuleChain, SecurityUser user) throws ThingsboardException; - void deleteEdge(Edge edge, SecurityUser user) throws ThingsboardException; + void delete(Edge edge, SecurityUser user) throws ThingsboardException; Edge assignEdgeToCustomer(TenantId tenantId, EdgeId edgeId, Customer customer, SecurityUser user) throws ThingsboardException; From 2b8bf1972e61ab6ef696303b2a0dc5fce8dd8cf5 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 17 May 2022 14:23:44 +0300 Subject: [PATCH 169/276] UI: Fix trip animation widget label calculation. Map widgets code cleanup. --- ui-ngx/src/app/core/schema-utils.ts | 87 - .../widget/lib/maps/common-maps-utils.ts | 19 - .../widget/lib/maps/map-widget.interface.ts | 5 - .../components/widget/lib/maps/map-widget2.ts | 52 +- .../components/widget/lib/maps/schemes.ts | 1603 ----------------- .../trip-animation.component.ts | 45 +- 6 files changed, 13 insertions(+), 1798 deletions(-) delete mode 100644 ui-ngx/src/app/core/schema-utils.ts delete mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts diff --git a/ui-ngx/src/app/core/schema-utils.ts b/ui-ngx/src/app/core/schema-utils.ts deleted file mode 100644 index 39891bf550..0000000000 --- a/ui-ngx/src/app/core/schema-utils.ts +++ /dev/null @@ -1,87 +0,0 @@ -/// -/// Copyright © 2016-2022 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 { JsonSettingsSchema } from '@shared/models/widget.models'; - -export function initSchema(): JsonSettingsSchema { - return { - schema: { - type: 'object', - properties: {}, - required: [] - }, - form: [], - groupInfoes: [] - }; -} - -export function addGroupInfo(schema: JsonSettingsSchema, title: string) { - schema.groupInfoes.push({ - formIndex: schema.groupInfoes?.length || 0, - GroupTitle: title - }); -} - -export function addToSchema(schema: JsonSettingsSchema, newSchema: JsonSettingsSchema) { - Object.assign(schema.schema.properties, newSchema.schema.properties); - schema.schema.required = schema.schema.required.concat(newSchema.schema.required); - schema.form.push(newSchema.form); -} - -export function mergeSchemes(schemes: JsonSettingsSchema[]): JsonSettingsSchema { - return schemes.reduce((finalSchema: JsonSettingsSchema, schema: JsonSettingsSchema) => { - return { - schema: { - properties: { - ...finalSchema.schema.properties, - ...schema.schema.properties - }, - required: [ - ...finalSchema.schema.required, - ...schema.schema.required - ] - }, - form: [ - ...finalSchema.form, - ...schema.form - ] - } as JsonSettingsSchema; - }, initSchema()); -} - -export function addCondition(schema: JsonSettingsSchema, condition: string, exclude: string[] = []): JsonSettingsSchema { - schema.form = schema.form.map(element => { - if (!exclude.includes(element) && !exclude.includes(element.key)) { - if (typeof element === 'string') { - return { - key: element, - condition - }; - } - if (typeof element === 'object') { - if (element.condition) { - element.condition += ' && ' + condition; - } - else { - element.condition = condition; - } - } - } - return element; - }); - return schema; -} - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts index ca6532c995..a1d4e8b5b6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts @@ -28,27 +28,8 @@ import { import { Observable, Observer, of } from 'rxjs'; import { map } from 'rxjs/operators'; import { FormattedData } from '@shared/models/widget.models'; -import _ from 'lodash'; -import { mapProviderSchema, providerSets } from '@home/components/widget/lib/maps/schemes'; -import { addCondition, mergeSchemes } from '@core/schema-utils'; import L from 'leaflet'; -export function getProviderSchema(mapProvider: MapProviders, ignoreImageMap = false) { - const providerSchema = _.cloneDeep(mapProviderSchema); - if (mapProvider) { - providerSchema.schema.properties.provider.default = mapProvider; - } - if (ignoreImageMap) { - providerSchema.form[0].items = providerSchema.form[0]?.items.filter(item => item.value !== 'image-map'); - } - return mergeSchemes([providerSchema, - ...Object.keys(providerSets)?.map( - (key: string) => { - const setting = providerSets[key]; - return addCondition(setting?.schema, `model.provider === '${setting.name}'`); - })]); -} - export function getRatio(firsMoment: number, secondMoment: number, intermediateMoment: number): number { return (intermediateMoment - firsMoment) / (secondMoment - firsMoment); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.interface.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.interface.ts index 252f0f6590..05b782f14e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.interface.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.interface.ts @@ -14,8 +14,6 @@ /// limitations under the License. /// -import { JsonSettingsSchema } from '@shared/models/widget.models'; -import { MapProviders } from '@home/components/widget/lib/maps/map-models'; import LeafletMap from '@home/components/widget/lib/maps/leaflet-map'; export interface MapWidgetInterface { @@ -26,8 +24,5 @@ export interface MapWidgetInterface { } export interface MapWidgetStaticInterface { - settingsSchema(mapProvider?: MapProviders, drawRoutes?: boolean): JsonSettingsSchema; - getProvidersSchema(mapProvider?: MapProviders, ignoreImageMap?: boolean): JsonSettingsSchema; - dataKeySettingsSchema(): object; actionSources(): object; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts index bccc8f394f..dca583c130 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts @@ -14,26 +14,11 @@ /// limitations under the License. /// -import { - defaultMapSettings, - MapProviders, - UnitedMapSettings, - WidgetUnitedMapSettings -} from './map-models'; +import { defaultMapSettings, MapProviders, UnitedMapSettings, WidgetUnitedMapSettings } from './map-models'; import LeafletMap from './leaflet-map'; -import { - commonMapSettingsSchema, - editorSettingSchema, - mapCircleSchema, - mapPolygonSchema, - markerClusteringSettingsSchema, - markerClusteringSettingsSchemaLeaflet, - routeMapSettingsSchema -} from './schemes'; import { MapWidgetInterface, MapWidgetStaticInterface } from './map-widget.interface'; -import { addCondition, addGroupInfo, addToSchema, initSchema, mergeSchemes } from '@core/schema-utils'; import { WidgetContext } from '@app/modules/home/models/widget-component.models'; -import { getDefCenterPosition, getProviderSchema, parseWithTranslation } from './common-maps-utils'; +import { getDefCenterPosition, parseWithTranslation } from './common-maps-utils'; import { Datasource, DatasourceData, @@ -108,39 +93,6 @@ export class MapWidgetController implements MapWidgetInterface { settings: WidgetUnitedMapSettings; pageLink: EntityDataPageLink; - public static dataKeySettingsSchema(): object { - return {}; - } - - public static getProvidersSchema(mapProvider: MapProviders, ignoreImageMap = false) { - return getProviderSchema(mapProvider, ignoreImageMap); - } - - public static settingsSchema(mapProvider: MapProviders, drawRoutes: boolean): JsonSettingsSchema { - const schema = initSchema(); - addToSchema(schema, this.getProvidersSchema(mapProvider)); - addGroupInfo(schema, 'Map Provider Settings'); - addToSchema(schema, commonMapSettingsSchema); - addGroupInfo(schema, 'Common Map Settings'); - addToSchema(schema, addCondition(mapPolygonSchema, 'model.showPolygon === true', ['showPolygon'])); - addGroupInfo(schema, 'Polygon Settings'); - addToSchema(schema, addCondition(mapCircleSchema, 'model.showCircle === true', ['showCircle'])); - addGroupInfo(schema, 'Circle Settings'); - if (drawRoutes) { - addToSchema(schema, routeMapSettingsSchema); - addGroupInfo(schema, 'Route Map Settings'); - } else { - const clusteringSchema = mergeSchemes([markerClusteringSettingsSchema, - addCondition(markerClusteringSettingsSchemaLeaflet, - `model.useClusterMarkers === true && model.provider !== "image-map"`)]); - addToSchema(schema, clusteringSchema); - addGroupInfo(schema, 'Markers Clustering Settings'); - addToSchema(schema, addCondition(editorSettingSchema, '(model.editablePolygon === true || model.draggableMarker === true)')); - addGroupInfo(schema, 'Editor settings'); - } - return schema; - } - public static actionSources(): object { return { markerClick: { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts deleted file mode 100644 index 6d2dc3f1b4..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts +++ /dev/null @@ -1,1603 +0,0 @@ -/// -/// Copyright © 2016-2022 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 { JsonSettingsSchema } from '@shared/models/widget.models'; - -export const googleMapSettingsSchema = -{ - schema: { - title: 'Google Map Configuration', - type: 'object', - properties: { - gmApiKey: { - title: 'Google Maps API Key', - type: 'string', - default: 'AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q' - }, - gmDefaultMapType: { - title: 'Default map type', - type: 'string', - default: 'roadmap' - } - }, - required: [] - }, - form: [ - 'gmApiKey', - { - key: 'gmDefaultMapType', - type: 'rc-select', - multiple: false, - items: [ - { - value: 'roadmap', - label: 'Roadmap' - }, - { - value: 'satellite', - label: 'Satellite' - }, - { - value: 'hybrid', - label: 'Hybrid' - }, - { - value: 'terrain', - label: 'Terrain' - } - ] - } - ] -}; - -export const tencentMapSettingsSchema = -{ - schema: { - title: 'Tencent Map Configuration', - type: 'object', - properties: { - tmApiKey: { - title: 'Tencent Maps API Key', - type: 'string', - default: '84d6d83e0e51e481e50454ccbe8986b' - }, - tmDefaultMapType: { - title: 'Default map type', - type: 'string', - default: 'roadmap' - } - }, - required: [] - }, - form: [ - 'tmApiKey', - { - key: 'tmDefaultMapType', - type: 'rc-select', - multiple: false, - items: [ - { - value: 'roadmap', - label: 'Roadmap' - }, - { - value: 'satellite', - label: 'Satellite' - }, - { - value: 'hybrid', - label: 'Hybrid' - }, - ] - } - ] -}; - -export const hereMapSettingsSchema = -{ - schema: { - title: 'HERE Map Configuration', - type: 'object', - properties: { - mapProviderHere: { - title: 'Map layer', - type: 'string', - default: 'HERE.normalDay' - }, - credentials: { - type: 'object', - title: 'Credentials', - properties: { - app_id: { - title: 'HERE app id', - type: 'string', - default: 'AhM6TzD9ThyK78CT3ptx' - }, - app_code: { - title: 'HERE app code', - type: 'string', - default: 'p6NPiITB3Vv0GMUFnkLOOg' - } - }, - required: ['app_id', 'app_code'] - } - }, - required: [] - }, - form: [ - { - key: 'mapProviderHere', - type: 'rc-select', - multiple: false, - items: [ - { - value: 'HERE.normalDay', - label: 'HERE.normalDay (Default)' - }, - { - value: 'HERE.normalNight', - label: 'HERE.normalNight' - }, - { - value: 'HERE.hybridDay', - label: 'HERE.hybridDay' - }, - { - value: 'HERE.terrainDay', - label: 'HERE.terrainDay' - } - ] - }, - 'credentials' - ] -}; - -export const openstreetMapSettingsSchema = -{ - schema: { - title: 'Openstreet Map Configuration', - type: 'object', - properties: { - mapProvider: { - title: 'Map provider', - type: 'string', - default: 'OpenStreetMap.Mapnik' - }, - useCustomProvider: { - title: 'Use custom provider', - type: 'boolean', - default: false - }, - customProviderTileUrl: { - title: 'Custom provider tile URL', - type: 'string', - default: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' - } - }, - required: [] - }, - form: [ - { - key: 'mapProvider', - type: 'rc-select', - multiple: false, - items: [ - { - value: 'OpenStreetMap.Mapnik', - label: 'OpenStreetMap.Mapnik (Default)' - }, - { - value: 'OpenStreetMap.HOT', - label: 'OpenStreetMap.HOT' - }, - { - value: 'Esri.WorldStreetMap', - label: 'Esri.WorldStreetMap' - }, - { - value: 'Esri.WorldTopoMap', - label: 'Esri.WorldTopoMap' - }, - { - value: 'CartoDB.Positron', - label: 'CartoDB.Positron' - }, - { - value: 'CartoDB.DarkMatter', - label: 'CartoDB.DarkMatter' - } - ] - }, - 'useCustomProvider', - { - key: 'customProviderTileUrl', - condition: 'model.useCustomProvider === true', - } - ] -}; - -export const commonMapSettingsSchema = -{ - schema: { - title: 'Map Configuration', - type: 'object', - properties: { - defaultZoomLevel: { - title: 'Default map zoom level (0 - 20)', - type: 'number' - }, - useDefaultCenterPosition: { - title: 'Use default map center position', - type: 'boolean', - default: false - }, - mapPageSize: { - title: 'Limit of entities to load', - type: 'number', - default: 16384 - }, - defaultCenterPosition: { - title: 'Default map center position (0,0)', - type: 'string', - default: '0,0' - }, - fitMapBounds: { - title: 'Fit map bounds to cover all markers', - type: 'boolean', - default: true - }, - draggableMarker: { - title: 'Draggable Marker', - type: 'boolean', - default: false - }, - disableScrollZooming: { - title: 'Disable scroll zooming', - type: 'boolean', - default: false - }, - disableZoomControl: { - title: 'Disable zoom control buttons', - type: 'boolean', - default: false - }, - latKeyName: { - title: 'Latitude key name', - type: 'string', - default: 'latitude' - }, - lngKeyName: { - title: 'Longitude key name', - type: 'string', - default: 'longitude' - }, - xPosKeyName: { - title: 'X position key name', - type: 'string', - default: 'xPos' - }, - yPosKeyName: { - title: 'Y position key name', - type: 'string', - default: 'yPos' - }, - showLabel: { - title: 'Show label', - type: 'boolean', - default: true - }, - label: { - title: 'Label (pattern examples: \'${entityName}\', \'${entityName}: (Text ${keyName} units.)\' )', - type: 'string', - default: '${entityName}' - }, - useLabelFunction: { - title: 'Use label function', - type: 'boolean', - default: false - }, - labelFunction: { - title: 'Label function: f(data, dsData, dsIndex)', - type: 'string' - }, - showTooltip: { - title: 'Show tooltip', - type: 'boolean', - default: true - }, - showTooltipAction: { - title: 'Action for displaying the tooltip', - type: 'string', - default: 'click' - }, - autocloseTooltip: { - title: 'Auto-close tooltips', - type: 'boolean', - default: true - }, - tooltipPattern: { - title: 'Tooltip (for ex. \'Text ${keyName} units.\' or Link text\')', - type: 'string', - default: '${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}' - }, - useTooltipFunction: { - title: 'Use tooltip function', - type: 'boolean', - default: false - }, - tooltipFunction: { - title: 'Tooltip function: f(data, dsData, dsIndex)', - type: 'string' - }, - posFunction: { - title: 'Position conversion function: f(origXPos, origYPos), should return x,y coordinates as double from 0 to 1 each', - type: 'string', - default: 'return {x: origXPos, y: origYPos};' - }, - markerOffsetX: { - title: 'Marker X offset relative to position multiplied by marker width', - type: 'number', - default: 0.5 - }, - markerOffsetY: { - title: 'Marker Y offset relative to position multiplied by marker height', - type: 'number', - default: 1 - }, - tooltipOffsetX: { - title: 'Tooltip X offset relative to marker anchor multiplied by marker width', - type: 'number', - default: 0 - }, - tooltipOffsetY: { - title: 'Tooltip Y offset relative to marker anchor multiplied by marker height', - type: 'number', - default: -1 - }, - color: { - title: 'Color', - type: 'string' - }, - useColorFunction: { - title: 'Use color function', - type: 'boolean', - default: false - }, - colorFunction: { - title: 'Color function: f(data, dsData, dsIndex)', - type: 'string' - }, - markerImage: { - title: 'Custom marker image', - type: 'string' - }, - markerImageSize: { - title: 'Custom marker image size (px)', - type: 'number', - default: 34 - }, - useMarkerImageFunction: { - title: 'Use marker image function', - type: 'boolean', - default: false - }, - markerImageFunction: { - title: 'Marker image function: f(data, images, dsData, dsIndex)', - type: 'string' - }, - markerImages: { - title: 'Marker images', - type: 'array', - items: { - title: 'Marker image', - type: 'string' - } - } - }, - required: [] - }, - form: [ - { - key: 'defaultZoomLevel', - condition: 'model.provider !== "image-map"' - }, - { - key: 'useDefaultCenterPosition', - condition: 'model.provider !== "image-map"' - }, - { - key: 'defaultCenterPosition', - condition: 'model.provider !== "image-map"' - }, - { - key: 'fitMapBounds', - condition: 'model.provider !== "image-map"' - }, - 'mapPageSize', - 'draggableMarker', - 'disableScrollZooming', - 'disableZoomControl', - { - key: 'latKeyName', - condition: 'model.provider !== "image-map"' - }, - { - key: 'lngKeyName', - condition: 'model.provider !== "image-map"' - }, - { - key: 'xPosKeyName', - condition: 'model.provider === "image-map"' - }, - { - key: 'yPosKeyName', - condition: 'model.provider === "image-map"' - }, - 'showLabel', - { - key: 'useLabelFunction', - condition: 'model.showLabel === true' - }, - { - key: 'label', - condition: 'model.showLabel === true && model.useLabelFunction !== true' - }, - { - key: 'labelFunction', - type: 'javascript', - helpId: 'widget/lib/map/label_fn', - condition: 'model.showLabel === true && model.useLabelFunction === true' - }, - 'showTooltip', - { - key: 'showTooltipAction', - type: 'rc-select', - multiple: false, - items: [ - { - value: 'click', - label: 'Show tooltip on click (Default)' - }, - { - value: 'hover', - label: 'Show tooltip on hover' - } - ], - condition: 'model.showTooltip === true' - }, - { - key: 'autocloseTooltip', - condition: 'model.showTooltip === true' - }, - { - key: 'useTooltipFunction', - condition: 'model.showTooltip === true' - }, - { - key: 'tooltipPattern', - type: 'textarea', - condition: 'model.showTooltip === true && model.useTooltipFunction !== true' - }, - { - key: 'tooltipFunction', - type: 'javascript', - helpId: 'widget/lib/map/tooltip_fn', - condition: 'model.showTooltip === true && model.useTooltipFunction === true' - }, - { - key: 'tooltipOffsetX', - condition: 'model.showTooltip === true' - }, - { - key: 'tooltipOffsetY', - condition: 'model.showTooltip === true' - }, - 'markerOffsetX', - 'markerOffsetY', - { - key: 'posFunction', - type: 'javascript', - helpId: 'widget/lib/map/position_fn', - condition: 'model.provider === "image-map"' - }, - { - key: 'color', - type: 'color' - }, - 'useColorFunction', - { - key: 'colorFunction', - type: 'javascript', - helpId: 'widget/lib/map/color_fn', - condition: 'model.useColorFunction === true' - }, - 'useMarkerImageFunction', - { - key: 'markerImage', - type: 'image', - condition: 'model.useMarkerImageFunction !== true' - }, - { - key: 'markerImageSize', - condition: 'model.useMarkerImageFunction !== true' - }, - { - key: 'markerImageFunction', - type: 'javascript', - helpId: 'widget/lib/map/marker_image_fn', - condition: 'model.useMarkerImageFunction === true' - }, - { - key: 'markerImages', - items: [ - { - key: 'markerImages[]', - type: 'image' - } - ], - condition: 'model.useMarkerImageFunction === true' - } - ] -}; - -export const mapPolygonSchema = -{ - schema: { - title: 'Map Polygon Configuration', - type: 'object', - properties: { - showPolygon: { - title: 'Show polygon', - type: 'boolean', - default: false - }, - polygonKeyName: { - title: 'Polygon key name', - type: 'string', - default: 'perimeter' - }, - editablePolygon: { - title: 'Enable polygon edit', - type: 'boolean', - default: false - }, - showPolygonLabel: { - title: 'Show polygon label', - type: 'boolean', - default: false - }, - polygonLabel: { - title: 'Polygon label (pattern examples: \'${entityName}\', \'${entityName}: (Text ${keyName} units.)\' )', - type: 'string', - default: '${entityName}' - }, - usePolygonLabelFunction: { - title: 'Use polygon label function', - type: 'boolean', - default: false - }, - polygonLabelFunction: { - title: 'Polygon label function: f(data, dsData, dsIndex)', - type: 'string' - }, - polygonColor: { - title: 'Polygon color', - type: 'string' - }, - polygonOpacity: { - title: 'Polygon opacity', - type: 'number', - default: 0.2 - }, - polygonStrokeColor: { - title: 'Stroke color', - type: 'string' - }, - polygonStrokeOpacity: { - title: 'Stroke opacity', - type: 'number', - default: 1 - }, - polygonStrokeWeight: { - title: 'Stroke weight', - type: 'number', - default: 3 - }, - showPolygonTooltip: { - title: 'Show polygon tooltip', - type: 'boolean', - default: false - }, - showPolygonTooltipAction: { - title: 'Action for displaying polygon tooltip', - type: 'string', - default: 'click' - }, - autoClosePolygonTooltip: { - title: 'Auto-close polygon tooltips', - type: 'boolean', - default: true - }, - polygonTooltipPattern: { - title: 'Tooltip (for ex. \'Text ${keyName} units.\' or Link text\')', - type: 'string', - default: '${entityName}

TimeStamp: ${ts:7}' - }, - usePolygonTooltipFunction: { - title: 'Use polygon tooltip function', - type: 'boolean', - default: false - }, - polygonTooltipFunction: { - title: 'Polygon tooltip function: f(data, dsData, dsIndex)', - type: 'string' - }, - usePolygonColorFunction: { - title: 'Use polygon color function', - type: 'boolean', - default: false - }, - polygonColorFunction: { - title: 'Polygon Color function: f(data, dsData, dsIndex)', - type: 'string' - }, - usePolygonStrokeColorFunction: { - title: 'Use polygon stroke color function', - type: 'boolean', - default: false - }, - polygonStrokeColorFunction: { - title: 'Polygon Stroke Color function: f(data, dsData, dsIndex)', - type: 'string' - } - }, - required: [] - }, - form: [ - 'showPolygon', - 'polygonKeyName', - 'editablePolygon', - 'showPolygonLabel', - { - key: 'usePolygonLabelFunction', - condition: 'model.showPolygonLabel === true' - }, - { - key: 'polygonLabel', - condition: 'model.showPolygonLabel === true && model.usePolygonLabelFunction !== true' - }, - { - key: 'polygonLabelFunction', - type: 'javascript', - helpId: 'widget/lib/map/label_fn', - condition: 'model.showPolygonLabel === true && model.usePolygonLabelFunction === true' - }, - { - key: 'polygonColor', - type: 'color' - }, - 'usePolygonColorFunction', - { - key: 'polygonColorFunction', - helpId: 'widget/lib/map/polygon_color_fn', - type: 'javascript', - condition: 'model.usePolygonColorFunction === true' - }, - 'polygonOpacity', - { - key: 'polygonStrokeColor', - type: 'color' - }, - 'usePolygonStrokeColorFunction', - { - key: 'polygonStrokeColorFunction', - helpId: 'widget/lib/map/polygon_color_fn', - type: 'javascript', - condition: 'model.usePolygonStrokeColorFunction === true' - }, - 'polygonStrokeOpacity', - 'polygonStrokeWeight', - 'showPolygonTooltip', - { - key: 'showPolygonTooltipAction', - type: 'rc-select', - multiple: false, - items: [ - { - value: 'click', - label: 'Show tooltip on click (Default)' - }, - { - value: 'hover', - label: 'Show tooltip on hover' - } - ], - condition: 'model.showPolygonTooltip === true' - }, - { - key: 'autoClosePolygonTooltip', - condition: 'model.showPolygonTooltip === true' - }, - { - key: 'usePolygonTooltipFunction', - condition: 'model.showPolygonTooltip === true' - }, - { - key: 'polygonTooltipPattern', - type: 'textarea', - condition: 'model.showPolygonTooltip === true && model.usePolygonTooltipFunction !== true' - }, - { - key: 'polygonTooltipFunction', - helpId: 'widget/lib/map/polygon_tooltip_fn', - type: 'javascript', - condition: 'model.showPolygonTooltip === true && model.usePolygonTooltipFunction === true' - } - ] -}; - -export const routeMapSettingsSchema = -{ - schema: { - title: 'Route Map Configuration', - type: 'object', - properties: { - strokeWeight: { - title: 'Stroke weight', - type: 'number', - default: 2 - }, - strokeOpacity: { - title: 'Stroke opacity', - type: 'number', - default: 1.0 - } - }, - required: [] - }, - form: [ - 'strokeWeight', - 'strokeOpacity' - ] -}; - -export const markerClusteringSettingsSchema = -{ - schema: { - title: 'Markers Clustering Configuration', - type: 'object', - properties: { - useClusterMarkers: { - title: 'Use map markers clustering', - type: 'boolean', - default: false - } - }, - required: [] - }, - form: [ - { - key: 'useClusterMarkers', - condition: 'model.provider !== "image-map"' - }, - ] -}; - -export const markerClusteringSettingsSchemaLeaflet = -{ - schema: { - title: 'Markers Clustering Configuration Leaflet', - type: 'object', - properties: { - zoomOnClick: { - title: 'Zoom when clicking on a cluster', - type: 'boolean', - default: true - }, - maxZoom: { - title: 'The maximum zoom level when a marker can be part of a cluster (0 - 18)', - type: 'number' - }, - showCoverageOnHover: { - title: 'Show the bounds of markers when mouse over a cluster', - type: 'boolean', - default: true - }, - animate: { - title: 'Show animation on markers when zooming', - type: 'boolean', - default: true - }, - maxClusterRadius: { - title: 'Maximum radius that a cluster will cover in pixels', - type: 'number', - default: 80 - }, - spiderfyOnMaxZoom: { - title: 'Spiderfy at the max zoom level (to see all cluster markers)', - type: 'boolean', - default: false - }, - chunkedLoading: { - title: 'Use chunks for adding markers so that the page does not freeze', - type: 'boolean', - default: false - }, - removeOutsideVisibleBounds: { - title: 'Use lazy load for adding markers', - type: 'boolean', - default: true - } - }, - required: [] - }, - form: [ - 'zoomOnClick', - 'maxZoom', - 'showCoverageOnHover', - 'animate', - 'maxClusterRadius', - 'spiderfyOnMaxZoom', - 'chunkedLoading', - 'removeOutsideVisibleBounds' - ] -}; - -export const imageMapSettingsSchema = -{ - schema: { - title: 'Image Map Configuration', - type: 'object', - properties: { - mapImageUrl: { - title: 'Image map background', - type: 'string', - default: 'data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==' - }, - imageEntityAlias: { - title: 'Image URL source entity alias', - type: 'string', - default: '' - }, - imageUrlAttribute: { - title: 'Image URL source entity attribute', - type: 'string', - default: '' - } - }, - required: [] - }, - form: [ - { - key: 'mapImageUrl', - type: 'image' - }, - 'imageEntityAlias', - 'imageUrlAttribute' - ] -}; - -export const pathSchema = -{ - schema: { - title: 'Trip Animation Path Configuration', - type: 'object', - properties: { - color: { - title: 'Path color', - type: 'string' - }, - strokeWeight: { - title: 'Stroke weight', - type: 'number', - default: 2 - }, - strokeOpacity: { - title: 'Stroke opacity', - type: 'number', - default: 1 - }, - useColorFunction: { - title: 'Use path color function', - type: 'boolean', - default: false - }, - colorFunction: { - title: 'Path color function: f(data, dsData, dsIndex)', - type: 'string' - }, - usePolylineDecorator: { - title: 'Use path decorator', - type: 'boolean', - default: false - }, - decoratorSymbol: { - title: 'Decorator symbol', - type: 'string', - default: 'arrowHead' - }, - decoratorSymbolSize: { - title: 'Decorator symbol size (px)', - type: 'number', - default: 10 - }, - useDecoratorCustomColor: { - title: 'Use path decorator custom color', - type: 'boolean', - default: false - }, - decoratorCustomColor: { - title: 'Decorator custom color', - type: 'string', - default: '#000' - }, - decoratorOffset: { - title: 'Decorator offset', - type: 'string', - default: '20px' - }, - endDecoratorOffset: { - title: 'End decorator offset', - type: 'string', - default: '20px' - }, - decoratorRepeat: { - title: 'Decorator repeat', - type: 'string', - default: '20px' - } - }, - required: [] - }, - form: [ - { - key: 'color', - type: 'color' - }, - 'useColorFunction', - { - key: 'colorFunction', - helpId: 'widget/lib/map/path_color_fn', - type: 'javascript', - condition: 'model.useColorFunction === true' - }, - 'strokeWeight', - 'strokeOpacity', - 'usePolylineDecorator', - { - key: 'decoratorSymbol', - type: 'rc-select', - multiple: false, - items: [{ - value: 'arrowHead', - label: 'Arrow' - }, { - value: 'dash', - label: 'Dash' - }] - }, - 'decoratorSymbolSize', - 'useDecoratorCustomColor', - { - key: 'decoratorCustomColor', - type: 'color' - }, - { - key: 'decoratorOffset', - type: 'textarea' - }, - { - key: 'endDecoratorOffset', - type: 'textarea' - }, - { - key: 'decoratorRepeat', - type: 'textarea' - } - ] -}; - -export const pointSchema = -{ - schema: { - title: 'Trip Animation Points Configuration', - type: 'object', - properties: { - showPoints: { - title: 'Show points', - type: 'boolean', - default: false - }, - pointColor: { - title: 'Point color', - type: 'string' - }, - useColorPointFunction: { - title: 'Use color point function', - type: 'boolean', - default: false - }, - colorPointFunction: { - title: 'Color point function: f(data, dsData, dsIndex)', - type: 'string' - }, - pointSize: { - title: 'Point size (px)', - type: 'number', - default: 10 - }, - usePointAsAnchor: { - title: 'Use point as anchor', - type: 'boolean', - default: false - }, - pointAsAnchorFunction: { - title: 'Point as anchor function: f(data, dsData, dsIndex)', - type: 'string' - }, - pointTooltipOnRightPanel: { - title: 'Independant point tooltip', - type: 'boolean', - default: true - }, - }, - required: [] - }, - form: [ - 'showPoints', - { - key: 'pointColor', - type: 'color' - }, - 'useColorPointFunction', - { - key: 'colorPointFunction', - helpId: 'widget/lib/map/path_point_color_fn', - type: 'javascript', - condition: 'model.useColorPointFunction === true' - }, - 'pointSize', - 'usePointAsAnchor', - { - key: 'pointAsAnchorFunction', - helpId: 'widget/lib/map/trip_point_as_anchor_fn', - type: 'javascript', - condition: 'model.usePointAsAnchor === true' - }, - 'pointTooltipOnRightPanel', - ] -}; - -export const mapProviderSchema = -{ - schema: { - title: 'Map Provider Configuration', - type: 'object', - properties: { - provider: { - title: 'Map Provider', - type: 'string', - default: 'openstreet-map' - } - }, - required: [] - }, - form: [ - { - key: 'provider', - type: 'rc-select', - multiple: false, - items: [ - { - value: 'google-map', - label: 'Google maps' - }, - { - value: 'openstreet-map', - label: 'Openstreet maps' - }, - { - value: 'here', - label: 'HERE maps' - }, - { - value: 'image-map', - label: 'Image map' - }, - { - value: 'tencent-map', - label: 'Tencent maps' - } - ] - } - ] -}; - -export const tripAnimationSchema = { - schema: { - title: 'Openstreet Map Configuration', - type: 'object', - properties: { - normalizationStep: { - title: 'Normalization data step (ms)', - type: 'number', - default: 1000 - }, - latKeyName: { - title: 'Latitude key name', - type: 'string', - default: 'latitude' - }, - lngKeyName: { - title: 'Longitude key name', - type: 'string', - default: 'longitude' - }, - showLabel: { - title: 'Show label', - type: 'boolean', - default: true - }, - label: { - title: 'Label (pattern examples: \'${entityName}\', \'${entityName}: (Text ${keyName} units.)\' )', - type: 'string', - default: '${entityName}' - }, - useLabelFunction: { - title: 'Use label function', - type: 'boolean', - default: false - }, - labelFunction: { - title: 'Label function: f(data, dsData, dsIndex)', - type: 'string' - }, - showTooltip: { - title: 'Show tooltip', - type: 'boolean', - default: true - }, - tooltipColor: { - title: 'Tooltip background color', - type: 'string', - default: '#fff' - }, - tooltipFontColor: { - title: 'Tooltip font color', - type: 'string', - default: '#000' - }, - tooltipOpacity: { - title: 'Tooltip opacity (0-1)', - type: 'number', - default: 1 - }, - tooltipPattern: { - title: 'Tooltip (for ex. \'Text ${keyName} units.\' or Link text\')', - type: 'string', - default: '${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}' - }, - useTooltipFunction: { - title: 'Use tooltip function', - type: 'boolean', - default: false - }, - tooltipFunction: { - title: 'Tooltip function: f(data, dsData, dsIndex)', - type: 'string' - }, - autocloseTooltip: { - title: 'Auto-close point popup', - type: 'boolean', - default: true - }, - markerImage: { - title: 'Custom marker image', - type: 'string' - }, - markerImageSize: { - title: 'Custom marker image size (px)', - type: 'number', - default: 34 - }, - rotationAngle: { - title: 'Set additional rotation angle for marker (deg)', - type: 'number', - default: 0 - }, - useMarkerImageFunction: { - title: 'Use marker image function', - type: 'boolean', - default: false - }, - markerImageFunction: { - title: 'Marker image function: f(data, images, dsData, dsIndex)', - type: 'string' - }, - markerImages: { - title: 'Marker images', - type: 'array', - items: { - title: 'Marker image', - type: 'string' - } - } - }, - required: [] - }, - form: [ - 'normalizationStep', - 'latKeyName', - 'lngKeyName', - 'showLabel', - { - key: 'useLabelFunction', - condition: 'model.showLabel === true' - }, - { - key: 'label', - condition: 'model.showLabel === true && model.useLabelFunction !== true' - }, - { - key: 'labelFunction', - type: 'javascript', - helpId: 'widget/lib/map/label_fn', - condition: 'model.showLabel === true && model.useLabelFunction === true' - }, - 'showTooltip', - { - key: 'tooltipColor', - type: 'color', - condition: 'model.showTooltip === true' - }, - { - key: 'tooltipFontColor', - type: 'color', - condition: 'model.showTooltip === true' - }, - { - key: 'tooltipOpacity', - condition: 'model.showTooltip === true' - }, - { - key: 'autocloseTooltip', - condition: 'model.showTooltip === true' - }, - { - key: 'useTooltipFunction', - condition: 'model.showTooltip === true', - }, - { - key: 'tooltipPattern', - type: 'textarea', - condition: 'model.showTooltip === true && model.useTooltipFunction !== true' - }, - { - key: 'tooltipFunction', - type: 'javascript', - helpId: 'widget/lib/map/tooltip_fn', - condition: 'model.showTooltip === true && model.useTooltipFunction === true' - }, - 'rotationAngle', - 'useMarkerImageFunction', - { - key: 'markerImage', - type: 'image', - condition: 'model.useMarkerImageFunction !== true' - }, - { - key: 'markerImageSize', - condition: 'model.useMarkerImageFunction !== true' - }, - { - key: 'markerImageFunction', - type: 'javascript', - helpId: 'widget/lib/map/marker_image_fn', - condition: 'model.useMarkerImageFunction === true' - }, - { - key: 'markerImages', - items: [ - { - key: 'markerImages[]', - type: 'image' - } - ], - condition: 'model.useMarkerImageFunction === true' - }] -}; - -interface IProvider { - schema: JsonSettingsSchema; - name: string; -} - -export const providerSets: { [key: string]: IProvider } = { - 'openstreet-map': { - schema: openstreetMapSettingsSchema, - name: 'openstreet-map' - }, - 'tencent-map': { - schema: tencentMapSettingsSchema, - name: 'tencent-map' - }, - 'google-map': { - schema: googleMapSettingsSchema, - name: 'google-map' - }, - here: { - schema: hereMapSettingsSchema, - name: 'here' - }, - 'image-map': { - schema: imageMapSettingsSchema, - name: 'image-map' - } -}; - -export const editorSettingSchema = - { - schema: { - title: 'Editor settings', - type: 'object', - properties: { - snappable: { - title: 'Enable snapping to other vertices for precision drawing', - type: 'boolean', - default: false - }, - initDragMode: { - title: 'Initialize map in draggable mode', - type: 'boolean', - default: false - }, - hideAllControlButton: { - title: 'Hide all edit control buttons', - type: 'boolean', - default: false - }, - hideDrawControlButton: { - title: 'Hide draw buttons', - type: 'boolean', - default: false - }, - hideEditControlButton: { - title: 'Hide edit buttons', - type: 'boolean', - default: false - }, - hideRemoveControlButton: { - title: 'Hide remove button', - type: 'boolean', - default: false - }, - }, - required: [] - }, - form: [ - 'snappable', - 'initDragMode', - 'hideAllControlButton', - { - key: 'hideDrawControlButton', - condition: 'model.hideAllControlButton == false' - }, - { - key: 'hideEditControlButton', - condition: 'model.hideAllControlButton == false' - }, - { - key: 'hideRemoveControlButton', - condition: 'model.hideAllControlButton == false' - } - ] - }; - -export const mapCircleSchema = - { - schema: { - title: 'Map Circle Configuration', - type: 'object', - properties: { - showCircle: { - title: 'Show circle', - type: 'boolean', - default: false - }, - circleKeyName: { - title: 'Circle key name', - type: 'string', - default: 'perimeter' - }, - editableCircle: { - title: 'Enable circle edit', - type: 'boolean', - default: false - }, - showCircleLabel: { - title: 'Show circle label', - type: 'boolean', - default: false - }, - circleLabel: { - title: 'Circle label (pattern examples: \'${entityName}\', \'${entityName}: (Text ${keyName} units.)\' )', - type: 'string', - default: '${entityName}' - }, - useCircleLabelFunction: { - title: 'Use circle label function', - type: 'boolean', - default: false - }, - circleLabelFunction: { - title: 'Circle label function: f(data, dsData, dsIndex)', - type: 'string' - }, - circleFillColor: { - title: 'Circle fill color', - type: 'string' - }, - useCircleFillColorFunction: { - title: 'Use circle fill color function', - type: 'boolean', - default: false - }, - circleFillColorFunction: { - title: 'Circle fill color function: f(data, dsData, dsIndex)', - type: 'string' - }, - circleFillColorOpacity: { - title: 'Circle fill color opacity', - type: 'number', - default: 0.2 - }, - circleStrokeColor: { - title: 'Circle stroke color', - type: 'string' - }, - useCircleStrokeColorFunction: { - title: 'Use circle stroke color function', - type: 'boolean', - default: false - }, - circleStrokeColorFunction: { - title: 'Circle stroke Color function: f(data, dsData, dsIndex)', - type: 'string' - }, - circleStrokeOpacity: { - title: 'Circle stroke opacity', - type: 'number', - default: 1 - }, - circleStrokeWeight: { - title: 'Circle stroke weight', - type: 'number', - default: 3 - }, - showCircleTooltip: { - title: 'Show circle tooltip', - type: 'boolean', - default: false - }, - showCircleTooltipAction: { - title: 'Action for displaying circle tooltip', - type: 'string', - default: 'click' - }, - autoCloseCircleTooltip: { - title: 'Auto-close circle tooltips', - type: 'boolean', - default: true - }, - circleTooltipPattern: { - title: 'Tooltip (for ex. \'Text ${keyName} units.\' or Link text\')', - type: 'string', - default: '${entityName}

Temperatur: ${temp:1}' - }, - useCircleTooltipFunction: { - title: 'Use circle tooltip function', - type: 'boolean', - default: false - }, - circleTooltipFunction: { - title: 'Circle tooltip function: f(data, dsData, dsIndex)', - type: 'string' - } - }, - required: [] - }, - form: [ - 'showCircle', - 'circleKeyName', - 'editableCircle', - 'showCircleLabel', - { - key: 'useCircleLabelFunction', - condition: 'model.showCircleLabel === true' - }, - { - key: 'circleLabel', - condition: 'model.showCircleLabel === true && model.useCircleLabelFunction !== true' - }, - { - key: 'circleLabelFunction', - type: 'javascript', - helpId: 'widget/lib/map/label_fn', - condition: 'model.showCircleLabel === true && model.useCircleLabelFunction === true' - }, - { - key: 'circleFillColor', - type: 'color' - }, - 'useCircleFillColorFunction', - { - key: 'circleFillColorFunction', - helpId: 'widget/lib/map/polygon_color_fn', - type: 'javascript', - condition: 'model.useCircleFillColorFunction === true' - }, - 'circleFillColorOpacity', - { - key: 'circleStrokeColor', - type: 'color' - }, - 'useCircleStrokeColorFunction', - { - key: 'circleStrokeColorFunction', - helpId: 'widget/lib/map/polygon_color_fn', - type: 'javascript', - condition: 'model.useCircleStrokeColorFunction === true' - }, - 'circleStrokeOpacity', - 'circleStrokeWeight', - 'showCircleTooltip', - { - key: 'showCircleTooltipAction', - type: 'rc-select', - multiple: false, - items: [ - { - value: 'click', - label: 'Show tooltip on click (Default)' - }, - { - value: 'hover', - label: 'Show tooltip on hover' - } - ], - condition: 'model.showCircleTooltip === true' - }, - { - key: 'autoCloseCircleTooltip', - condition: 'model.showCircleTooltip === true' - }, - { - key: 'useCircleTooltipFunction', - condition: 'model.showCircleTooltip === true' - }, - { - key: 'circleTooltipPattern', - type: 'textarea', - condition: 'model.showCircleTooltip === true && model.useCircleTooltipFunction !== true' - }, - { - key: 'circleTooltipFunction', - helpId: 'widget/lib/map/polygon_tooltip_fn', - type: 'javascript', - condition: 'model.showCircleTooltip === true && model.useCircleTooltipFunction === true' - } - ] - }; diff --git a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts b/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts index 524e7f8ca5..7320b2aeac 100644 --- a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts @@ -32,30 +32,22 @@ import { MapProviders, WidgetUnitedTripAnimationSettings } from '@home/components/widget/lib/maps/map-models'; -import { addCondition, addGroupInfo, addToSchema, initSchema } from '@app/core/schema-utils'; -import { - mapCircleSchema, - mapPolygonSchema, - pathSchema, - pointSchema, - tripAnimationSchema -} from '@home/components/widget/lib/maps/schemes'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { WidgetContext } from '@app/modules/home/models/widget-component.models'; import { findAngle, - getProviderSchema, getRatio, interpolateOnLineSegment, parseWithTranslation } from '@home/components/widget/lib/maps/common-maps-utils'; -import { FormattedData, JsonSettingsSchema, WidgetConfig } from '@shared/models/widget.models'; +import { FormattedData, WidgetConfig } from '@shared/models/widget.models'; import moment from 'moment'; import { - deepClone, - formattedDataArrayFromDatasourceData, formattedDataFormDatasourceData, + formattedDataArrayFromDatasourceData, + formattedDataFormDatasourceData, isDefined, - isUndefined, mergeFormattedData, + isUndefined, + mergeFormattedData, parseFunction, safeExecute } from '@core/utils'; @@ -101,23 +93,6 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy useAnchors: boolean; currentTime: number; - static getSettingsSchema(): JsonSettingsSchema { - const schema = initSchema(); - addToSchema(schema, getProviderSchema(null, true)); - addGroupInfo(schema, 'Map Provider Settings'); - addToSchema(schema, tripAnimationSchema); - addGroupInfo(schema, 'Trip Animation Settings'); - addToSchema(schema, pathSchema); - addGroupInfo(schema, 'Path Settings'); - addToSchema(schema, addCondition(pointSchema, 'model.showPoints === true', ['showPoints'])); - addGroupInfo(schema, 'Path Points Settings'); - addToSchema(schema, addCondition(mapPolygonSchema, 'model.showPolygon === true', ['showPolygon'])); - addGroupInfo(schema, 'Polygon Settings'); - addToSchema(schema, addCondition(mapCircleSchema, 'model.showCircle === true', ['showCircle'])); - addGroupInfo(schema, 'Circle Settings'); - return schema; - } - ngOnInit(): void { this.widgetConfig = this.ctx.widgetConfig; this.settings = { @@ -286,10 +261,12 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy } calcLabel(points: FormattedData[]) { - const data = points[this.activeTrip.dsIndex]; - const labelText: string = this.settings.useLabelFunction ? - safeExecute(this.settings.parsedLabelFunction, [data, points, data.dsIndex]) : this.settings.label; - this.label = this.sanitizer.bypassSecurityTrustHtml(parseWithTranslation.parseTemplate(labelText, data, true)); + if (this.activeTrip) { + const data = points[this.activeTrip.dsIndex]; + const labelText: string = this.settings.useLabelFunction ? + safeExecute(this.settings.parsedLabelFunction, [data, points, data.dsIndex]) : this.settings.label; + this.label = this.sanitizer.bypassSecurityTrustHtml(parseWithTranslation.parseTemplate(labelText, data, true)); + } } private interpolateArray(originData: FormattedData[]): {[time: number]: FormattedData} { From a80e83cb1297f3a486fe8114dd38f36728e8c46b Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 17 May 2022 17:38:30 +0300 Subject: [PATCH 170/276] UI: Add security page for user --- .../http/two-factor-authentication.service.ts | 13 +- .../app/modules/home/models/services.map.ts | 4 +- .../modules/home/pages/home-pages.module.ts | 2 + .../email-auth-dialog.component.html | 104 ----------- .../sms-auth-dialog.component.html | 105 ----------- .../sms-auth-dialog.component.scss | 15 -- .../totp-auth-dialog.component.html | 97 ---------- .../totp-auth-dialog.component.scss | 15 -- .../pages/profile/profile-routing.module.ts | 19 +- .../home/pages/profile/profile.component.html | 99 ++-------- .../home/pages/profile/profile.component.scss | 23 +-- .../home/pages/profile/profile.component.ts | 99 +--------- .../home/pages/profile/profile.module.ts | 8 +- .../authentication-dialog.component.scss | 77 ++++++++ .../authentication-dialog.map.ts | 29 +++ .../email-auth-dialog.component.html | 102 +++++++++++ .../email-auth-dialog.component.ts | 40 ++-- .../sms-auth-dialog.component.html | 105 +++++++++++ .../sms-auth-dialog.component.ts | 40 ++-- .../totp-auth-dialog.component.html | 92 ++++++++++ .../totp-auth-dialog.component.ts | 17 +- .../pages/security/security-routing.module.ts | 84 +++++++++ .../pages/security/security.component.html | 80 ++++++++ .../pages/security/security.component.scss | 115 ++++++++++++ .../home/pages/security/security.component.ts | 171 ++++++++++++++++++ .../home/pages/security/security.module.ts | 39 ++++ .../components/user-menu.component.html | 4 + .../shared/components/user-menu.component.ts | 4 + .../shared/models/two-factor-auth.models.ts | 12 +- .../assets/locale/locale.constant-en_US.json | 100 +++++++--- 30 files changed, 1088 insertions(+), 626 deletions(-) delete mode 100644 ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.html delete mode 100644 ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.html delete mode 100644 ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.scss delete mode 100644 ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.html delete mode 100644 ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/security/authentication-dialog/authentication-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/security/authentication-dialog/authentication-dialog.map.ts create mode 100644 ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.html rename ui-ngx/src/app/modules/home/pages/{profile => security}/authentication-dialog/email-auth-dialog.component.ts (71%) create mode 100644 ui-ngx/src/app/modules/home/pages/security/authentication-dialog/sms-auth-dialog.component.html rename ui-ngx/src/app/modules/home/pages/{profile => security}/authentication-dialog/sms-auth-dialog.component.ts (71%) create mode 100644 ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.html rename ui-ngx/src/app/modules/home/pages/{profile => security}/authentication-dialog/totp-auth-dialog.component.ts (85%) create mode 100644 ui-ngx/src/app/modules/home/pages/security/security-routing.module.ts create mode 100644 ui-ngx/src/app/modules/home/pages/security/security.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/security/security.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/security/security.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/security/security.module.ts diff --git a/ui-ngx/src/app/core/http/two-factor-authentication.service.ts b/ui-ngx/src/app/core/http/two-factor-authentication.service.ts index 36183db9d8..c2a7a5235d 100644 --- a/ui-ngx/src/app/core/http/two-factor-authentication.service.ts +++ b/ui-ngx/src/app/core/http/two-factor-authentication.service.ts @@ -39,8 +39,8 @@ export class TwoFactorAuthenticationService { return this.http.get(`/api/2fa/settings`, defaultHttpOptionsFromConfig(config)); } - saveTwoFaSettings(settings: TwoFactorAuthSettings, config?: RequestConfig): Observable { - return this.http.post(`/api/2fa/settings`, settings, defaultHttpOptionsFromConfig(config)); + saveTwoFaSettings(settings: TwoFactorAuthSettings, config?: RequestConfig): Observable { + return this.http.post(`/api/2fa/settings`, settings, defaultHttpOptionsFromConfig(config)); } getAvailableTwoFaProviders(config?: RequestConfig): Observable> { @@ -67,8 +67,9 @@ export class TwoFactorAuthenticationService { } verifyAndSaveTwoFaAccountConfig(authConfig: TwoFactorAuthAccountConfig, verificationCode: number, - config?: RequestConfig): Observable { - return this.http.post(`/api/2fa/account/config?verificationCode=${verificationCode}`, authConfig, defaultHttpOptionsFromConfig(config)); + config?: RequestConfig): Observable { + return this.http.post(`/api/2fa/account/config?verificationCode=${verificationCode}`, + authConfig, defaultHttpOptionsFromConfig(config)); } deleteTwoFaAccountConfig(providerType: TwoFactorAuthProviderType, config?: RequestConfig): Observable { @@ -76,4 +77,8 @@ export class TwoFactorAuthenticationService { defaultHttpOptionsFromConfig(config)); } + requestTwoFaVerificationCodeSend(providerType: TwoFactorAuthProviderType, config?: RequestConfig) { + return this.http.post(`/api/auth/2fa/verification/send?providerType=${providerType}`, defaultHttpOptionsFromConfig(config)); + } + } diff --git a/ui-ngx/src/app/modules/home/models/services.map.ts b/ui-ngx/src/app/modules/home/models/services.map.ts index 1f3a2bf5ae..9795dcdeda 100644 --- a/ui-ngx/src/app/modules/home/models/services.map.ts +++ b/ui-ngx/src/app/modules/home/models/services.map.ts @@ -36,6 +36,7 @@ import { BroadcastService } from '@core/services/broadcast.service'; import { ImportExportService } from '@home/components/import-export/import-export.service'; import { DeviceProfileService } from '@core/http/device-profile.service'; import { OtaPackageService } from '@core/http/ota-package.service'; +import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; export const ServicesMap = new Map>( [ @@ -59,6 +60,7 @@ export const ServicesMap = new Map>( ['router', Router], ['importExport', ImportExportService], ['deviceProfileService', DeviceProfileService], - ['otaPackageService', OtaPackageService] + ['otaPackageService', OtaPackageService], + ['twoFactorAuthenticationService', TwoFactorAuthenticationService] ] ); diff --git a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts index 576ab1c9b2..cf3aec6191 100644 --- a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts +++ b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts @@ -19,6 +19,7 @@ import { NgModule } from '@angular/core'; import { AdminModule } from './admin/admin.module'; import { HomeLinksModule } from './home-links/home-links.module'; import { ProfileModule } from './profile/profile.module'; +import { SecurityModule } from '@home/pages/security/security.module'; import { TenantModule } from '@modules/home/pages/tenant/tenant.module'; import { CustomerModule } from '@modules/home/pages/customer/customer.module'; import { AuditLogModule } from '@modules/home/pages/audit-log/audit-log.module'; @@ -42,6 +43,7 @@ import { OtaUpdateModule } from '@home/pages/ota-update/ota-update.module'; AdminModule, HomeLinksModule, ProfileModule, + SecurityModule, TenantProfileModule, TenantModule, DeviceProfileModule, diff --git a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.html b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.html deleted file mode 100644 index 46710e6ba0..0000000000 --- a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.html +++ /dev/null @@ -1,104 +0,0 @@ - - -

Enable email authenticator

- - -
- - -
-
- - - done - - - Add email -
- - user.email - - - {{ 'user.email-required' | translate }} - - - {{ 'user.invalid-email-format' | translate }} - - -
- - -
-
-
- - Authentication verification -
-

Enter the 6-digit code here

-

Enter the 6 digit verification code we just sent to {{ emailConfigForm.get('email').value }}.

- - 6-digit code - - -
- - -
-
-
- - Two-factor authentication activated -
-

Email authentication enabled

-

If we notice an attempted login from a device or browser we don’t recognize, you will be prompted to enter the security code that will be sent to your email address.

-
- -
-
-
-
-
diff --git a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.html b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.html deleted file mode 100644 index 03c6c7a465..0000000000 --- a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.html +++ /dev/null @@ -1,105 +0,0 @@ - - -

Enable SMS authenticator

- - -
- - -
-
- - - done - - - Add phone number -
-

Enter the phone number including the area code.

- - Phone number - - - {{ 'admin.number-to-required' | translate }} - - - {{ 'admin.phone-number-pattern' | translate }} - - -
- - -
-
-
- - Authentication verification -
-

Enter the 6-digit code here

-

Enter the 6 digit verification code we just sent to {{ smsConfigForm.get('phone').value }}.

- - 6-digit code - - -
- - -
-
-
- - Two-factor authentication activated -
-

SMS authentication enabled

-

If we notice an attempted login from a device or browser we don’t recognize, you will be prompted to enter the security code that will be sent to the phone number.

-
- -
-
-
-
-
diff --git a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.scss deleted file mode 100644 index ec6f008a80..0000000000 --- a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.scss +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ diff --git a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.html b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.html deleted file mode 100644 index 09afbb2de0..0000000000 --- a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.html +++ /dev/null @@ -1,97 +0,0 @@ - - -

Enable authenticator app

- - -
- - -
-
- - - done - - - Get app -
-

You'll need to use a verification app such as Google Authenticator, Authy, or Duo. Install from your app store

-
- - -
-
-
- - Authentication verification -
-

1. Scan this QR code with your verification app

-

Once your app reads the QR code, you'll get a 6-digit code.

- -

2. Enter the 6-digit code here

-

Enter the code from the app below. Once connected, we'll remember your phone so you can use it each time you log in.

- - 6-digit code - - -
- - -
-
-
- - Two-factor authentication activated -
-

Success

-

The next time you login from an unrecognized browser or device, you will need to provide a two-factor authentication code.

-
- -
-
-
-
-
diff --git a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.scss deleted file mode 100644 index ec6f008a80..0000000000 --- a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.scss +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright © 2016-2022 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. - */ diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile-routing.module.ts b/ui-ngx/src/app/modules/home/pages/profile/profile-routing.module.ts index e7dae87288..440db606fd 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/profile-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/profile/profile-routing.module.ts @@ -26,8 +26,6 @@ import { AppState } from '@core/core.state'; import { UserService } from '@core/http/user.service'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { Observable } from 'rxjs'; -import { TwoFactorAuthProviderType } from '@shared/models/two-factor-auth.models'; -import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; @Injectable() export class UserProfileResolver implements Resolve { @@ -42,17 +40,6 @@ export class UserProfileResolver implements Resolve { } } -@Injectable() -export class UserTwoFAProvidersResolver implements Resolve> { - - constructor(private twoFactorAuthService: TwoFactorAuthenticationService) { - } - - resolve(): Observable> { - return this.twoFactorAuthService.getAvailableTwoFaProviders(); - } -} - const routes: Routes = [ { path: 'profile', @@ -67,8 +54,7 @@ const routes: Routes = [ } }, resolve: { - user: UserProfileResolver, - providers: UserTwoFAProvidersResolver + user: UserProfileResolver } } ]; @@ -77,8 +63,7 @@ const routes: Routes = [ imports: [RouterModule.forChild(routes)], exports: [RouterModule], providers: [ - UserProfileResolver, - UserTwoFAProvidersResolver + UserProfileResolver ] }) export class ProfileRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile.component.html b/ui-ngx/src/app/modules/home/pages/profile/profile.component.html index a1e9ec0523..7300f7ea7c 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/profile.component.html +++ b/ui-ngx/src/app/modules/home/pages/profile/profile.component.html @@ -18,12 +18,13 @@
-
+
profile.profile {{ profile ? profile.get('email').value : '' }}
-
+
profile.last-login-time
@@ -77,21 +78,23 @@ {{ 'dashboard.home-dashboard-hide-toolbar' | translate }} -
- -
-
- - {{ expirationJwtData }} +
+
+ +
+
+ +
{{ expirationJwtData }}
+
diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile.component.scss b/ui-ngx/src/app/modules/home/pages/profile/profile.component.scss index f3f41c41d8..737c7c0999 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/profile.component.scss +++ b/ui-ngx/src/app/modules/home/pages/profile/profile.component.scss @@ -39,8 +39,10 @@ font-weight: 400; } .profile-btn-subtext { - opacity: 0.7; - padding: 10px; + font: 400 14px / 16px Roboto, "Helvetica Neue", sans-serif; + letter-spacing: 0.25px; + opacity: 0.6; + padding: 8px 0; } .tb-home-dashboard { tb-dashboard-autocomplete { @@ -59,21 +61,4 @@ } } } - .description { - padding-bottom: 8px; - color: #808080; - margin-right: 8px; - } - - .provider { - padding: 14px 0; - - .mat-h3 { - margin-bottom: 8px; - } - - .checkbox-label { - font-size: 14px; - } - } } diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts b/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts index 0c676aca46..76187b58c2 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts +++ b/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, OnInit, ViewChild } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { UserService } from '@core/http/user.service'; import { AuthUser, User } from '@shared/models/user.model'; import { Authority } from '@shared/models/authority.enum'; @@ -37,12 +37,6 @@ import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { DatePipe } from '@angular/common'; import { ClipboardService } from 'ngx-clipboard'; -import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; -import { AccountTwoFaSettings, TwoFactorAuthProviderType } from '@shared/models/two-factor-auth.models'; -import { MatSlideToggle } from '@angular/material/slide-toggle'; -import { TotpAuthDialogComponent } from '@home/pages/profile/authentication-dialog/totp-auth-dialog.component'; -import { SMSAuthDialogComponent } from '@home/pages/profile/authentication-dialog/sms-auth-dialog.component'; -import { EmailAuthDialogComponent, } from '@home/pages/profile/authentication-dialog/email-auth-dialog.component'; @Component({ selector: 'tb-profile', @@ -53,25 +47,8 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir authorities = Authority; profile: FormGroup; - twoFactorAuth: FormGroup; user: User; languageList = env.supportedLangs; - allowTwoFactorAuth = false; - allowSMS2faProvider = false; - allowTOTP2faProvider = false; - allowEmail2faProvider = false; - twoFactorAuthProviderType = TwoFactorAuthProviderType; - - @ViewChild('totp') totp: MatSlideToggle; - - private authDialogMap = new Map( -[ - [TwoFactorAuthProviderType.TOTP, TotpAuthDialogComponent], - [TwoFactorAuthProviderType.SMS, SMSAuthDialogComponent], - [TwoFactorAuthProviderType.EMAIL, EmailAuthDialogComponent] - ] - ); - private readonly authUser: AuthUser; get jwtToken(): string { @@ -92,7 +69,6 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir private userService: UserService, private authService: AuthService, private translate: TranslateService, - private twoFaService: TwoFactorAuthenticationService, public dialog: MatDialog, public dialogService: DialogService, public fb: FormBuilder, @@ -104,9 +80,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir ngOnInit() { this.buildProfileForm(); - this.buildTwoFactorForm(); this.userLoaded(this.route.snapshot.data.user); - this.twoFactorLoad(this.route.snapshot.data.providers); } private buildProfileForm() { @@ -120,19 +94,6 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir }); } - private buildTwoFactorForm() { - this.twoFactorAuth = this.fb.group({ - TOTP: [false], - SMS: [false], - EMAIL: [false], - useByDefault: [null] - }); - this.twoFactorAuth.get('useByDefault').valueChanges.subscribe(value => { - this.twoFaService.updateTwoFaAccountConfig(value, true, {ignoreLoading: true}) - .subscribe(data => this.processTwoFactorAuthConfig(data)); - }); - } - save(): void { this.user = {...this.user, ...this.profile.value}; if (!this.user.additionalInfo) { @@ -190,37 +151,6 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir this.profile.get('homeDashboardHideToolbar').setValue(homeDashboardHideToolbar); } - private twoFactorLoad(providers: TwoFactorAuthProviderType[]) { - if (providers.length) { - this.allowTwoFactorAuth = true; - this.twoFaService.getAccountTwoFaSettings().subscribe(data => this.processTwoFactorAuthConfig(data)); - providers.forEach(provider => { - switch (provider) { - case TwoFactorAuthProviderType.SMS: - this.allowSMS2faProvider = true; - break; - case TwoFactorAuthProviderType.TOTP: - this.allowTOTP2faProvider = true; - break; - case TwoFactorAuthProviderType.EMAIL: - this.allowEmail2faProvider = true; - break; - } - }); - } - } - - private processTwoFactorAuthConfig(setting?: AccountTwoFaSettings) { - if (setting) { - Object.values(setting.configs).forEach(config => { - this.twoFactorAuth.get(config.providerType).setValue(true); - if (config.useByDefault) { - this.twoFactorAuth.get('useByDefault').setValue(config.providerType, {emitEvent: false}); - } - }); - } - } - confirmForm(): FormGroup { return this.profile; } @@ -249,31 +179,4 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir })); } } - - confirm2FAChange(event: MouseEvent, provider: TwoFactorAuthProviderType) { - event.stopPropagation(); - event.preventDefault(); - const providerName = provider === TwoFactorAuthProviderType.TOTP ? 'authenticator app' : `${provider.toLowerCase()} authentication`; - if (this.twoFactorAuth.get(provider).value) { - this.dialogService.confirm(`Are you sure you want to disable ${providerName}?`, - `Disabling ${providerName} will make your account less secure`).subscribe(res => { - if (res) { - this.twoFaService.deleteTwoFaAccountConfig(provider).subscribe(data => this.processTwoFactorAuthConfig(data)); - } - }); - } else { - this.dialog.open(this.authDialogMap.get(provider), { - disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: { - email: this.user.email - } - }).afterClosed().subscribe(res => { - if (res) { - this.twoFactorAuth.get(provider).setValue(res); - this.twoFactorAuth.get('useByDefault').setValue(provider, {emitEvent: false}); - } - }); - } - } } diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile.module.ts b/ui-ngx/src/app/modules/home/pages/profile/profile.module.ts index 5210158537..4a8bcab074 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/profile.module.ts +++ b/ui-ngx/src/app/modules/home/pages/profile/profile.module.ts @@ -20,17 +20,11 @@ import { ProfileComponent } from './profile.component'; import { SharedModule } from '@shared/shared.module'; import { ProfileRoutingModule } from './profile-routing.module'; import { ChangePasswordDialogComponent } from '@modules/home/pages/profile/change-password-dialog.component'; -import { TotpAuthDialogComponent } from './authentication-dialog/totp-auth-dialog.component'; -import { SMSAuthDialogComponent } from '@home/pages/profile/authentication-dialog/sms-auth-dialog.component'; -import { EmailAuthDialogComponent } from '@home/pages/profile/authentication-dialog/email-auth-dialog.component'; @NgModule({ declarations: [ ProfileComponent, - ChangePasswordDialogComponent, - TotpAuthDialogComponent, - SMSAuthDialogComponent, - EmailAuthDialogComponent + ChangePasswordDialogComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/authentication-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/authentication-dialog.component.scss new file mode 100644 index 0000000000..9b8b17b102 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/authentication-dialog.component.scss @@ -0,0 +1,77 @@ +/** + * Copyright © 2016-2022 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. + */ +:host{ + .mat-toolbar > h2 { + font-weight: 400; + letter-spacing: 0.25px; + } + + form { + display: block; + } + + .mat-body-1 { + margin-bottom: 0; + letter-spacing: 0.25px; + + &:not(:first-of-type) { + margin: 0 0 8px; + } + } + + .input-container { + max-width: 290px; + } + + .code-container { + max-width: 170px; + } + + .result-title { + font: 500 18px / 24px Roboto, "Helvetica Neue", sans-serif; + letter-spacing: 0.1px; + margin: 8px 0; + text-align: center; + } + + .result-description { + text-align: center; + margin: 0 0 16px; + letter-spacing: 0.25px; + max-width: 500px; + } + + .step-description { + max-width: 450px; + &.input { + margin: 12px 0 0; + } + } + + .qr-code-description { + text-align: center; + max-width: 180px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + margin-bottom: 0; + } + + & ::ng-deep { + .mat-horizontal-stepper-header{ + pointer-events: none !important; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/authentication-dialog.map.ts b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/authentication-dialog.map.ts new file mode 100644 index 0000000000..503fdd69a6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/authentication-dialog.map.ts @@ -0,0 +1,29 @@ +/// +/// Copyright © 2016-2022 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 { Type } from '@angular/core'; +import { TwoFactorAuthProviderType } from '@shared/models/two-factor-auth.models'; +import { TotpAuthDialogComponent } from './totp-auth-dialog.component'; +import { SMSAuthDialogComponent } from './sms-auth-dialog.component'; +import { EmailAuthDialogComponent } from './email-auth-dialog.component'; + +export const authenticationDialogMap = new Map>( + [ + [TwoFactorAuthProviderType.TOTP, TotpAuthDialogComponent], + [TwoFactorAuthProviderType.SMS, SMSAuthDialogComponent], + [TwoFactorAuthProviderType.EMAIL, EmailAuthDialogComponent] + ] +); diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.html b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.html new file mode 100644 index 0000000000..d591dc7261 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.html @@ -0,0 +1,102 @@ + + +

security.2fa.dialog.enable-email-title

+ + +
+ + +
+
+ + + done + + + {{ 'security.2fa.dialog.email-step-label' | translate }} +
+

security.2fa.dialog.email-step-description

+
+ + + + + {{ 'user.email-required' | translate }} + + + {{ 'user.invalid-email-format' | translate }} + + + +
+
+
+ + {{ 'security.2fa.dialog.verification-step-label' | translate }} +
+

+ {{ 'security.2fa.dialog.verification-step-description' | translate : {address: emailConfigForm.get('email').value} }} +

+
+ + + + + {{ 'security.2fa.dialog.verification-code-invalid' | translate }} + + + +
+
+
+ + {{ 'security.2fa.dialog.activation-step-label' | translate }} +
+

security.2fa.dialog.success

+

security.2fa.dialog.activation-step-description-email

+
+ +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.ts similarity index 71% rename from ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.ts rename to ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.ts index be5976c78b..166ff799b7 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.ts @@ -32,7 +32,7 @@ export interface EmailAuthDialogData { @Component({ selector: 'tb-email-auth-dialog', templateUrl: './email-auth-dialog.component.html', - styleUrls: ['./email-auth-dialog.component.scss'] + styleUrls: ['./authentication-dialog.component.scss'] }) export class EmailAuthDialogComponent extends DialogComponent { @@ -68,20 +68,28 @@ export class EmailAuthDialogComponent extends DialogComponent { - this.stepper.next(); - }); + if (this.emailConfigForm.valid) { + this.authAccountConfig = { + providerType: TwoFactorAuthProviderType.EMAIL, + useByDefault: true, + email: this.emailConfigForm.get('email').value as string + }; + this.twoFaService.submitTwoFaAccountConfig(this.authAccountConfig).subscribe(() => { + this.stepper.next(); + }); + } else { + this.showFormErrors(this.emailConfigForm); + } break; case 1: - this.twoFaService.verifyAndSaveTwoFaAccountConfig(this.authAccountConfig, - this.emailVerificationForm.get('verificationCode').value).subscribe(() => { - this.stepper.next(); - }); + if (this.emailVerificationForm.valid) { + this.twoFaService.verifyAndSaveTwoFaAccountConfig(this.authAccountConfig, + this.emailVerificationForm.get('verificationCode').value).subscribe(() => { + this.stepper.next(); + }); + } else { + this.showFormErrors(this.emailVerificationForm); + } break; } } @@ -90,4 +98,10 @@ export class EmailAuthDialogComponent extends DialogComponent 1); } + private showFormErrors(form: FormGroup) { + Object.keys(form.controls).forEach(field => { + const control = form.get(field); + control.markAsTouched({onlySelf: true}); + }); + } } diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/sms-auth-dialog.component.html b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/sms-auth-dialog.component.html new file mode 100644 index 0000000000..61b093dd00 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/sms-auth-dialog.component.html @@ -0,0 +1,105 @@ + + +

security.2fa.dialog.enable-sms-title

+ + +
+ + +
+
+ + + done + + + {{ 'security.2fa.dialog.sms-step-label' | translate }} +
+

security.2fa.dialog.sms-step-description

+
+ + + + + {{ 'admin.number-to-required' | translate }} + + + {{ 'admin.phone-number-pattern' | translate }} + + + + +
+
+
+ + {{ 'security.2fa.dialog.verification-step-label' | translate }} +
+

+ {{ 'security.2fa.dialog.verification-step-description' | translate : {address: smsConfigForm.get('phone').value} }} +

+
+ + + + + {{ 'security.2fa.dialog.verification-code-invalid' | translate }} + + + +
+
+
+ + {{ 'security.2fa.dialog.activation-step-label' | translate }} +
+

security.2fa.dialog.success

+

security.2fa.dialog.activation-step-description-sms

+
+ +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/sms-auth-dialog.component.ts similarity index 71% rename from ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.ts rename to ui-ngx/src/app/modules/home/pages/security/authentication-dialog/sms-auth-dialog.component.ts index ed707922eb..030b67fd7a 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/sms-auth-dialog.component.ts @@ -29,7 +29,7 @@ import { MatStepper } from '@angular/material/stepper'; @Component({ selector: 'tb-sms-auth-dialog', templateUrl: './sms-auth-dialog.component.html', - styleUrls: ['./sms-auth-dialog.component.scss'] + styleUrls: ['./authentication-dialog.component.scss'] }) export class SMSAuthDialogComponent extends DialogComponent { @@ -66,20 +66,28 @@ export class SMSAuthDialogComponent extends DialogComponent { - this.stepper.next(); - }); + if (this.smsConfigForm.valid) { + this.authAccountConfig = { + providerType: TwoFactorAuthProviderType.SMS, + useByDefault: true, + phoneNumber: this.smsConfigForm.get('phone').value as string + }; + this.twoFaService.submitTwoFaAccountConfig(this.authAccountConfig).subscribe(() => { + this.stepper.next(); + }); + } else { + this.showFormErrors(this.smsConfigForm); + } break; case 1: - this.twoFaService.verifyAndSaveTwoFaAccountConfig(this.authAccountConfig, - this.smsVerificationForm.get('verificationCode').value).subscribe(() => { - this.stepper.next(); - }); + if (this.smsVerificationForm.valid) { + this.twoFaService.verifyAndSaveTwoFaAccountConfig(this.authAccountConfig, + this.smsVerificationForm.get('verificationCode').value).subscribe(() => { + this.stepper.next(); + }); + } else { + this.showFormErrors(this.smsVerificationForm); + } break; } } @@ -88,4 +96,10 @@ export class SMSAuthDialogComponent extends DialogComponent 1); } + private showFormErrors(form: FormGroup) { + Object.keys(form.controls).forEach(field => { + const control = form.get(field); + control.markAsTouched({onlySelf: true}); + }); + } } diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.html b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.html new file mode 100644 index 0000000000..6de8259011 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.html @@ -0,0 +1,92 @@ + + +

security.2fa.dialog.enable-totp-title

+ + +
+ + +
+
+ + + done + + + {{ 'security.2fa.dialog.totp-step-label' | translate }} +
+

security.2fa.dialog.totp-step-description-open

+

security.2fa.dialog.totp-step-description-install

+
+ +
+
+
+ + {{ 'security.2fa.dialog.verification-step-label' | translate }} +
+

security.2fa.dialog.scan-qr-code

+ +

security.2fa.dialog.enter-verification-code

+ + + + + {{ 'security.2fa.dialog.verification-code-invalid' | translate }} + + +
+
+ +
+
+ + {{ 'security.2fa.dialog.activation-step-label' | translate }} +
+

security.2fa.dialog.success

+

security.2fa.dialog.activation-step-description-totp

+
+ +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.ts similarity index 85% rename from ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.ts rename to ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.ts index 86b596ac2b..52aab77b57 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.ts @@ -28,7 +28,7 @@ import { MatStepper } from '@angular/material/stepper'; @Component({ selector: 'tb-totp-auth-dialog', templateUrl: './totp-auth-dialog.component.html', - styleUrls: ['./totp-auth-dialog.component.scss'] + styleUrls: ['./authentication-dialog.component.scss'] }) export class TotpAuthDialogComponent extends DialogComponent { @@ -67,10 +67,17 @@ export class TotpAuthDialogComponent extends DialogComponent { - this.stepper.next(); - }); + if (this.totpConfigForm.valid) { + this.twoFaService.verifyAndSaveTwoFaAccountConfig(this.authAccountConfig, + this.totpConfigForm.get('verificationCode').value).subscribe(() => { + this.stepper.next(); + }); + } else { + Object.keys(this.totpConfigForm.controls).forEach(field => { + const control = this.totpConfigForm.get(field); + control.markAsTouched({onlySelf: true}); + }); + } } closeDialog() { diff --git a/ui-ngx/src/app/modules/home/pages/security/security-routing.module.ts b/ui-ngx/src/app/modules/home/pages/security/security-routing.module.ts new file mode 100644 index 0000000000..430635cd8e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/security/security-routing.module.ts @@ -0,0 +1,84 @@ +/// +/// Copyright © 2016-2022 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 { Injectable, NgModule } from '@angular/core'; +import { Resolve, RouterModule, Routes } from '@angular/router'; + +import { SecurityComponent } from './security.component'; +import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; +import { Authority } from '@shared/models/authority.enum'; +import { User } from '@shared/models/user.model'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { UserService } from '@core/http/user.service'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { Observable } from 'rxjs'; +import { TwoFactorAuthProviderType } from '@shared/models/two-factor-auth.models'; +import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; + +@Injectable() +export class UserProfileResolver implements Resolve { + + constructor(private store: Store, + private userService: UserService) { + } + + resolve(): Observable { + const userId = getCurrentAuthUser(this.store).userId; + return this.userService.getUser(userId); + } +} + +@Injectable() +export class UserTwoFAProvidersResolver implements Resolve> { + + constructor(private twoFactorAuthService: TwoFactorAuthenticationService) { + } + + resolve(): Observable> { + return this.twoFactorAuthService.getAvailableTwoFaProviders(); + } +} + +const routes: Routes = [ + { + path: 'security', + component: SecurityComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + title: 'security.security', + breadcrumb: { + label: 'security.security', + icon: 'lock' + } + }, + resolve: { + user: UserProfileResolver, + providers: UserTwoFAProvidersResolver + } + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [ + UserProfileResolver, + UserTwoFAProvidersResolver + ] +}) +export class SecurityRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/security/security.component.html b/ui-ngx/src/app/modules/home/pages/security/security.component.html new file mode 100644 index 0000000000..19d414535e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/security/security.component.html @@ -0,0 +1,80 @@ + +
+ + +
+
+ security.security +
+
+ profile.last-login-time + +
+
+
+ +
+ +
{{ expirationJwtData }}
+
+
+
+ + + admin.2fa.2fa + + +
security.2fa.2fa-description
+
+ +

security.2fa.authenticate-with

+
+
+ + +
+

{{ providersData.get(provider).name | translate }}

+
+
+ {{ providersData.get(provider).description | translate }} +
+ + +
+ + security.2fa.main-2fa-method + +
+ +
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/security/security.component.scss b/ui-ngx/src/app/modules/home/pages/security/security.component.scss new file mode 100644 index 0000000000..b14b2ebd18 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/security/security.component.scss @@ -0,0 +1,115 @@ +/** + * Copyright © 2016-2022 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 "../../../../../scss/constants"; + +:host { + .profile-container { + padding: 8px; + } + mat-card.profile-card { + @media #{$mat-gt-sm} { + width: 70%; + } + @media #{$mat-gt-md} { + width: 50%; + } + @media #{$mat-gt-xl} { + width: 45%; + } + .title-container { + margin: 0; + } + .profile-email { + font-size: 16px; + font-weight: 400; + } + .mat-subheader { + line-height: 24px; + color: rgba(0,0,0,0.54); + font-size: 14px; + font-weight: 400; + } + .profile-last-login-ts { + font-size: 16px; + font-weight: 400; + } + .profile-btn-subtext { + font: 400 14px / 16px Roboto, "Helvetica Neue", sans-serif; + letter-spacing: 0.25px; + opacity: 0.6; + padding: 8px 0; + } + .tb-home-dashboard { + tb-dashboard-autocomplete { + @media #{$mat-gt-sm} { + padding-right: 12px; + } + + @media #{$mat-lt-md} { + padding-bottom: 12px; + } + } + mat-checkbox { + @media #{$mat-gt-sm} { + margin-top: 16px; + } + } + } + } + + .description { + color: rgba(0, 0, 0, 0.54); + letter-spacing: 0.25px; + margin-right: 8px; + } + + .auth-title { + font-weight: 500; + line-height: 20px; + letter-spacing: 0.25px; + margin: 0; + } + + .mat-divider-horizontal { + left: 16px; + right: 16px; + width: auto; + } + + .provider { + padding: 24px 0; + + .provider-title { + font: 400 14px / 16px Roboto, "Helvetica Neue", sans-serif; + letter-spacing: 0.25px; + margin: 0 0 8px; + } + + .description { + max-width: 85%; + } + + .checkbox-label { + font-size: 14px; + letter-spacing: 0.25px; + opacity: 0.87; + } + + .mat-radio-button { + margin-top: 8px; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/security/security.component.ts b/ui-ngx/src/app/modules/home/pages/security/security.component.ts new file mode 100644 index 0000000000..d5ded5f4f8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/security/security.component.ts @@ -0,0 +1,171 @@ +/// +/// Copyright © 2016-2022 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 { Component, OnInit } from '@angular/core'; +import { User } from '@shared/models/user.model'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material/dialog'; +import { DialogService } from '@core/services/dialog.service'; +import { ActivatedRoute } from '@angular/router'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { DatePipe } from '@angular/common'; +import { ClipboardService } from 'ngx-clipboard'; +import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; +import { + AccountTwoFaSettings, + twoFactorAuthProvidersData, + TwoFactorAuthProviderType +} from '@shared/models/two-factor-auth.models'; +import { authenticationDialogMap } from '@home/pages/security/authentication-dialog/authentication-dialog.map'; + +@Component({ + selector: 'tb-security', + templateUrl: './security.component.html', + styleUrls: ['./security.component.scss'] +}) +export class SecurityComponent extends PageComponent implements OnInit { + + twoFactorAuth: FormGroup; + user: User; + allowTwoFactorProviders: TwoFactorAuthProviderType[] = []; + providersData = twoFactorAuthProvidersData; + + get jwtToken(): string { + return `Bearer ${localStorage.getItem('jwt_token')}`; + } + + get jwtTokenExpiration(): string { + return localStorage.getItem('jwt_token_expiration'); + } + + get expirationJwtData(): string { + const expirationData = this.datePipe.transform(this.jwtTokenExpiration, 'yyyy-MM-dd HH:mm:ss'); + return this.translate.instant('profile.valid-till', { expirationData }); + } + + constructor(protected store: Store, + private route: ActivatedRoute, + private translate: TranslateService, + private twoFaService: TwoFactorAuthenticationService, + public dialog: MatDialog, + public dialogService: DialogService, + public fb: FormBuilder, + private datePipe: DatePipe, + private clipboardService: ClipboardService) { + super(store); + } + + ngOnInit() { + this.buildTwoFactorForm(); + this.user = this.route.snapshot.data.user; + this.twoFactorLoad(this.route.snapshot.data.providers); + } + + private buildTwoFactorForm() { + this.twoFactorAuth = this.fb.group({ + TOTP: [false], + SMS: [false], + EMAIL: [false], + useByDefault: [null] + }); + this.twoFactorAuth.get('useByDefault').valueChanges.subscribe(value => { + this.twoFaService.updateTwoFaAccountConfig(value, true, {ignoreLoading: true}) + .subscribe(data => this.processTwoFactorAuthConfig(data)); + }); + } + + private twoFactorLoad(providers: TwoFactorAuthProviderType[]) { + if (providers.length) { + this.twoFaService.getAccountTwoFaSettings().subscribe(data => this.processTwoFactorAuthConfig(data)); + Object.values(TwoFactorAuthProviderType).forEach(type => { + if (providers.includes(type)) { + this.allowTwoFactorProviders.push(type); + } + }); + } + } + + private processTwoFactorAuthConfig(setting: AccountTwoFaSettings) { + const configs = setting.configs; + Object.values(TwoFactorAuthProviderType).forEach(provider => { + if (configs[provider]) { + this.twoFactorAuth.get(provider).setValue(true); + if (configs[provider].useByDefault) { + this.twoFactorAuth.get('useByDefault').setValue(provider, {emitEvent: false}); + } + } else { + this.twoFactorAuth.get(provider).setValue(false); + } + }); + } + + trackByProvider(i: number, provider: TwoFactorAuthProviderType) { + return provider; + } + + copyToken() { + if (+this.jwtTokenExpiration < Date.now()) { + this.store.dispatch(new ActionNotificationShow({ + message: this.translate.instant('profile.tokenCopiedWarnMessage'), + type: 'warn', + duration: 1500, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } else { + this.clipboardService.copyFromContent(this.jwtToken); + this.store.dispatch(new ActionNotificationShow({ + message: this.translate.instant('profile.tokenCopiedSuccessMessage'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } + } + + confirm2FAChange(event: MouseEvent, provider: TwoFactorAuthProviderType) { + event.stopPropagation(); + event.preventDefault(); + if (this.twoFactorAuth.get(provider).value) { + const providerName = this.translate.instant(`security.2fa.provider.${provider.toLowerCase()}`); + this.dialogService.confirm( + this.translate.instant('security.2fa.disable-2fa-provider-title', {name: providerName}), + this.translate.instant('security.2fa.disable-2fa-provider-text', {name: providerName}), + ).subscribe(res => { + if (res) { + this.twoFaService.deleteTwoFaAccountConfig(provider).subscribe(data => this.processTwoFactorAuthConfig(data)); + } + }); + } else { + const dialogData = provider === TwoFactorAuthProviderType.EMAIL ? {email: this.user.email} : {}; + this.dialog.open(authenticationDialogMap.get(provider), { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: dialogData + }).afterClosed().subscribe(res => { + if (res) { + this.twoFactorAuth.get(provider).setValue(res); + this.twoFactorAuth.get('useByDefault').setValue(provider, {emitEvent: false}); + } + }); + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/security/security.module.ts b/ui-ngx/src/app/modules/home/pages/security/security.module.ts new file mode 100644 index 0000000000..2df54747a7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/security/security.module.ts @@ -0,0 +1,39 @@ +/// +/// Copyright © 2016-2022 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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SecurityComponent } from './security.component'; +import { SharedModule } from '@shared/shared.module'; +import { SecurityRoutingModule } from './security-routing.module'; +import { TotpAuthDialogComponent } from './authentication-dialog/totp-auth-dialog.component'; +import { SMSAuthDialogComponent } from '@home/pages/security/authentication-dialog/sms-auth-dialog.component'; +import { EmailAuthDialogComponent } from '@home/pages/security/authentication-dialog/email-auth-dialog.component'; + +@NgModule({ + declarations: [ + SecurityComponent, + TotpAuthDialogComponent, + SMSAuthDialogComponent, + EmailAuthDialogComponent + ], + imports: [ + CommonModule, + SharedModule, + SecurityRoutingModule + ] +}) +export class SecurityModule { } diff --git a/ui-ngx/src/app/shared/components/user-menu.component.html b/ui-ngx/src/app/shared/components/user-menu.component.html index 5dfcebf3f7..ffe9cdc158 100644 --- a/ui-ngx/src/app/shared/components/user-menu.component.html +++ b/ui-ngx/src/app/shared/components/user-menu.component.html @@ -32,6 +32,10 @@ account_circle home.profile +