diff --git a/application/src/main/data/json/edge/install_instructions/docker/instructions.md b/application/src/main/data/json/edge/install_instructions/docker/instructions.md new file mode 100644 index 0000000000..6b93447851 --- /dev/null +++ b/application/src/main/data/json/edge/install_instructions/docker/instructions.md @@ -0,0 +1,107 @@ +## Install ThingsBoard Edge and connect to cloud instructions + +Here is the list of commands, that can be used to quickly install and connect ThingsBoard Edge to the cloud using docker compose. + +### Prerequisites + +Install Docker CE and Docker Compose. + +### Create data and logs folders + +Run following commands, before starting docker container(s), to create folders for storing data and logs. +These commands additionally will change owner of newly created folders to docker container user. +To do this (to change user) **chown** command is used, and this command requires *sudo* permissions (command will request password for a *sudo* access): + +```bash +mkdir -p ~/.mytb-edge-data && sudo chown -R 799:799 ~/.mytb-edge-data +mkdir -p ~/.mytb-edge-logs && sudo chown -R 799:799 ~/.mytb-edge-logs +{:copy-code} +``` + +### Running ThingsBoard Edge as docker service + +${LOCALHOST_WARNING} + +Create docker compose file for ThingsBoard Edge service: + +```bash +nano docker-compose.yml +{:copy-code} +``` + +Add the following lines to the yml file: + +```bash +version: '3.0' +services: + mytbedge: + restart: always + image: "thingsboard/tb-edge:${TB_EDGE_VERSION}" + ports: + - "8080:8080" + - "1883:1883" + - "5683-5688:5683-5688/udp" + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/tb-edge + CLOUD_ROUTING_KEY: ${CLOUD_ROUTING_KEY} + CLOUD_ROUTING_SECRET: ${CLOUD_ROUTING_SECRET} + CLOUD_RPC_HOST: ${BASE_URL} + CLOUD_RPC_PORT: ${CLOUD_RPC_PORT} + CLOUD_RPC_SSL_ENABLED: ${CLOUD_RPC_SSL_ENABLED} + volumes: + - ~/.mytb-edge-data:/data + - ~/.mytb-edge-logs:/var/log/tb-edge + postgres: + restart: always + image: "postgres:12" + ports: + - "5432" + environment: + POSTGRES_DB: tb-edge + POSTGRES_PASSWORD: postgres + volumes: + - ~/.mytb-edge-data/db:/var/lib/postgresql/data +{:copy-code} +``` + +#### [Optional] Update bind ports +If ThingsBoard Edge is going to be running on the same machine where ThingsBoard server (cloud) is running, you'll need to update docker compose port mapping to avoid port collision between ThingsBoard server and ThingsBoard Edge. + +Please update next lines of `docker-compose.yml` file: + +```bash +ports: + - "18080:8080" + - "11883:1883" + - "15683-15688:5683-5688/udp" +``` +Make sure that ports above (18080, 11883, 15683-15688) are not used by any other application. + +#### Start ThingsBoard Edge +Set the terminal in the directory which contains the `docker-compose.yml` file and execute the following commands to up this docker compose directly: + +```bash +docker compose up -d +docker compose logs -f mytbedge +{:copy-code} +``` + +###### NOTE: Docker Compose V2 vs docker-compose (with a hyphen) + +ThingsBoard supports Docker Compose V2 (Docker Desktop or Compose plugin) starting from **3.4.2** release, because **docker-compose** as standalone setup is no longer supported by Docker. +We **strongly** recommend to update to Docker Compose V2 and use it. +If you still rely on using Docker Compose as docker-compose (with a hyphen), then please execute the following commands to start ThingsBoard Edge: + +```bash +docker-compose up -d +docker-compose logs -f mytbedge +``` + +#### Open ThingsBoard Edge UI + +Once started, you will be able to open **ThingsBoard Edge UI** using the following link http://localhost:8080. + +###### NOTE: Edge HTTP bind port update + +Use next **ThingsBoard Edge UI** link **http://localhost:18080** if you updated HTTP 8080 bind port to **18080**. + diff --git a/application/src/main/data/json/edge/install_instructions/docker/localhost_warning.md b/application/src/main/data/json/edge/install_instructions/docker/localhost_warning.md new file mode 100644 index 0000000000..d2a5bad26b --- /dev/null +++ b/application/src/main/data/json/edge/install_instructions/docker/localhost_warning.md @@ -0,0 +1,3 @@ +###### WARNING NOTE: 'localhost' can not be used as CLOUD_RPC_HOST + +Please note that your ThingsBoard base URL is **'localhost'** at the moment. **'localhost'** cannot be used for docker containers - please update **CLOUD_RPC_HOST** environment variable below to the IP address of your machine (*docker **host** machine*). IP address must be `192.168.1.XX` or similar format. In other case - ThingsBoard Edge service, that is running in docker container, will not be able to connect to the cloud. diff --git a/application/src/main/data/upgrade/3.4.3/schema_update.sql b/application/src/main/data/upgrade/3.4.3/schema_update.sql new file mode 100644 index 0000000000..e6fd32bc2d --- /dev/null +++ b/application/src/main/data/upgrade/3.4.3/schema_update.sql @@ -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. +-- + +CREATE TABLE IF NOT EXISTS alarm_comment ( + id uuid NOT NULL, + created_time bigint NOT NULL, + alarm_id uuid NOT NULL, + user_id uuid, + type varchar(255) NOT NULL, + comment varchar(10000), + CONSTRAINT fk_alarm_comment_alarm_id FOREIGN KEY (alarm_id) REFERENCES alarm(id) ON DELETE CASCADE +) PARTITION BY RANGE (created_time); +CREATE INDEX IF NOT EXISTS idx_alarm_comment_alarm_id ON alarm_comment(alarm_id); 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 8b19a6954c..8fb9804e71 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -50,6 +50,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.common.stats.TbApiUsageReportClient; +import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; @@ -278,6 +279,10 @@ public class ActorSystemContext { @Getter private AlarmSubscriptionService alarmService; + @Autowired + @Getter + private AlarmCommentService alarmCommentService; + @Autowired @Getter private JsInvokeService jsInvokeService; 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 ba1c4e9796..db83eec2e8 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 @@ -71,6 +71,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgProcessingStackItem; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; @@ -104,6 +105,7 @@ import org.thingsboard.server.service.script.RuleNodeTbelScriptEngine; import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -591,6 +593,11 @@ class DefaultTbContext implements TbContext { return mainCtx.getAlarmService(); } + @Override + public AlarmCommentService getAlarmCommentService() { + return mainCtx.getAlarmCommentService(); + } + @Override public RuleChainService getRuleChainService() { return mainCtx.getRuleChainService(); @@ -788,6 +795,12 @@ class DefaultTbContext implements TbContext { return metaData; } + + @Override + public void schedule(Runnable runnable, long delay, TimeUnit timeUnit) { + mainCtx.getScheduler().schedule(runnable, delay, timeUnit); + } + @Override public void checkTenantEntity(EntityId entityId) { if (!this.getTenantId().equals(TenantIdLoader.findTenantId(this, entityId))) { diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java new file mode 100644 index 0000000000..1af3c8c8bf --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java @@ -0,0 +1,127 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 io.swagger.annotations.ApiOperation; +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.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AlarmCommentId; +import org.thingsboard.server.common.data.id.AlarmId; +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.alarm.TbAlarmCommentService; +import org.thingsboard.server.service.security.permission.Operation; + +import static org.thingsboard.server.controller.ControllerConstants.ALARM_COMMENT_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ALARM_COMMENT_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES; +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_OR_CUSTOMER_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; + +@RestController +@TbCoreComponent +@RequiredArgsConstructor +@RequestMapping("/api") +public class AlarmCommentController extends BaseController { + public static final String ALARM_ID = "alarmId"; + public static final String ALARM_COMMENT_ID = "commentId"; + + private final TbAlarmCommentService tbAlarmCommentService; + + @ApiOperation(value = "Create or update Alarm Comment ", + notes = "Creates or Updates the Alarm Comment. " + + "When creating comment, platform generates Alarm Comment Id as " + UUID_WIKI_LINK + + "The newly created Alarm Comment id will be present in the response. Specify existing Alarm Comment id to update the alarm. " + + "Referencing non-existing Alarm Comment Id will cause 'Not Found' error. " + + "\n\n To create new Alarm comment entity it is enough to specify 'comment' json element with 'text' node, for example: {\"comment\": { \"text\": \"my comment\"}}. " + + "\n\n If comment type is not specified the default value 'OTHER' will be saved. If 'alarmId' or 'userId' specified in body it will be ignored." + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH + , produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/alarm/{alarmId}/comment", method = RequestMethod.POST) + @ResponseBody + public AlarmComment saveAlarmComment(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) + @PathVariable(ALARM_ID) String strAlarmId, @ApiParam(value = "A JSON value representing the comment.") @RequestBody AlarmComment alarmComment) throws ThingsboardException { + checkParameter(ALARM_ID, strAlarmId); + AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); + Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); + alarmComment.setAlarmId(alarmId); + return tbAlarmCommentService.saveAlarmComment(alarm, alarmComment, getCurrentUser()); + } + + @ApiOperation(value = "Delete Alarm comment (deleteAlarmComment)", + notes = "Deletes the Alarm comment. Referencing non-existing Alarm comment Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/alarm/{alarmId}/comment/{commentId}", method = RequestMethod.DELETE) + @ResponseBody + public void deleteAlarmComment(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId, @ApiParam(value = ALARM_COMMENT_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_COMMENT_ID) String strCommentId) throws ThingsboardException { + checkParameter(ALARM_ID, strAlarmId); + AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); + Alarm alarm = checkAlarmId(alarmId, Operation.DELETE); + + AlarmCommentId alarmCommentId = new AlarmCommentId(toUUID(strCommentId)); + AlarmComment alarmComment = checkAlarmCommentId(alarmCommentId, alarmId); + tbAlarmCommentService.deleteAlarmComment(alarm, alarmComment, getCurrentUser()); + } + + @ApiOperation(value = "Get Alarm comments (getAlarmComments)", + notes = "Returns a page of alarm comments for specified alarm. " + + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/alarm/{alarmId}/comment", method = RequestMethod.GET) + @ResponseBody + public PageData getAlarmComments( + @ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) + @PathVariable(ALARM_ID) String strAlarmId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ALARM_COMMENT_SORT_PROPERTY_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortOrder + ) throws Exception { + checkParameter(ALARM_ID, strAlarmId); + AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); + Alarm alarm = alarmService.findAlarmByIdAsync(getCurrentUser().getTenantId(), alarmId).get(); + checkNotNull(alarm, "Alarm with id [" + alarmId + "] is not found"); + checkEntityId(alarm.getOriginator(), Operation.READ); + + PageLink pageLink = createPageLink(pageSize, page, null, sortProperty, sortOrder); + return checkNotNull(alarmCommentService.findAlarmComments(alarm.getTenantId(), alarmId, pageLink)); + } +} 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 43d049926b..a211acbfc1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -54,6 +54,7 @@ import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; @@ -64,6 +65,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.edge.EdgeInfo; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AlarmCommentId; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; @@ -98,6 +100,7 @@ import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; @@ -132,6 +135,7 @@ import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.component.ComponentDiscoveryService; +import org.thingsboard.server.service.edge.instructions.EdgeInstallService; import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.entitiy.TbNotificationEntityService; import org.thingsboard.server.service.ota.OtaPackageStateService; @@ -200,6 +204,9 @@ public abstract class BaseController { @Autowired protected AlarmSubscriptionService alarmService; + @Autowired + protected AlarmCommentService alarmCommentService; + @Autowired protected DeviceCredentialsService deviceCredentialsService; @@ -281,6 +288,9 @@ public abstract class BaseController { @Autowired(required = false) protected EdgeRpcService edgeRpcService; + @Autowired(required = false) + protected EdgeInstallService edgeInstallService; + @Autowired protected TbNotificationEntityService notificationEntityService; @@ -716,6 +726,20 @@ public abstract class BaseController { } } + AlarmComment checkAlarmCommentId(AlarmCommentId alarmCommentId, AlarmId alarmId) throws ThingsboardException { + try { + validateId(alarmCommentId, "Incorrect alarmCommentId " + alarmCommentId); + AlarmComment alarmComment = alarmCommentService.findAlarmCommentByIdAsync(getCurrentUser().getTenantId(), alarmCommentId).get(); + checkNotNull(alarmComment, "Alarm comment with id [" + alarmCommentId + "] is not found"); + if (!alarmId.equals(alarmComment.getAlarmId())) { + throw new ThingsboardException("Alarm id does not match with comment alarm id", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + return alarmComment; + } catch (Exception e) { + throw handleException(e, false); + } + } + WidgetsBundle checkWidgetsBundleId(WidgetsBundleId widgetsBundleId, Operation operation) throws ThingsboardException { try { validateId(widgetsBundleId, "Incorrect widgetsBundleId " + widgetsBundleId); diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index 2409a19ac5..0aafe5e368 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -44,6 +44,8 @@ public class ControllerConstants { protected static final String USER_ID_PARAM_DESCRIPTION = "A string value representing the user id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ASSET_ID_PARAM_DESCRIPTION = "A string value representing the asset id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ALARM_ID_PARAM_DESCRIPTION = "A string value representing the alarm id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; + + protected static final String ALARM_COMMENT_ID_PARAM_DESCRIPTION = "A string value representing the alarm comment id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ENTITY_ID_PARAM_DESCRIPTION = "A string value representing the entity id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String OTA_PACKAGE_ID_PARAM_DESCRIPTION = "A string value representing the ota package id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ENTITY_TYPE_PARAM_DESCRIPTION = "A string value representing the entity type. For example, 'DEVICE'"; @@ -100,6 +102,7 @@ public class ControllerConstants { protected static final String ASSET_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, description, isDefault"; protected static final String ASSET_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type, label, customerTitle"; protected static final String ALARM_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, startTs, endTs, type, ackTs, clearTs, severity, status"; + protected static final String ALARM_COMMENT_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime"; protected static final String EVENT_SORT_PROPERTY_ALLOWABLE_VALUES = "ts, id"; protected static final String EDGE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type, label, customerTitle"; protected static final String RULE_CHAIN_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, root"; 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 6a5dd44d0b..b6713ee0b6 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java @@ -39,6 +39,7 @@ import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeInfo; +import org.thingsboard.server.common.data.edge.EdgeInstallInstructions; import org.thingsboard.server.common.data.edge.EdgeSearchQuery; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -63,6 +64,7 @@ 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 javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -595,4 +597,24 @@ public class EdgeController extends BaseController { return edgeBulkImportService.processBulkImport(request, user); } + + @ApiOperation(value = "Get Edge Docker Install Instructions (getEdgeDockerInstallInstructions)", + notes = "Get a docker install instructions for provided edge id." + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/instructions/{edgeId}", method = RequestMethod.GET) + @ResponseBody + public EdgeInstallInstructions getEdgeDockerInstallInstructions( + @ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) + @PathVariable("edgeId") String strEdgeId, + HttpServletRequest request) throws ThingsboardException { + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + edgeId = checkNotNull(edgeId); + Edge edge = checkEdgeId(edgeId, Operation.READ); + return checkNotNull(edgeInstallService.getDockerInstallInstructions(getTenantId(), edge, request)); + } catch (Exception e) { + throw handleException(e); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java index 0f299fefae..996d2beef4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java @@ -426,11 +426,7 @@ public class EntityViewController extends BaseController { checkParameter(ENTITY_VIEW_ID, strEntityViewId); EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId)); checkEntityViewId(entityViewId, Operation.ASSIGN_TO_CUSTOMER); - - Customer publicCustomer = customerService.findOrCreatePublicCustomer(getTenantId()); - - return tbEntityViewService.assignEntityViewToPublicCustomer(getTenantId(), getCurrentUser().getCustomerId(), - publicCustomer, entityViewId, getCurrentUser()); + return tbEntityViewService.assignEntityViewToPublicCustomer(getTenantId(), entityViewId, getCurrentUser()); } @ApiOperation(value = "Assign entity view to edge (assignEntityViewToEdge)", 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 9facea6dde..1b8d0eb981 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -233,6 +233,9 @@ public class ThingsboardInstallService { log.info("Upgrading ThingsBoard from version 3.4.1 to 3.4.2 ..."); databaseEntitiesUpgradeService.upgradeDatabase("3.4.1"); dataUpdateService.updateData("3.4.1"); + case "3.4.3": + log.info("Upgrading ThingsBoard from version 3.4.3 to 3.5.0 ..."); + databaseEntitiesUpgradeService.upgradeDatabase("3.4.3"); log.info("Updating system data..."); systemDataLoaderService.updateSystemWidgets(); break; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallService.java new file mode 100644 index 0000000000..5a602a5a14 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallService.java @@ -0,0 +1,94 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.edge.instructions; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeInstallInstructions; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.install.InstallScripts; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +@Service +@Slf4j +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") +@TbCoreComponent +public class DefaultEdgeInstallService implements EdgeInstallService { + + private static final String EDGE_DIR = "edge"; + + private static final String EDGE_INSTALL_INSTRUCTIONS_DIR = "install_instructions"; + + private final InstallScripts installScripts; + + @Value("${edges.rpc.port}") + private int rpcPort; + + @Value("${edges.rpc.ssl.enabled}") + private boolean sslEnabled; + + @Value("${app.version:unknown}") + private String appVersion; + + @Override + public EdgeInstallInstructions getDockerInstallInstructions(TenantId tenantId, Edge edge, HttpServletRequest request) { + String dockerInstallInstructions = readFile(resolveFile("docker", "instructions.md")); + String baseUrl = request.getServerName(); + if (baseUrl.contains("localhost") || baseUrl.contains("127.0.0.1")) { + String localhostWarning = readFile(resolveFile("docker", "localhost_warning.md")); + dockerInstallInstructions = dockerInstallInstructions.replace("${LOCALHOST_WARNING}", localhostWarning); + dockerInstallInstructions = dockerInstallInstructions.replace("${BASE_URL}", "!!!REPLACE_ME_TO_HOST_IP_ADDRESS!!!"); + } else { + dockerInstallInstructions = dockerInstallInstructions.replace("${LOCALHOST_WARNING}", ""); + dockerInstallInstructions = dockerInstallInstructions.replace("${BASE_URL}", baseUrl); + } + dockerInstallInstructions = dockerInstallInstructions.replace("${TB_EDGE_VERSION}", appVersion + "EDGE"); + dockerInstallInstructions = dockerInstallInstructions.replace("${CLOUD_ROUTING_KEY}", edge.getRoutingKey()); + dockerInstallInstructions = dockerInstallInstructions.replace("${CLOUD_ROUTING_SECRET}", edge.getSecret()); + dockerInstallInstructions = dockerInstallInstructions.replace("${CLOUD_RPC_PORT}", Integer.toString(rpcPort)); + dockerInstallInstructions = dockerInstallInstructions.replace("${CLOUD_RPC_SSL_ENABLED}", Boolean.toString(sslEnabled)); + return new EdgeInstallInstructions(dockerInstallInstructions); + } + + private String readFile(Path file) { + try { + return new String(Files.readAllBytes(file), StandardCharsets.UTF_8); + } catch (IOException e) { + log.warn("Failed to read file: {}", file, e); + throw new RuntimeException(e); + } + } + + private Path resolveFile(String subDir, String... subDirs) { + return getEdgeInstallInstructionsDir().resolve(Paths.get(subDir, subDirs)); + } + + private Path getEdgeInstallInstructionsDir() { + return Paths.get(installScripts.getDataDir(), InstallScripts.JSON_DIR, EDGE_DIR, EDGE_INSTALL_INSTRUCTIONS_DIR); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallService.java new file mode 100644 index 0000000000..a660875baf --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallService.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.service.edge.instructions; + +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeInstallInstructions; +import org.thingsboard.server.common.data.id.TenantId; + +import javax.servlet.http.HttpServletRequest; + +public interface EdgeInstallService { + + EdgeInstallInstructions getDockerInstallInstructions(TenantId tenantId, Edge edge, HttpServletRequest request); + +} 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 28cc32efa3..38ba0c0794 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 @@ -193,8 +193,10 @@ public final class EdgeGrpcSession implements Closeable { private void doSync(EdgeSyncCursor cursor) { if (cursor.hasNext()) { - log.info("[{}][{}] starting sync process, cursor current idx = {}", edge.getTenantId(), edge.getId(), cursor.getCurrentIdx()); - ListenableFuture uuidListenableFuture = startProcessingEdgeEvents(cursor.getNext()); + EdgeEventFetcher next = cursor.getNext(); + log.info("[{}][{}] starting sync process, cursor current idx = {}, class = {}", + edge.getTenantId(), edge.getId(), cursor.getCurrentIdx(), next.getClass().getSimpleName()); + ListenableFuture uuidListenableFuture = startProcessingEdgeEvents(next); Futures.addCallback(uuidListenableFuture, new FutureCallback<>() { @Override public void onSuccess(@Nullable UUID result) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java index e9232584be..91764716d0 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.edge.rpc; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.edge.EdgeContextComponent; @@ -41,9 +42,9 @@ import java.util.NoSuchElementException; public class EdgeSyncCursor { - List fetchers = new LinkedList<>(); + private final List fetchers = new LinkedList<>(); - int currentIdx = 0; + private int currentIdx = 0; public EdgeSyncCursor(EdgeContextComponent ctx, Edge edge, boolean fullSync) { if (fullSync) { @@ -53,8 +54,10 @@ public class EdgeSyncCursor { fetchers.add(new DeviceProfilesEdgeEventFetcher(ctx.getDeviceProfileService())); fetchers.add(new AssetProfilesEdgeEventFetcher(ctx.getAssetProfileService())); fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService())); + Customer publicCustomer = ctx.getCustomerService().findOrCreatePublicCustomer(edge.getTenantId()); + fetchers.add(new CustomerEdgeEventFetcher(publicCustomer.getId())); if (edge.getCustomerId() != null && !EntityId.NULL_UUID.equals(edge.getCustomerId().getId())) { - fetchers.add(new CustomerEdgeEventFetcher()); + fetchers.add(new CustomerEdgeEventFetcher(edge.getCustomerId())); fetchers.add(new CustomerUsersEdgeEventFetcher(ctx.getUserService(), edge.getCustomerId())); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetProfileMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetProfileMsgConstructor.java index ec71217955..60a1cbd6cb 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetProfileMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetProfileMsgConstructor.java @@ -49,6 +49,10 @@ public class AssetProfileMsgConstructor { if (assetProfile.getImage() != null) { builder.setImage(ByteString.copyFrom(assetProfile.getImage().getBytes(StandardCharsets.UTF_8))); } + if (assetProfile.getDefaultEdgeRuleChainId() != null) { + builder.setDefaultRuleChainIdMSB(assetProfile.getDefaultEdgeRuleChainId().getId().getMostSignificantBits()) + .setDefaultRuleChainIdLSB(assetProfile.getDefaultEdgeRuleChainId().getId().getLeastSignificantBits()); + } return builder.build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java index 522bcd3a4e..5af2a1e737 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java @@ -66,6 +66,10 @@ public class DeviceMsgConstructor { builder.setFirmwareIdMSB(device.getFirmwareId().getId().getMostSignificantBits()) .setFirmwareIdLSB(device.getFirmwareId().getId().getLeastSignificantBits()); } + if (device.getSoftwareId() != null) { + builder.setSoftwareIdMSB(device.getSoftwareId().getId().getMostSignificantBits()) + .setSoftwareIdLSB(device.getSoftwareId().getId().getLeastSignificantBits()); + } if (conflictName != null) { builder.setConflictName(conflictName); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceProfileMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceProfileMsgConstructor.java index 7865adb292..0295d2bacd 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceProfileMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceProfileMsgConstructor.java @@ -65,6 +65,18 @@ public class DeviceProfileMsgConstructor { builder.setFirmwareIdMSB(deviceProfile.getFirmwareId().getId().getMostSignificantBits()) .setFirmwareIdLSB(deviceProfile.getFirmwareId().getId().getLeastSignificantBits()); } + if (deviceProfile.getSoftwareId() != null) { + builder.setSoftwareIdMSB(deviceProfile.getSoftwareId().getId().getMostSignificantBits()) + .setSoftwareIdLSB(deviceProfile.getSoftwareId().getId().getLeastSignificantBits()); + } + if (deviceProfile.getDefaultEdgeRuleChainId() != null) { + builder.setDefaultRuleChainIdMSB(deviceProfile.getDefaultEdgeRuleChainId().getId().getMostSignificantBits()) + .setDefaultRuleChainIdLSB(deviceProfile.getDefaultEdgeRuleChainId().getId().getLeastSignificantBits()); + } + if (deviceProfile.getDefaultDashboardId() != null) { + builder.setDefaultDashboardIdMSB(deviceProfile.getDefaultDashboardId().getId().getMostSignificantBits()) + .setDefaultDashboardIdLSB(deviceProfile.getDefaultDashboardId().getId().getLeastSignificantBits()); + } return builder.build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerEdgeEventFetcher.java index de88c2f05b..23848798a3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerEdgeEventFetcher.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; 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.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -33,6 +34,8 @@ import java.util.List; @AllArgsConstructor public class CustomerEdgeEventFetcher implements EdgeEventFetcher { + private final CustomerId customerId; + @Override public PageLink getPageLink(int pageSize) { return null; @@ -42,7 +45,7 @@ public class CustomerEdgeEventFetcher implements EdgeEventFetcher { public PageData fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) { List result = new ArrayList<>(); result.add(EdgeUtils.constructEdgeEvent(edge.getTenantId(), edge.getId(), - EdgeEventType.CUSTOMER, EdgeEventActionType.ADDED, edge.getCustomerId(), null)); + EdgeEventType.CUSTOMER, EdgeEventActionType.ADDED, customerId, null)); // @voba - returns PageData object to be in sync with other fetchers return new PageData<>(result, 1, result.size(), false); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index 3b39520606..9776cf3a4d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -306,6 +306,12 @@ public abstract class BaseEdgeProcessor { return futures; } + protected ListenableFuture handleUnsupportedMsgType(UpdateMsgType msgType) { + String errMsg = String.format("Unsupported msg type %s", msgType); + log.error(errMsg); + return Futures.immediateFailedFuture(new RuntimeException(errMsg)); + } + protected UpdateMsgType getUpdateMsgType(EdgeEventActionType actionType) { switch (actionType) { case UPDATED: diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java index 02aca5165c..963f26212a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java @@ -132,8 +132,7 @@ public class DeviceEdgeProcessor extends BaseEdgeProcessor { return Futures.immediateFuture(null); case UNRECOGNIZED: default: - log.error("Unsupported msg type {}", deviceUpdateMsg.getMsgType()); - return Futures.immediateFailedFuture(new RuntimeException("Unsupported msg type " + deviceUpdateMsg.getMsgType())); + return handleUnsupportedMsgType(deviceUpdateMsg.getMsgType()); } } @@ -246,11 +245,9 @@ public class DeviceEdgeProcessor extends BaseEdgeProcessor { if (deviceDataOpt.isPresent()) { device.setDeviceData(deviceDataOpt.get()); } + deviceValidator.validate(device, Device::getTenantId); if (created) { - deviceValidator.validate(device, Device::getTenantId); device.setId(deviceId); - } else { - deviceValidator.validate(device, Device::getTenantId); } Device savedDevice = deviceService.saveDevice(device, false); tbClusterService.onDeviceUpdated(savedDevice, created ? null : device, false); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/RelationEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/RelationEdgeProcessor.java index 8b07d771d6..b2bec3559d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/RelationEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/RelationEdgeProcessor.java @@ -80,23 +80,25 @@ public class RelationEdgeProcessor extends BaseEdgeProcessor { case ENTITY_UPDATED_RPC_MESSAGE: if (isEntityExists(tenantId, entityRelation.getTo()) && isEntityExists(tenantId, entityRelation.getFrom())) { - relationService.saveRelationAsync(tenantId, entityRelation); + return Futures.transform(relationService.saveRelationAsync(tenantId, entityRelation), + (result) -> null, dbCallbackExecutorService); + } else { + log.warn("Skipping relating update msg because from/to entity doesn't exists on edge, {}", relationUpdateMsg); + return Futures.immediateFuture(null); } - break; case ENTITY_DELETED_RPC_MESSAGE: - relationService.deleteRelation(tenantId, entityRelation); - break; + return Futures.transform(relationService.deleteRelationAsync(tenantId, entityRelation), + (result) -> null, dbCallbackExecutorService); case UNRECOGNIZED: - log.error("Unsupported msg type"); + default: + return handleUnsupportedMsgType(relationUpdateMsg.getMsgType()); } - return Futures.immediateFuture(null); } catch (Exception e) { log.error("Failed to process relation update msg [{}]", relationUpdateMsg, e); return Futures.immediateFailedFuture(new RuntimeException("Failed to process relation update msg", e)); } } - private boolean isEntityExists(TenantId tenantId, EntityId entityId) throws ThingsboardException { switch (entityId.getEntityType()) { case DEVICE: 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 481ee7762e..7e1a459e3a 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 @@ -22,6 +22,7 @@ import com.google.common.util.concurrent.SettableFuture; import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; @@ -334,7 +335,7 @@ public class TelemetryEdgeProcessor extends BaseEdgeProcessor { return null; } return constructEntityDataProtoMsg(entityId, edgeEvent.getAction(), - JsonUtils.parse(JacksonUtil.OBJECT_MAPPER.writeValueAsString(edgeEvent.getBody()))); + JsonParser.parseString(JacksonUtil.OBJECT_MAPPER.writeValueAsString(edgeEvent.getBody()))); } private DownlinkMsg constructEntityDataProtoMsg(EntityId entityId, EdgeEventActionType actionType, JsonElement entityData) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java index 2f72d96770..af299c84c4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java @@ -137,49 +137,50 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { } String scope = attributesRequestMsg.getScope(); ListenableFuture> findAttrFuture = attributesService.findAll(tenantId, entityId, scope); - return Futures.transformAsync(findAttrFuture, ssAttributes -> { - if (ssAttributes == null || ssAttributes.isEmpty()) { - log.trace("[{}][{}] No attributes found for entity {} [{}]", tenantId, - edge.getName(), - entityId.getEntityType(), - entityId.getId()); - return Futures.immediateFuture(null); - } - return processEntityAttributesAndAddToEdgeQueue(tenantId, entityId, edge, entityType, scope, ssAttributes, attributesRequestMsg); - }, dbCallbackExecutorService); + return Futures.transformAsync(findAttrFuture, ssAttributes + -> processEntityAttributesAndAddToEdgeQueue(tenantId, entityId, edge, entityType, scope, ssAttributes, attributesRequestMsg), + dbCallbackExecutorService); } private ListenableFuture processEntityAttributesAndAddToEdgeQueue(TenantId tenantId, EntityId entityId, Edge edge, EdgeEventType entityType, String scope, List ssAttributes, AttributesRequestMsg attributesRequestMsg) { try { - Map entityData = new HashMap<>(); - ObjectNode attributes = JacksonUtil.OBJECT_MAPPER.createObjectNode(); - for (AttributeKvEntry attr : ssAttributes) { - if (DefaultDeviceStateService.PERSISTENT_ATTRIBUTES.contains(attr.getKey()) - && !DefaultDeviceStateService.INACTIVITY_TIMEOUT.equals(attr.getKey())) { - continue; + ListenableFuture future; + if (ssAttributes == null || ssAttributes.isEmpty()) { + log.trace("[{}][{}] No attributes found for entity {} [{}]", tenantId, + edge.getName(), + entityId.getEntityType(), + entityId.getId()); + future = Futures.immediateFuture(null); + } else { + Map entityData = new HashMap<>(); + ObjectNode attributes = JacksonUtil.OBJECT_MAPPER.createObjectNode(); + for (AttributeKvEntry attr : ssAttributes) { + if (DefaultDeviceStateService.PERSISTENT_ATTRIBUTES.contains(attr.getKey()) + && !DefaultDeviceStateService.INACTIVITY_TIMEOUT.equals(attr.getKey())) { + continue; + } + if (attr.getDataType() == DataType.BOOLEAN && attr.getBooleanValue().isPresent()) { + attributes.put(attr.getKey(), attr.getBooleanValue().get()); + } else if (attr.getDataType() == DataType.DOUBLE && attr.getDoubleValue().isPresent()) { + attributes.put(attr.getKey(), attr.getDoubleValue().get()); + } else if (attr.getDataType() == DataType.LONG && attr.getLongValue().isPresent()) { + attributes.put(attr.getKey(), attr.getLongValue().get()); + } else { + attributes.put(attr.getKey(), attr.getValueAsString()); + } } - if (attr.getDataType() == DataType.BOOLEAN && attr.getBooleanValue().isPresent()) { - attributes.put(attr.getKey(), attr.getBooleanValue().get()); - } else if (attr.getDataType() == DataType.DOUBLE && attr.getDoubleValue().isPresent()) { - attributes.put(attr.getKey(), attr.getDoubleValue().get()); - } else if (attr.getDataType() == DataType.LONG && attr.getLongValue().isPresent()) { - attributes.put(attr.getKey(), attr.getLongValue().get()); + if (attributes.size() > 0) { + entityData.put("kv", attributes); + entityData.put("scope", scope); + JsonNode body = JacksonUtil.OBJECT_MAPPER.valueToTree(entityData); + log.debug("Sending attributes data msg, entityId [{}], attributes [{}]", entityId, body); + future = saveEdgeEvent(tenantId, edge.getId(), entityType, EdgeEventActionType.ATTRIBUTES_UPDATED, entityId, body); } else { - attributes.put(attr.getKey(), attr.getValueAsString()); + future = Futures.immediateFuture(null); } } - ListenableFuture future; - if (attributes.size() > 0) { - entityData.put("kv", attributes); - entityData.put("scope", scope); - JsonNode body = JacksonUtil.OBJECT_MAPPER.valueToTree(entityData); - log.debug("Sending attributes data msg, entityId [{}], attributes [{}]", entityId, body); - future = saveEdgeEvent(tenantId, edge.getId(), entityType, EdgeEventActionType.ATTRIBUTES_UPDATED, entityId, body); - } else { - future = Futures.immediateFuture(null); - } return Futures.transformAsync(future, v -> processLatestTimeseriesAndAddToEdgeQueue(tenantId, entityId, edge, entityType), dbCallbackExecutorService); } catch (Exception e) { String errMsg = String.format("[%s] Failed to save attribute updates to the edge [%s]", edge.getId(), attributesRequestMsg); @@ -199,16 +200,18 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { entityId.getId()); return Futures.immediateFuture(null); } - List> futures = new ArrayList<>(); + Map> tsData = new HashMap<>(); for (TsKvEntry tsKvEntry : tsKvEntries) { if (DefaultDeviceStateService.PERSISTENT_ATTRIBUTES.contains(tsKvEntry.getKey())) { continue; } - ObjectNode entityBody = JacksonUtil.OBJECT_MAPPER.createObjectNode(); - ObjectNode ts = JacksonUtil.OBJECT_MAPPER.createObjectNode(); - ts.put(tsKvEntry.getKey(), tsKvEntry.getValueAsString()); - entityBody.set("data", ts); - entityBody.put("ts", tsKvEntry.getTs()); + tsData.computeIfAbsent(tsKvEntry.getTs(), k -> new HashMap<>()).put(tsKvEntry.getKey(), tsKvEntry.getValue()); + } + List> futures = new ArrayList<>(); + for (Map.Entry> entry : tsData.entrySet()) { + Map entityBody = new HashMap<>(); + entityBody.put("data", entry.getValue()); + entityBody.put("ts", entry.getKey()); futures.add(saveEdgeEvent(tenantId, edge.getId(), entityType, EdgeEventActionType.TIMESERIES_UPDATED, entityId, JacksonUtil.valueToTree(entityBody))); } return Futures.transform(Futures.allAsList(futures), v -> null, dbCallbackExecutorService); 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 9f34c061fe..10af591647 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 @@ -34,6 +34,7 @@ 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.TimePageLink; +import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.edge.EdgeService; @@ -65,6 +66,8 @@ public abstract class AbstractTbEntityService { @Autowired protected AlarmSubscriptionService alarmSubscriptionService; @Autowired + protected AlarmCommentService alarmCommentService; + @Autowired protected CustomerService customerService; @Autowired protected TbClusterService tbClusterService; 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 8f8531a5b4..441c183343 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.User; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -130,13 +131,9 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS public void notifyAssignOrUnassignEntityToCustomer(TenantId tenantId, I entityId, CustomerId customerId, E entity, ActionType actionType, - User user, boolean sendToEdge, - Object... additionalInfo) { + User user, Object... additionalInfo) { logEntityAction(tenantId, entityId, entity, customerId, actionType, user, additionalInfo); - - if (sendToEdge) { - sendEntityNotificationMsg(tenantId, entityId, edgeTypeByActionType(actionType), JacksonUtil.toString(customerId)); - } + sendEntityNotificationMsg(tenantId, entityId, edgeTypeByActionType(actionType), JacksonUtil.toString(customerId)); } @Override @@ -229,6 +226,11 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS sendEntityNotificationMsg(alarm.getTenantId(), alarm.getId(), edgeTypeByActionType(actionType)); } + @Override + public void notifyAlarmComment(Alarm alarm, AlarmComment alarmComment, ActionType actionType, User user) { + logEntityAction(alarm.getTenantId(), alarm.getId(), alarm, alarm.getCustomerId(), actionType, user, alarmComment); + } + @Override public void notifyCreateOrUpdateOrDelete(TenantId tenantId, CustomerId customerId, I entityId, E entity, User user, 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 dc6e148cab..7058cdb6f7 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.User; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -73,8 +74,7 @@ public interface TbNotificationEntityService { void notifyAssignOrUnassignEntityToCustomer(TenantId tenantId, I entityId, CustomerId customerId, E entity, ActionType actionType, - User user, boolean sendToEdge, - Object... additionalInfo); + User user, Object... additionalInfo); void notifyAssignOrUnassignEntityToEdge(TenantId tenantId, I entityId, CustomerId customerId, EdgeId edgeId, @@ -102,6 +102,9 @@ public interface TbNotificationEntityService { void notifyCreateOrUpdateAlarm(Alarm alarm, ActionType actionType, User user, Object... additionalInfo); + void notifyAlarmComment(Alarm alarm, AlarmComment alarmComment, ActionType actionType, User user); + + void notifyCreateOrUpdateOrDelete(TenantId tenantId, CustomerId customerId, I entityId, E entity, User user, ActionType actionType, boolean sendNotifyMsgToEdge, diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java new file mode 100644 index 0000000000..ee98af5f0f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java @@ -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. + */ +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.User; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.service.entitiy.AbstractTbEntityService; + +@Service +@AllArgsConstructor +public class DefaultTbAlarmCommentService extends AbstractTbEntityService implements TbAlarmCommentService{ + @Override + public AlarmComment saveAlarmComment(Alarm alarm, AlarmComment alarmComment, User user) throws ThingsboardException { + ActionType actionType = alarmComment.getId() == null ? ActionType.ADDED_COMMENT : ActionType.UPDATED_COMMENT; + UserId userId = user.getId(); + alarmComment.setUserId(userId); + try { + AlarmComment savedAlarmComment = checkNotNull(alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment)); + notificationEntityService.notifyAlarmComment(alarm, savedAlarmComment, actionType, user); + return savedAlarmComment; + } catch (Exception e) { + notificationEntityService.logEntityAction(alarm.getTenantId(), emptyId(EntityType.ALARM), alarm, actionType, user, e, alarmComment); + throw e; + } + } + + @Override + public void deleteAlarmComment(Alarm alarm, AlarmComment alarmComment, User user) { + alarmCommentService.deleteAlarmComment(alarm.getTenantId(), alarmComment.getId()); + notificationEntityService.notifyAlarmComment(alarm, alarmComment, ActionType.DELETED_COMMENT, user); + } +} 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 8157eff3e5..7e778fd840 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 @@ -24,6 +24,8 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentType; import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -56,6 +58,14 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb long ackTs = System.currentTimeMillis(); ListenableFuture future = alarmSubscriptionService.ackAlarm(alarm.getTenantId(), alarm.getId(), ackTs); return Futures.transform(future, result -> { + AlarmComment alarmComment = AlarmComment.builder() + .alarmId(alarm.getId()) + .type(AlarmCommentType.SYSTEM) + .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was acknowledged by user %s", + (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) + .put("userId", user.getId().toString())) + .build(); + alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment); alarm.setAckTs(ackTs); alarm.setStatus(alarm.getStatus().isCleared() ? AlarmStatus.CLEARED_ACK : AlarmStatus.ACTIVE_ACK); notificationEntityService.notifyCreateOrUpdateAlarm(alarm, ActionType.ALARM_ACK, user); @@ -68,6 +78,14 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb long clearTs = System.currentTimeMillis(); ListenableFuture future = alarmSubscriptionService.clearAlarm(alarm.getTenantId(), alarm.getId(), null, clearTs); return Futures.transform(future, result -> { + AlarmComment alarmComment = AlarmComment.builder() + .alarmId(alarm.getId()) + .type(AlarmCommentType.SYSTEM) + .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was cleared by user %s", + (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) + .put("userId", user.getId().toString())) + .build(); + alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment); alarm.setClearTs(clearTs); alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK); notificationEntityService.notifyCreateOrUpdateAlarm(alarm, ActionType.ALARM_CLEAR, user); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmCommentService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmCommentService.java new file mode 100644 index 0000000000..d8bb798d49 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmCommentService.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.service.entitiy.alarm; + +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.exception.ThingsboardException; + +public interface TbAlarmCommentService { + AlarmComment saveAlarmComment(Alarm alarm, AlarmComment alarmComment, User user) throws ThingsboardException; + + void deleteAlarmComment(Alarm alarm, AlarmComment alarmComment, User user); +} 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 22edb2a01f..14a312b403 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 @@ -98,7 +98,7 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb try { Asset savedAsset = checkNotNull(assetService.assignAssetToCustomer(tenantId, assetId, customerId)); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, assetId, customerId, savedAsset, - actionType, user, true, assetId.toString(), customerId.toString(), customer.getName()); + actionType, user, assetId.toString(), customerId.toString(), customer.getName()); return savedAsset; } catch (Exception e) { @@ -115,7 +115,7 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb Asset savedAsset = checkNotNull(assetService.unassignAssetFromCustomer(tenantId, assetId)); CustomerId customerId = customer.getId(); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, assetId, customerId, savedAsset, - actionType, user, true, assetId.toString(), customerId.toString(), customer.getName()); + actionType, user, assetId.toString(), customerId.toString(), customer.getName()); return savedAsset; } catch (Exception e) { @@ -131,7 +131,7 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb Customer publicCustomer = customerService.findOrCreatePublicCustomer(tenantId); Asset savedAsset = checkNotNull(assetService.assignAssetToCustomer(tenantId, assetId, publicCustomer.getId())); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, assetId, savedAsset.getCustomerId(), savedAsset, - actionType, user, false, actionType.toString(), publicCustomer.getId().toString(), publicCustomer.getName()); + actionType, user, assetId.toString(), publicCustomer.getId().toString(), publicCustomer.getName()); return savedAsset; } 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 7fadab17ba..1f78b67716 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 @@ -84,7 +84,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement try { Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(tenantId, dashboardId, customerId)); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, customerId, savedDashboard, - actionType, user, true, dashboardId.toString(), customerId.toString(), customer.getName()); + actionType, user, dashboardId.toString(), customerId.toString(), customer.getName()); return savedDashboard; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.DASHBOARD), actionType, @@ -102,7 +102,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement Customer publicCustomer = customerService.findOrCreatePublicCustomer(tenantId); Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(tenantId, dashboardId, publicCustomer.getId())); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, publicCustomer.getId(), savedDashboard, - actionType, user, false, dashboardId.toString(), + actionType, user, dashboardId.toString(), publicCustomer.getId().toString(), publicCustomer.getName()); return savedDashboard; } catch (Exception e) { @@ -120,7 +120,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement Customer publicCustomer = customerService.findOrCreatePublicCustomer(tenantId); Dashboard savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(tenantId, dashboardId, publicCustomer.getId())); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, publicCustomer.getId(), dashboard, - actionType, user, false, dashboardId.toString(), + actionType, user, dashboardId.toString(), publicCustomer.getId().toString(), publicCustomer.getName()); return savedDashboard; } catch (Exception e) { @@ -160,14 +160,14 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(tenantId, dashboardId, customerId)); ShortCustomerInfo customerInfo = savedDashboard.getAssignedCustomerInfo(customerId); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, savedDashboard.getId(), customerId, savedDashboard, - actionType, user, true, dashboardId.toString(), customerId.toString(), customerInfo.getTitle()); + actionType, user, dashboardId.toString(), customerId.toString(), customerInfo.getTitle()); } actionType = ActionType.UNASSIGNED_FROM_CUSTOMER; for (CustomerId customerId : removedCustomerIds) { ShortCustomerInfo customerInfo = dashboard.getAssignedCustomerInfo(customerId); savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(tenantId, dashboardId, customerId)); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, savedDashboard.getId(), customerId, savedDashboard, - ActionType.UNASSIGNED_FROM_CUSTOMER, user, true, dashboardId.toString(), customerId.toString(), customerInfo.getTitle()); + ActionType.UNASSIGNED_FROM_CUSTOMER, user, dashboardId.toString(), customerId.toString(), customerInfo.getTitle()); } return savedDashboard; } @@ -197,7 +197,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(tenantId, dashboardId, customerId)); ShortCustomerInfo customerInfo = savedDashboard.getAssignedCustomerInfo(customerId); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, customerId, savedDashboard, - actionType, user, true, dashboardId.toString(), customerId.toString(), customerInfo.getTitle()); + actionType, user, dashboardId.toString(), customerId.toString(), customerInfo.getTitle()); } return savedDashboard; } @@ -227,7 +227,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement ShortCustomerInfo customerInfo = dashboard.getAssignedCustomerInfo(customerId); savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(tenantId, dashboardId, customerId)); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, customerId, savedDashboard, - actionType, user, true, dashboardId.toString(), customerId.toString(), customerInfo.getTitle()); + actionType, user, dashboardId.toString(), customerId.toString(), customerInfo.getTitle()); } return savedDashboard; } @@ -282,7 +282,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement try { Dashboard savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(tenantId, dashboardId, customer.getId())); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, customer.getId(), savedDashboard, - actionType, user, true, dashboardId.toString(), customer.getId().toString(), customer.getName()); + actionType, user, dashboardId.toString(), customer.getId().toString(), customer.getName()); return savedDashboard; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.DASHBOARD), actionType, user, e, dashboardId.toString()); 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 25053676fd..704d3e0e0b 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 @@ -117,7 +117,7 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T try { Device savedDevice = checkNotNull(deviceService.assignDeviceToCustomer(tenantId, deviceId, customerId)); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, deviceId, customerId, savedDevice, - actionType, user, true, deviceId.toString(), customerId.toString(), customer.getName()); + actionType, user, deviceId.toString(), customerId.toString(), customer.getName()); return savedDevice; } catch (Exception e) { @@ -137,7 +137,7 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T CustomerId customerId = customer.getId(); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, deviceId, customerId, savedDevice, - actionType, user, true, deviceId.toString(), customerId.toString(), customer.getName()); + actionType, user, deviceId.toString(), customerId.toString(), customer.getName()); return savedDevice; } catch (Exception e) { @@ -155,7 +155,7 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T Device savedDevice = checkNotNull(deviceService.assignDeviceToCustomer(tenantId, deviceId, publicCustomer.getId())); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, deviceId, savedDevice.getCustomerId(), savedDevice, - actionType, user, false, deviceId.toString(), + actionType, user, deviceId.toString(), publicCustomer.getId().toString(), publicCustomer.getName()); return savedDevice; 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 b3b70a174e..7eb34c7008 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 @@ -90,7 +90,7 @@ public class DefaultTbEdgeService extends AbstractTbEntityService implements TbE try { Edge savedEdge = checkNotNull(edgeService.assignEdgeToCustomer(tenantId, edgeId, customerId)); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, edgeId, customerId, savedEdge, - actionType, user, true, edgeId.toString(), customerId.toString(), customer.getName()); + actionType, user, edgeId.toString(), customerId.toString(), customer.getName()); return savedEdge; } catch (Exception e) { @@ -109,7 +109,7 @@ public class DefaultTbEdgeService extends AbstractTbEntityService implements TbE try { Edge savedEdge = checkNotNull(edgeService.unassignEdgeFromCustomer(tenantId, edgeId)); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, edgeId, customerId, savedEdge, - actionType, user, true, edgeId.toString(), customerId.toString(), customer.getName()); + actionType, user, edgeId.toString(), customerId.toString(), customer.getName()); return savedEdge; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.EDGE), diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java index 3ae5fe67f2..f9cbf60f38 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java @@ -149,7 +149,7 @@ public class DefaultTbEntityViewService extends AbstractTbEntityService implemen try { EntityView savedEntityView = checkNotNull(entityViewService.assignEntityViewToCustomer(tenantId, entityViewId, customerId)); notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, entityViewId, customerId, savedEntityView, - ActionType.ASSIGNED_TO_CUSTOMER, user, true, entityViewId.toString(), customerId.toString(), customer.getName()); + ActionType.ASSIGNED_TO_CUSTOMER, user, entityViewId.toString(), customerId.toString(), customer.getName()); return savedEntityView; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ENTITY_VIEW), @@ -159,18 +159,19 @@ public class DefaultTbEntityViewService extends AbstractTbEntityService implemen } @Override - public EntityView assignEntityViewToPublicCustomer(TenantId tenantId, CustomerId customerId, Customer publicCustomer, - EntityViewId entityViewId, User user) throws ThingsboardException { + public EntityView assignEntityViewToPublicCustomer(TenantId tenantId, EntityViewId entityViewId, User user) throws ThingsboardException { + ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER; + Customer publicCustomer = customerService.findOrCreatePublicCustomer(tenantId); try { EntityView savedEntityView = checkNotNull(entityViewService.assignEntityViewToCustomer(tenantId, entityViewId, publicCustomer.getId())); - notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, entityViewId, customerId, savedEntityView, - ActionType.ASSIGNED_TO_CUSTOMER, user, false, savedEntityView.getEntityId().toString(), + notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, entityViewId, savedEntityView.getCustomerId(), savedEntityView, + actionType, user, savedEntityView.getId().toString(), publicCustomer.getId().toString(), publicCustomer.getName()); return savedEntityView; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ENTITY_VIEW), - ActionType.ASSIGNED_TO_CUSTOMER, user, e, entityViewId.toString()); + actionType, user, e, entityViewId.toString()); throw e; } } @@ -211,14 +212,17 @@ public class DefaultTbEntityViewService extends AbstractTbEntityService implemen @Override public EntityView unassignEntityViewFromCustomer(TenantId tenantId, EntityViewId entityViewId, Customer customer, User user) throws ThingsboardException { + ActionType actionType = ActionType.UNASSIGNED_FROM_CUSTOMER; try { EntityView savedEntityView = checkNotNull(entityViewService.unassignEntityViewFromCustomer(tenantId, entityViewId)); + notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, entityViewId, customer.getId(), savedEntityView, - ActionType.UNASSIGNED_FROM_CUSTOMER, user, true, customer.getId().toString(), customer.getName()); + actionType, user, savedEntityView.getId().toString(), customer.getId().toString(), customer.getName()); + return savedEntityView; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ENTITY_VIEW), - ActionType.UNASSIGNED_FROM_CUSTOMER, user, e, entityViewId.toString()); + actionType, user, e, entityViewId.toString()); throw e; } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java index c4dbedd2eb..6bdefc74f5 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java @@ -39,8 +39,7 @@ public interface TbEntityViewService extends ComponentLifecycleListener { EntityView assignEntityViewToCustomer(TenantId tenantId, EntityViewId entityViewId, Customer customer, User user) throws ThingsboardException; - EntityView assignEntityViewToPublicCustomer(TenantId tenantId, CustomerId customerId, Customer publicCustomer, - EntityViewId entityViewId, User user) throws ThingsboardException; + EntityView assignEntityViewToPublicCustomer(TenantId tenantId, EntityViewId entityViewId, User user) throws ThingsboardException; EntityView assignEntityViewToEdge(TenantId tenantId, CustomerId customerId, EntityViewId entityViewId, Edge edge, User user) throws ThingsboardException; 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 80665d544f..b1eb0a004f 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 @@ -677,6 +677,37 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService log.error("Failed updating schema!!!", e); } break; + case "3.4.3": + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + log.info("Updating schema ..."); + if (isOldSchema(conn, 3004002)) { + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.4.3", SCHEMA_UPDATE_SQL); + loadSql(schemaUpdateFile, conn); + + try { + conn.createStatement().execute("ALTER TABLE asset_profile ADD COLUMN default_edge_rule_chain_id uuid"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script + } catch (Exception e) { + } + try { + conn.createStatement().execute("ALTER TABLE device_profile ADD COLUMN default_edge_rule_chain_id uuid"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script + } catch (Exception e) { + } + try { + conn.createStatement().execute("ALTER TABLE asset_profile ADD CONSTRAINT fk_default_edge_rule_chain_asset_profile FOREIGN KEY (default_edge_rule_chain_id) REFERENCES rule_chain(id)"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script + } catch (Exception e) { + } + try { + conn.createStatement().execute("ALTER TABLE device_profile ADD CONSTRAINT fk_default_edge_rule_chain_device_profile FOREIGN KEY (default_edge_rule_chain_id) REFERENCES rule_chain(id)"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script + } catch (Exception e) { + } + + conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3005000;"); + } + 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/telemetry/DefaultAlarmSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java index 956dde8024..0b6eaf162e 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java @@ -23,8 +23,11 @@ import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentType; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; @@ -41,6 +44,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.stats.TbApiUsageReportClient; +import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmOperationResult; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.gen.transport.TransportProtos; @@ -61,6 +65,7 @@ import java.util.Optional; public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService implements AlarmSubscriptionService { private final AlarmService alarmService; + private final AlarmCommentService alarmCommentService; private final TbApiUsageReportClient apiUsageClient; private final TbApiUsageStateService apiUsageStateService; @@ -68,11 +73,13 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService PartitionService partitionService, AlarmService alarmService, TbApiUsageReportClient apiUsageClient, - TbApiUsageStateService apiUsageStateService) { + TbApiUsageStateService apiUsageStateService, + AlarmCommentService alarmCommentService) { super(clusterService, partitionService); this.alarmService = alarmService; this.apiUsageClient = apiUsageClient; this.apiUsageStateService = apiUsageStateService; + this.alarmCommentService = alarmCommentService; } @Autowired(required = false) @@ -90,6 +97,15 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService AlarmOperationResult result = alarmService.createOrUpdateAlarm(alarm, apiUsageStateService.getApiUsageState(alarm.getTenantId()).isAlarmCreationEnabled()); if (result.isSuccessful()) { onAlarmUpdated(result); + AlarmSeverity oldSeverity = result.getOldSeverity(); + if (oldSeverity != null && !oldSeverity.equals(result.getAlarm().getSeverity())) { + AlarmComment alarmComment = AlarmComment.builder() + .alarmId(alarm.getId()) + .type(AlarmCommentType.SYSTEM) + .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm severity was updated from %s to %s", oldSeverity, result.getAlarm().getSeverity()))) + .build(); + alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment); + } } if (result.isCreated()) { apiUsageClient.report(alarm.getTenantId(), null, ApiUsageRecordKey.CREATED_ALARMS_COUNT); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 47273d03b5..c17d03b312 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -277,6 +277,8 @@ sql: partition_size: "${SQL_EDGE_EVENTS_PARTITION_SIZE_HOURS:168}" # Number of hours to partition the events. The current value corresponds to one week. audit_logs: partition_size: "${SQL_AUDIT_LOGS_PARTITION_SIZE_HOURS:168}" # Default value - 1 week + alarm_comments: + partition_size: "${SQL_ALARM_COMMENTS_PARTITION_SIZE_HOURS:168}" # Default value - 1 week # Specify whether to sort entities before batch update. Should be enabled for cluster mode to avoid deadlocks batch_sort: "${SQL_BATCH_SORT:true}" # Specify whether to remove null characters from strValue of attributes and timeseries before insert diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java index 8de7157b52..672e55825b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java @@ -367,7 +367,7 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { Mockito.verify(tbClusterService, times(cntTime)).pushMsgToCore(Mockito.any(ToDeviceActorNotificationMsg.class), Mockito.isNull()); } - private void testLogEntityAction(HasName entity, EntityId originatorId, TenantId tenantId, + protected void testLogEntityAction(HasName entity, EntityId originatorId, TenantId tenantId, CustomerId customerId, UserId userId, String userName, ActionType actionType, int cntTime, Object... additionalInfo) { ArgumentMatcher matcherEntityEquals = entity == null ? Objects::isNull : argument -> argument.toString().equals(entity.toString()); 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 9ea269918e..dfe2daba37 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -702,16 +702,16 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { } protected Edge constructEdge(String name, String type) { - return constructEdge(tenantId, name, type); + return constructEdge(tenantId, name, type, StringUtils.randomAlphanumeric(20), StringUtils.randomAlphanumeric(20)); } - protected Edge constructEdge(TenantId tenantId, String name, String type) { + protected Edge constructEdge(TenantId tenantId, String name, String type, String routingKey, String secret) { Edge edge = new Edge(); edge.setTenantId(tenantId); edge.setName(name); edge.setType(type); - edge.setSecret(StringUtils.randomAlphanumeric(20)); - edge.setRoutingKey(StringUtils.randomAlphanumeric(20)); + edge.setRoutingKey(routingKey); + edge.setSecret(secret); return edge; } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAlarmCommentControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAlarmCommentControllerTest.java new file mode 100644 index 0000000000..337abee74f --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAlarmCommentControllerTest.java @@ -0,0 +1,363 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.TextNode; +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.AdditionalAnswers; +import org.mockito.Mockito; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.test.context.ContextConfiguration; +import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; +import org.thingsboard.server.common.data.alarm.AlarmCommentType; +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.id.AlarmId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.dao.alarm.AlarmDao; + +import java.util.LinkedList; +import java.util.List; + +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +@ContextConfiguration(classes = {BaseAlarmCommentControllerTest.Config.class}) +public abstract class BaseAlarmCommentControllerTest extends AbstractControllerTest { + + protected Device customerDevice; + protected Alarm alarm; + + static class Config { + @Bean + @Primary + public AlarmDao alarmDao(AlarmDao alarmDao) { + return Mockito.mock(AlarmDao.class, AdditionalAnswers.delegatesTo(alarmDao)); + } + } + + @Before + public void setup() throws Exception { + loginTenantAdmin(); + + Device device = new Device(); + device.setTenantId(tenantId); + device.setName("Test device"); + device.setLabel("Label"); + device.setType("Type"); + device.setCustomerId(customerId); + customerDevice = doPost("/api/device", device, Device.class); + + alarm = Alarm.builder() + .tenantId(tenantId) + .customerId(customerId) + .originator(customerDevice.getId()) + .status(AlarmStatus.ACTIVE_UNACK) + .severity(AlarmSeverity.CRITICAL) + .type("test alarm type") + .build(); + + alarm = doPost("/api/alarm", alarm, Alarm.class); + + resetTokens(); + } + + @After + public void teardown() throws Exception { + Mockito.reset(tbClusterService, auditLogService); + loginSysAdmin(); + deleteDifferentTenant(); + } + + @Test + public void testCreateAlarmCommentViaCustomer() throws Exception { + loginCustomerUser(); + + Mockito.reset(tbClusterService, auditLogService); + + AlarmComment createdComment = createAlarmComment(alarm.getId()); + + testLogEntityAction(alarm, alarm.getId(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ADDED_COMMENT, 1, createdComment); + } + + @Test + public void testCreateAlarmCommentViaTenant() throws Exception { + loginTenantAdmin(); + + Mockito.reset(tbClusterService, auditLogService); + + AlarmComment createdComment = createAlarmComment(alarm.getId()); + Assert.assertEquals(AlarmCommentType.OTHER, createdComment.getType()); + + testLogEntityAction(alarm, alarm.getId(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ADDED_COMMENT, 1, createdComment); + } + + @Test + public void testUpdateAlarmCommentViaCustomer() throws Exception { + loginCustomerUser(); + AlarmComment savedComment = createAlarmComment(alarm.getId()); + + Mockito.reset(tbClusterService, auditLogService); + + JsonNode newComment = JacksonUtil.newObjectNode().set("text", new TextNode("Updated comment")); + savedComment.setComment(newComment); + AlarmComment updatedAlarmComment = saveAlarmComment(alarm.getId(), savedComment); + + Assert.assertNotNull(updatedAlarmComment); + Assert.assertEquals(newComment.get("text"), updatedAlarmComment.getComment().get("text")); + Assert.assertEquals("true", updatedAlarmComment.getComment().get("edited").asText()); + Assert.assertNotNull(updatedAlarmComment.getComment().get("editedOn")); + + testLogEntityAction(alarm, alarm.getId(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.UPDATED_COMMENT, 1, savedComment); + } + + @Test + public void testUpdateAlarmViaTenant() throws Exception { + loginTenantAdmin(); + AlarmComment savedComment = createAlarmComment(alarm.getId()); + + Mockito.reset(tbClusterService, auditLogService); + + JsonNode newComment = JacksonUtil.newObjectNode().set("text", new TextNode("Updated comment")); + savedComment.setComment(newComment); + AlarmComment updatedAlarmComment = saveAlarmComment(alarm.getId(), savedComment); + + Assert.assertNotNull(updatedAlarmComment); + Assert.assertEquals(newComment.get("text"), updatedAlarmComment.getComment().get("text")); + Assert.assertEquals("true", updatedAlarmComment.getComment().get("edited").asText()); + Assert.assertNotNull(updatedAlarmComment.getComment().get("editedOn")); + + testLogEntityAction(alarm, alarm.getId(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.UPDATED_COMMENT, 1, updatedAlarmComment); + } + + @Test + public void testUpdateAlarmViaDifferentTenant() throws Exception { + loginTenantAdmin(); + AlarmComment savedComment = createAlarmComment(alarm.getId()); + + loginDifferentTenant(); + + Mockito.reset(tbClusterService, auditLogService); + JsonNode newComment = JacksonUtil.newObjectNode().set("text", new TextNode("Updated comment")); + savedComment.setComment(newComment); + + doPost("/api/alarm/" + alarm.getId() + "/comment", savedComment) + .andExpect(status().isForbidden()) + .andExpect(statusReason(containsString(msgErrorPermission))); + + testNotifyEntityNever(alarm.getId(), savedComment); + } + + @Test + public void testUpdateAlarmViaDifferentCustomer() throws Exception { + loginCustomerUser(); + AlarmComment savedComment = createAlarmComment(alarm.getId()); + + loginDifferentCustomer(); + + Mockito.reset(tbClusterService, auditLogService); + JsonNode newComment = JacksonUtil.newObjectNode().set("text", new TextNode("Updated comment")); + savedComment.setComment(newComment); + + doPost("/api/alarm/" + alarm.getId() + "/comment", savedComment) + .andExpect(status().isForbidden()) + .andExpect(statusReason(containsString(msgErrorPermission))); + + testNotifyEntityNever(alarm.getId(), savedComment); + } + + @Test + public void testDeleteAlarmСommentViaCustomer() throws Exception { + loginCustomerUser(); + AlarmComment alarmComment = createAlarmComment(alarm.getId()); + + Mockito.reset(tbClusterService, auditLogService); + + doDelete("/api/alarm/" + alarm.getId() + "/comment/" + alarmComment.getId()) + .andExpect(status().isOk()); + + testLogEntityAction(alarm, alarm.getId(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.DELETED_COMMENT, 1, alarmComment); + } + + @Test + public void testDeleteAlarmViaTenant() throws Exception { + loginTenantAdmin(); + AlarmComment alarmComment = createAlarmComment(alarm.getId()); + + Mockito.reset(tbClusterService, auditLogService); + + doDelete("/api/alarm/" + alarm.getId() + "/comment/" + alarmComment.getId()) + .andExpect(status().isOk()); + + testLogEntityAction(alarm, alarm.getId(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.DELETED_COMMENT, 1, alarmComment); + } + + @Test + public void testDeleteAlarmViaDifferentTenant() throws Exception { + loginTenantAdmin(); + AlarmComment alarmComment = createAlarmComment(alarm.getId()); + + loginDifferentTenant(); + + Mockito.reset(tbClusterService, auditLogService); + + doDelete("/api/alarm/" + alarm.getId() + "/comment/" + alarmComment.getId()) + .andExpect(status().isForbidden()) + .andExpect(statusReason(containsString(msgErrorPermission))); + + testNotifyEntityNever(alarm.getId(), alarm); + } + + @Test + public void testDeleteAlarmViaDifferentCustomer() throws Exception { + loginCustomerUser(); + AlarmComment alarmComment = createAlarmComment(alarm.getId()); + + loginDifferentCustomer(); + + Mockito.reset(tbClusterService, auditLogService); + + doDelete("/api/alarm/" + alarm.getId() + "/comment/" + alarmComment.getId()) + .andExpect(status().isForbidden()) + .andExpect(statusReason(containsString(msgErrorPermission))); + + testNotifyEntityNever(alarm.getId(), alarm); + } + + @Test + public void testFindAlarmCommentsViaCustomerUser() throws Exception { + loginCustomerUser(); + + List createdAlarmComments = new LinkedList<>(); + + final int size = 10; + for (int i = 0; i < size; i++) { + createdAlarmComments.add( + createAlarmComment(alarm.getId(), RandomStringUtils.randomAlphanumeric(10)) + ); + } + + var response = doGetTyped( + "/api/alarm/" + alarm.getId() + "/comment?page=0&pageSize=" + size, + new TypeReference>() {} + ); + var foundAlarmCommentInfos = response.getData(); + Assert.assertNotNull("Found pageData is null", foundAlarmCommentInfos); + Assert.assertNotEquals( + "Expected alarms are not found!", + 0, foundAlarmCommentInfos.size() + ); + + boolean allMatch = createdAlarmComments.stream() + .allMatch(alarmComment -> foundAlarmCommentInfos.stream() + .map(AlarmCommentInfo::getComment) + .anyMatch(comment -> alarmComment.getComment().equals(comment)) + ); + Assert.assertTrue("Created alarm comment doesn't match any found!", allMatch); + } + + @Test + public void testFindAlarmsViaDifferentCustomerUser() throws Exception { + loginCustomerUser(); + + final int size = 10; + List createdAlarmComments = new LinkedList<>(); + for (int i = 0; i < size; i++) { + createdAlarmComments.add( + createAlarmComment(alarm.getId(), RandomStringUtils.randomAlphanumeric(10)) + ); + } + + loginDifferentCustomer(); + doGet("/api/alarm/" + alarm.getId() + "/comment?page=0&pageSize=" + size) + .andExpect(status().isForbidden()) + .andExpect(statusReason(containsString(msgErrorPermission))); + } + + @Test + public void testFindAlarmCommentsViaPublicCustomer() throws Exception { + loginTenantAdmin(); + + Device device = new Device(); + device.setName("Test Public Device"); + device.setLabel("Label"); + device.setCustomerId(customerId); + device = doPost("/api/device", device, Device.class); + device = doPost("/api/customer/public/device/" + device.getUuidId(), Device.class); + + String publicId = device.getCustomerId().toString(); + + Alarm alarm = Alarm.builder() + .originator(device.getId()) + .status(AlarmStatus.ACTIVE_UNACK) + .severity(AlarmSeverity.CRITICAL) + .type("Test") + .build(); + + alarm = doPost("/api/alarm", alarm, Alarm.class); + + Mockito.reset(tbClusterService, auditLogService); + AlarmComment alarmComment = createAlarmComment(alarm.getId()); + + resetTokens(); + + JsonNode publicLoginRequest = JacksonUtil.toJsonNode("{\"publicId\": \"" + publicId + "\"}"); + JsonNode tokens = doPost("/api/auth/login/public", publicLoginRequest, JsonNode.class); + this.token = tokens.get("token").asText(); + + PageData pageData = doGetTyped( + "/api/alarm/" + alarm.getId() + "/comment" + "?page=0&pageSize=1", new TypeReference>() {} + ); + + Assert.assertNotNull("Found pageData is null", pageData); + Assert.assertNotEquals("Expected alarms are not found!", 0, pageData.getTotalElements()); + + AlarmCommentInfo alarmCommentInfo = pageData.getData().get(0); + boolean equals = alarmComment.getId().equals(alarmCommentInfo.getId()) && alarmComment.getComment().equals(alarmCommentInfo.getComment()); + Assert.assertTrue("Created alarm doesn't match the found one!", equals); + } + + private AlarmComment createAlarmComment(AlarmId alarmId, String text) { + AlarmComment alarmComment = AlarmComment.builder() + .comment(JacksonUtil.newObjectNode().set("text", new TextNode(text))) + .build(); + + return saveAlarmComment(alarmId, alarmComment); + } + private AlarmComment createAlarmComment(AlarmId alarmId) { + return createAlarmComment(alarmId, "Please take a look"); + } + private AlarmComment saveAlarmComment(AlarmId alarmId, AlarmComment alarmComment) { + alarmComment = doPost("/api/alarm/" + alarmId + "/comment", alarmComment, AlarmComment.class); + Assert.assertNotNull(alarmComment); + + return alarmComment; + } +} diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java index 37cc703d55..b0614dd8e1 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java @@ -398,6 +398,41 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { Assert.assertEquals(ModelConstants.NULL_UUID, foundAsset.getCustomerId().getId()); } + @Test + public void testAssignUnassignAssetToPublicCustomer() throws Exception { + Asset asset = new Asset(); + asset.setName("My asset"); + asset.setType("default"); + Asset savedAsset = doPost("/api/asset", asset, Asset.class); + + Mockito.reset(tbClusterService, auditLogService); + + Asset assignedAsset = doPost("/api/customer/public/asset/" + savedAsset.getId().getId().toString(), Asset.class); + + Customer publicCustomer = doGet("/api/customer/" + assignedAsset.getCustomerId(), Customer.class); + Assert.assertTrue(publicCustomer.isPublic()); + + testNotifyEntityAllOneTime(assignedAsset, assignedAsset.getId(), assignedAsset.getId(), + savedTenant.getId(), publicCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(), + ActionType.ASSIGNED_TO_CUSTOMER, assignedAsset.getId().toString(), publicCustomer.getId().toString(), publicCustomer.getTitle()); + + Asset foundAsset = doGet("/api/asset/" + savedAsset.getId().getId().toString(), Asset.class); + Assert.assertEquals(publicCustomer.getId(), foundAsset.getCustomerId()); + + Mockito.reset(tbClusterService, auditLogService); + + Asset unassignedAsset = + doDelete("/api/customer/asset/" + savedAsset.getId().getId().toString(), Asset.class); + Assert.assertEquals(ModelConstants.NULL_UUID, unassignedAsset.getCustomerId().getId()); + + testNotifyEntityAllOneTime(savedAsset, savedAsset.getId(), savedAsset.getId(), + savedTenant.getId(), publicCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(), + ActionType.UNASSIGNED_FROM_CUSTOMER, savedAsset.getId().toString(), publicCustomer.getId().toString(), publicCustomer.getTitle()); + + foundAsset = doGet("/api/asset/" + savedAsset.getId().getId().toString(), Asset.class); + Assert.assertEquals(ModelConstants.NULL_UUID, foundAsset.getCustomerId().getId()); + } + @Test public void testAssignAssetToNonExistentCustomer() throws Exception { Asset asset = new Asset(); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java index c6bf570284..e486fb1f42 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java @@ -30,6 +30,7 @@ import org.springframework.test.context.ContextConfiguration; 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.ShortCustomerInfo; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; @@ -216,6 +217,49 @@ public abstract class BaseDashboardControllerTest extends AbstractControllerTest Assert.assertTrue(foundDashboard.getAssignedCustomers() == null || foundDashboard.getAssignedCustomers().isEmpty()); } + @Test + public void testAssignUnassignDashboardToPublicCustomer() throws Exception { + Dashboard dashboard = new Dashboard(); + dashboard.setTitle("My dashboard"); + Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class); + + Mockito.reset(tbClusterService, auditLogService); + + Dashboard assignedDashboard = doPost("/api/customer/public/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); + + CustomerId publicCustomerId = null; + for (ShortCustomerInfo assignedCustomer : assignedDashboard.getAssignedCustomers()) { + if (assignedCustomer.isPublic()) { + publicCustomerId = assignedCustomer.getCustomerId(); + } + } + Assert.assertNotNull(publicCustomerId); + Customer publicCustomer = doGet("/api/customer/" + publicCustomerId, Customer.class); + Assert.assertTrue(publicCustomer.isPublic()); + + testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(assignedDashboard, assignedDashboard.getId(), assignedDashboard.getId(), + savedTenant.getId(), publicCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ASSIGNED_TO_CUSTOMER, + assignedDashboard .getId().getId().toString(), publicCustomer.getId().getId().toString(), publicCustomer.getTitle()); + + Dashboard foundDashboard = doGet("/api/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); + Assert.assertTrue(foundDashboard.getAssignedCustomers().contains(publicCustomer.toShortCustomerInfo())); + + Mockito.reset(tbClusterService, auditLogService); + + Dashboard unassignedDashboard = + doDelete("/api/customer/public/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); + + testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(assignedDashboard, assignedDashboard.getId(), assignedDashboard.getId(), + savedTenant.getId(), publicCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.UNASSIGNED_FROM_CUSTOMER, + unassignedDashboard.getId().getId().toString(), publicCustomer.getId().getId().toString(), publicCustomer.getTitle()); + + Assert.assertTrue(unassignedDashboard.getAssignedCustomers() == null || unassignedDashboard.getAssignedCustomers().isEmpty()); + + foundDashboard = doGet("/api/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); + + Assert.assertTrue(foundDashboard.getAssignedCustomers() == null || foundDashboard.getAssignedCustomers().isEmpty()); + } + @Test public void testAssignDashboardToNonExistentCustomer() throws Exception { Dashboard dashboard = new Dashboard(); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java index 0e9d840711..334cdb4bd8 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java @@ -521,6 +521,45 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { Assert.assertEquals(ModelConstants.NULL_UUID, foundDevice.getCustomerId().getId()); } + @Test + public void testAssignUnassignDeviceToPublicCustomer() throws Exception { + Device device = new Device(); + device.setName("My device"); + device.setType("default"); + Device savedDevice = doPost("/api/device", device, Device.class); + + Mockito.reset(tbClusterService, auditLogService, gatewayNotificationsService); + + Device assignedDevice = doPost("/api/customer/public/device/" + savedDevice.getId().getId(), Device.class); + + Customer publicCustomer = doGet("/api/customer/" + assignedDevice.getCustomerId(), Customer.class); + Assert.assertTrue(publicCustomer.isPublic()); + + testNotifyEntityAllOneTime(assignedDevice, assignedDevice.getId(), assignedDevice.getId(), savedTenant.getId(), + publicCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ASSIGNED_TO_CUSTOMER, + assignedDevice.getId().getId().toString(), publicCustomer.getId().getId().toString(), + publicCustomer.getTitle()); + testNotificationUpdateGatewayNever(); + + Device foundDevice = doGet("/api/device/" + savedDevice.getId().getId(), Device.class); + Assert.assertEquals(publicCustomer.getId(), foundDevice.getCustomerId()); + + Mockito.reset(tbClusterService, auditLogService, gatewayNotificationsService); + + Device unassignedDevice = + doDelete("/api/customer/device/" + savedDevice.getId().getId(), Device.class); + Assert.assertEquals(ModelConstants.NULL_UUID, unassignedDevice.getCustomerId().getId()); + + testNotifyEntityAllOneTime(unassignedDevice, unassignedDevice.getId(), unassignedDevice.getId(), savedTenant.getId(), + publicCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.UNASSIGNED_FROM_CUSTOMER, + unassignedDevice.getId().getId().toString(), publicCustomer.getId().getId().toString(), + publicCustomer.getTitle()); + testNotificationDeleteGatewayNever(); + + foundDevice = doGet("/api/device/" + savedDevice.getId().getId(), Device.class); + Assert.assertEquals(ModelConstants.NULL_UUID, foundDevice.getCustomerId().getId()); + } + @Test public void testAssignDeviceToNonExistentCustomer() throws Exception { Device device = new Device(); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java index 9106b5d824..3471c89c5d 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java @@ -50,6 +50,7 @@ import org.thingsboard.server.edge.imitator.EdgeImitator; import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; +import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; @@ -57,6 +58,10 @@ import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; +import java.io.File; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -92,7 +97,7 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { } } - @Before + @Before public void beforeTest() throws Exception { loginSysAdmin(); @@ -327,7 +332,7 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { String customerIdStr = customerId.getId().toString(); String msgError = msgErrorNoFound("Customer", customerIdStr); - doPost("/api/customer/" + customerIdStr+ "/edge/" + savedEdge.getId().getId().toString()) + doPost("/api/customer/" + customerIdStr + "/edge/" + savedEdge.getId().getId().toString()) .andExpect(status().isNotFound()) .andExpect(statusReason(containsString(msgError))); @@ -820,7 +825,7 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { EdgeImitator edgeImitator = new EdgeImitator(EDGE_HOST, EDGE_PORT, edge.getRoutingKey(), edge.getSecret()); edgeImitator.ignoreType(UserCredentialsUpdateMsg.class); - edgeImitator.expectMessageAmount(19); + edgeImitator.expectMessageAmount(20); edgeImitator.connect(); assertThat(edgeImitator.waitForMessages()).as("await for messages on first connect").isTrue(); @@ -833,9 +838,10 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { assertThat(edgeImitator.findAllMessagesByType(AssetUpdateMsg.class)).as("two msgs - one during sync process, and one more once asset assigned to edge").hasSize(2); assertThat(edgeImitator.findAllMessagesByType(UserUpdateMsg.class)).as("one msg during sync process for tenant admin user").hasSize(1); assertThat(edgeImitator.findAllMessagesByType(AdminSettingsUpdateMsg.class)).as("admin setting update").hasSize(4); + assertThat(edgeImitator.findAllMessagesByType(CustomerUpdateMsg.class)).as("one msg during sync process for 'Public' customer").hasSize(1); verifyRuleChainMsgsAreRoot(ruleChainUpdateMsgs); - edgeImitator.expectMessageAmount(14); + edgeImitator.expectMessageAmount(15); doPost("/api/edge/sync/" + edge.getId()); assertThat(edgeImitator.waitForMessages()).as("await for messages after edge sync rest api call").isTrue(); @@ -848,6 +854,7 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { assertThat(edgeImitator.findAllMessagesByType(UserUpdateMsg.class)).as("user update msg").hasSize(1); assertThat(edgeImitator.findAllMessagesByType(AdminSettingsUpdateMsg.class)).as("admin setting update msg").hasSize(4); assertThat(edgeImitator.findAllMessagesByType(DeviceUpdateMsg.class)).as("asset update msg").hasSize(1); + assertThat(edgeImitator.findAllMessagesByType(CustomerUpdateMsg.class)).as("one msg during sync process for 'Public' customer").hasSize(1); verifyRuleChainMsgsAreRoot(ruleChainUpdateMsgs); edgeImitator.allowIgnoredTypes(); @@ -886,4 +893,13 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { Edge edge = constructEdge(name, "default"); return doPost("/api/edge", edge, Edge.class); } + + @Test + public void testGetEdgeInstallInstructions() throws Exception { + Edge edge = constructEdge(tenantId, "Edge for Test Docker Install Instructions", "default", "7390c3a6-69b0-9910-d155-b90aca4b772e", "l7q4zsjplzwhk16geqxy"); + Edge savedEdge = doPost("/api/edge", edge, Edge.class); + String installInstructions = doGet("/api/edge/instructions/" + savedEdge.getId().getId().toString(), String.class); + Assert.assertTrue(installInstructions.contains("l7q4zsjplzwhk16geqxy")); + Assert.assertTrue(installInstructions.contains("7390c3a6-69b0-9910-d155-b90aca4b772e")); + } } 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 fe3fefc4e7..2255d53cbb 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java @@ -310,7 +310,40 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes testNotifyEntityAllOneTime(unAssignedView, savedView.getId(), savedView.getId(), tenantId, savedView.getCustomerId(), tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.UNASSIGNED_FROM_CUSTOMER, - savedView.getCustomerId().getId().toString(), savedCustomer.getTitle()); + assignedView.getId().getId().toString(), savedView.getCustomerId().getId().toString(), savedCustomer.getTitle()); + } + + @Test + public void testAssignAndUnAssignedEntityViewToPublicCustomer() throws Exception { + EntityView savedView = getNewSavedEntityView("Test entity view"); + Mockito.reset(tbClusterService, auditLogService); + + EntityView assignedView = doPost( + "/api/customer/public/entityView/" + savedView.getId().getId().toString(), + EntityView.class); + Customer publicCustomer = doGet("/api/customer/" + assignedView.getCustomerId(), Customer.class); + Assert.assertTrue(publicCustomer.isPublic()); + + testBroadcastEntityStateChangeEventNever(assignedView.getId()); + testNotifyEntityAllOneTime(assignedView, assignedView.getId(), assignedView.getId(), + tenantId, assignedView.getCustomerId(), tenantAdminUserId, TENANT_ADMIN_EMAIL, + ActionType.ASSIGNED_TO_CUSTOMER, + assignedView.getId().getId().toString(), assignedView.getCustomerId().getId().toString(), publicCustomer.getTitle()); + + EntityView foundView = doGet("/api/entityView/" + savedView.getId().getId().toString(), EntityView.class); + assertEquals(publicCustomer.getId(), foundView.getCustomerId()); + + EntityView unAssignedView = doDelete("/api/customer/entityView/" + savedView.getId().getId().toString(), EntityView.class); + assertEquals(ModelConstants.NULL_UUID, unAssignedView.getCustomerId().getId()); + + foundView = doGet("/api/entityView/" + savedView.getId().getId().toString(), EntityView.class); + assertEquals(ModelConstants.NULL_UUID, foundView.getCustomerId().getId()); + + testBroadcastEntityStateChangeEventNever(foundView.getId()); + testNotifyEntityAllOneTime(unAssignedView, unAssignedView.getId(), unAssignedView.getId(), + tenantId, publicCustomer.getId(), tenantAdminUserId, TENANT_ADMIN_EMAIL, + ActionType.UNASSIGNED_FROM_CUSTOMER, + unAssignedView.getId().getId().toString(), publicCustomer.getId().getId().toString(), publicCustomer.getTitle()); } @Test @@ -428,7 +461,7 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes testBroadcastEntityStateChangeEventNever(loadedNamesOfView1.get(0).getId()); testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAnyAdditionalInfoAny(new EntityView(), new EntityView(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, - ActionType.UNASSIGNED_FROM_CUSTOMER, ActionType.UNASSIGNED_FROM_CUSTOMER, cntEntity, cntEntity, 2); + ActionType.UNASSIGNED_FROM_CUSTOMER, ActionType.UNASSIGNED_FROM_CUSTOMER, cntEntity, cntEntity, 3); PageData pageData = doGetTypedWithPageLink(urlTemplate, PAGE_DATA_ENTITY_VIEW_TYPE_REF, new PageLink(4, 0, name1)); diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/AlarmCommentControllerSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/AlarmCommentControllerSqlTest.java new file mode 100644 index 0000000000..ecf2791c1c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/sql/AlarmCommentControllerSqlTest.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.BaseAlarmCommentControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class AlarmCommentControllerSqlTest extends BaseAlarmCommentControllerTest { +} diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java index 5314d4872f..24a8548bba 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java @@ -27,6 +27,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.OtaPackageInfo; @@ -51,11 +53,13 @@ import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceProfileId; 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.ota.ChecksumAlgorithm; +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.common.data.query.EntityKeyValueType; @@ -63,6 +67,7 @@ import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.NumericFilterPredicate; 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.security.Authority; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.edge.EdgeEventService; @@ -70,6 +75,7 @@ import org.thingsboard.server.edge.imitator.EdgeImitator; import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; +import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.EdgeConfiguration; @@ -142,7 +148,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { installation(); edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret()); - edgeImitator.expectMessageAmount(21); + edgeImitator.expectMessageAmount(22); edgeImitator.connect(); requestEdgeRuleChainMetadata(); @@ -269,6 +275,9 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { // 1 message from user fetcher validateUsers(); + + // 1 message from public customer fetcher + validatePublicCustomer(); } private void validateEdgeConfiguration() throws Exception { @@ -442,6 +451,17 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { testAutoGeneratedCodeByProtobuf(userUpdateMsg); } + private void validatePublicCustomer() throws Exception { + Optional customerUpdateMsgOpt = edgeImitator.findMessageByType(CustomerUpdateMsg.class); + Assert.assertTrue(customerUpdateMsgOpt.isPresent()); + CustomerUpdateMsg customerUpdateMsg = customerUpdateMsgOpt.get(); + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, customerUpdateMsg.getMsgType()); + UUID customerUUID = new UUID(customerUpdateMsg.getIdMSB(), customerUpdateMsg.getIdLSB()); + Customer customer = doGet("/api/customer/" + customerUUID, Customer.class); + Assert.assertNotNull(customer); + Assert.assertTrue(customer.isPublic()); + } + protected Device saveDeviceOnCloudAndVerifyDeliveryToEdge() throws Exception { // create device and assign to edge Device savedDevice = saveDevice(StringUtils.randomAlphanumeric(15), thermostatDeviceProfile.getName()); @@ -500,13 +520,13 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { return doPost("/api/asset", asset, Asset.class); } - protected OtaPackageInfo saveOtaPackageInfo(DeviceProfileId deviceProfileId) { + protected OtaPackageInfo saveOtaPackageInfo(DeviceProfileId deviceProfileId, OtaPackageType type) { SaveOtaPackageInfoRequest firmwareInfo = new SaveOtaPackageInfoRequest(); firmwareInfo.setDeviceProfileId(deviceProfileId); - firmwareInfo.setType(FIRMWARE); - firmwareInfo.setTitle("Firmware Edge " + StringUtils.randomAlphanumeric(3)); + firmwareInfo.setType(type); + firmwareInfo.setTitle(type.name() + " Edge " + StringUtils.randomAlphanumeric(3)); firmwareInfo.setVersion("v1.0"); - firmwareInfo.setTag("My firmware #1 v1.0"); + firmwareInfo.setTag("My " + type.name() + " #1 v1.0"); firmwareInfo.setUsesUrl(true); firmwareInfo.setUrl("http://localhost:8080/v1/package"); firmwareInfo.setAdditionalInfo(JacksonUtil.newObjectNode()); @@ -541,6 +561,49 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { Assert.assertEquals(source.hashCode(), target.hashCode()); } + protected RuleChainId createEdgeRuleChainAndAssignToEdge(String ruleChainName) throws Exception { + edgeImitator.expectMessageAmount(1); + RuleChain ruleChain = new RuleChain(); + ruleChain.setName(ruleChainName); + ruleChain.setType(RuleChainType.EDGE); + RuleChain savedRuleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class); + doPost("/api/edge/" + edge.getUuidId() + + "/ruleChain/" + savedRuleChain.getUuidId(), RuleChain.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + return savedRuleChain.getId(); + } + + protected void unAssignFromEdgeAndDeleteRuleChain(RuleChainId ruleChainId) throws Exception { + edgeImitator.expectMessageAmount(1); + doDelete("/api/edge/" + edge.getUuidId() + + "/ruleChain/" + ruleChainId.getId(), RuleChain.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + // delete rule chain + doDelete("/api/ruleChain/" + ruleChainId.getId()) + .andExpect(status().isOk()); + } + + protected DashboardId createDashboardAndAssignToEdge(String dashboardName) throws Exception { + edgeImitator.expectMessageAmount(1); + Dashboard dashboard = new Dashboard(); + dashboard.setTitle(dashboardName); + Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class); + doPost("/api/edge/" + edge.getUuidId() + + "/dashboard/" + savedDashboard.getUuidId(), Dashboard.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + return savedDashboard.getId(); + } + + protected void unAssignFromEdgeAndDeleteDashboard(DashboardId dashboardId) throws Exception { + edgeImitator.expectMessageAmount(1); + doDelete("/api/edge/" + edge.getUuidId() + + "/dashboard/" + dashboardId.getId(), RuleChain.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + // delete dashboard + doDelete("/api/dashboard/" + dashboardId.getId()) + .andExpect(status().isOk()); + } } diff --git a/application/src/test/java/org/thingsboard/server/edge/BaseAssetProfileEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/BaseAssetProfileEdgeTest.java index 567a196cec..2e2f7a6e9f 100644 --- a/application/src/test/java/org/thingsboard/server/edge/BaseAssetProfileEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/BaseAssetProfileEdgeTest.java @@ -20,6 +20,7 @@ import com.google.protobuf.ByteString; import org.junit.Assert; import org.junit.Test; import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; @@ -31,8 +32,11 @@ abstract public class BaseAssetProfileEdgeTest extends AbstractEdgeTest { @Test public void testAssetProfiles() throws Exception { + RuleChainId buildingsRuleChainId = createEdgeRuleChainAndAssignToEdge("Buildings Rule Chain"); + // create asset profile AssetProfile assetProfile = this.createAssetProfile("Building"); + assetProfile.setDefaultEdgeRuleChainId(buildingsRuleChainId); edgeImitator.expectMessageAmount(1); assetProfile = doPost("/api/assetProfile", assetProfile, AssetProfile.class); Assert.assertTrue(edgeImitator.waitForMessages()); @@ -43,6 +47,8 @@ abstract public class BaseAssetProfileEdgeTest extends AbstractEdgeTest { Assert.assertEquals(assetProfile.getUuidId().getMostSignificantBits(), assetProfileUpdateMsg.getIdMSB()); Assert.assertEquals(assetProfile.getUuidId().getLeastSignificantBits(), assetProfileUpdateMsg.getIdLSB()); Assert.assertEquals("Building", assetProfileUpdateMsg.getName()); + Assert.assertEquals(buildingsRuleChainId.getId().getMostSignificantBits(), assetProfileUpdateMsg.getDefaultRuleChainIdMSB()); + Assert.assertEquals(buildingsRuleChainId.getId().getLeastSignificantBits(), assetProfileUpdateMsg.getDefaultRuleChainIdLSB()); // update asset profile assetProfile.setImage("IMAGE"); @@ -66,5 +72,7 @@ abstract public class BaseAssetProfileEdgeTest extends AbstractEdgeTest { Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, assetProfileUpdateMsg.getMsgType()); Assert.assertEquals(assetProfile.getUuidId().getMostSignificantBits(), assetProfileUpdateMsg.getIdMSB()); Assert.assertEquals(assetProfile.getUuidId().getLeastSignificantBits(), assetProfileUpdateMsg.getIdLSB()); + + unAssignFromEdgeAndDeleteRuleChain(buildingsRuleChainId); } } diff --git a/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java index 4af8fede14..804f014bdc 100644 --- a/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java @@ -43,6 +43,7 @@ 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.EntityId; +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.common.data.security.DeviceCredentials; @@ -174,7 +175,7 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest { // create device and assign to edge; update device Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge(); - verifyUpdateFirmwareIdAndDeviceData(savedDevice); + verifyUpdateFirmwareIdSoftwareIdAndDeviceData(savedDevice); // update device credentials - ACCESS_TOKEN edgeImitator.expectMessageAmount(1); @@ -210,15 +211,20 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest { Assert.assertEquals(deviceCredentials.getCredentialsValue(), deviceCredentialsUpdateMsg.getCredentialsValue()); } - private void verifyUpdateFirmwareIdAndDeviceData(Device savedDevice) throws InterruptedException { - // create ota package + private void verifyUpdateFirmwareIdSoftwareIdAndDeviceData(Device savedDevice) throws InterruptedException { + // create ota packages edgeImitator.expectMessageAmount(1); - OtaPackageInfo firmwareOtaPackageInfo = saveOtaPackageInfo(thermostatDeviceProfile.getId()); + OtaPackageInfo firmwareOtaPackageInfo = saveOtaPackageInfo(thermostatDeviceProfile.getId(), OtaPackageType.FIRMWARE); + Assert.assertTrue(edgeImitator.waitForMessages()); + + edgeImitator.expectMessageAmount(1); + OtaPackageInfo softwareOtaPackageInfo = saveOtaPackageInfo(thermostatDeviceProfile.getId(), OtaPackageType.SOFTWARE); Assert.assertTrue(edgeImitator.waitForMessages()); // update device edgeImitator.expectMessageAmount(1); savedDevice.setFirmwareId(firmwareOtaPackageInfo.getId()); + savedDevice.setSoftwareId(softwareOtaPackageInfo.getId()); DeviceData deviceData = new DeviceData(); deviceData.setConfiguration(new DefaultDeviceConfiguration()); @@ -239,6 +245,8 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest { Assert.assertEquals(savedDevice.getType(), deviceUpdateMsg.getType()); Assert.assertEquals(firmwareOtaPackageInfo.getUuidId().getMostSignificantBits(), deviceUpdateMsg.getFirmwareIdMSB()); Assert.assertEquals(firmwareOtaPackageInfo.getUuidId().getLeastSignificantBits(), deviceUpdateMsg.getFirmwareIdLSB()); + Assert.assertEquals(softwareOtaPackageInfo.getUuidId().getMostSignificantBits(), deviceUpdateMsg.getSoftwareIdMSB()); + Assert.assertEquals(softwareOtaPackageInfo.getUuidId().getLeastSignificantBits(), deviceUpdateMsg.getSoftwareIdLSB()); Optional deviceDataOpt = dataDecodingEncodingService.decode(deviceUpdateMsg.getDeviceDataBytes().toByteArray()); Assert.assertTrue(deviceDataOpt.isPresent()); @@ -693,7 +701,7 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest { public void testVerifyDeliveryOfLatestTimeseriesOnAttributesRequest() throws Exception { Device device = findDeviceByName("Edge Device 1"); - JsonNode timeseriesData = mapper.readTree("{\"temperature\":25}"); + JsonNode timeseriesData = mapper.readTree("{\"temperature\":25, \"isEnabled\": true}"); doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, timeseriesData); @@ -732,9 +740,17 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest { TransportProtos.PostTelemetryMsg timeseriesUpdatedMsg = latestEntityDataMsg.getPostTelemetryMsg(); Assert.assertEquals(1, timeseriesUpdatedMsg.getTsKvListList().size()); TransportProtos.TsKvListProto tsKvListProto = timeseriesUpdatedMsg.getTsKvListList().get(0); - Assert.assertEquals(1, tsKvListProto.getKvList().size()); - TransportProtos.KeyValueProto keyValueProto = tsKvListProto.getKvList().get(0); - Assert.assertEquals(25, keyValueProto.getLongV()); - Assert.assertEquals("temperature", keyValueProto.getKey()); + Assert.assertEquals(2, tsKvListProto.getKvList().size()); + for (TransportProtos.KeyValueProto keyValueProto : tsKvListProto.getKvList()) { + if ("temperature".equals(keyValueProto.getKey())) { + Assert.assertEquals(TransportProtos.KeyValueType.LONG_V, keyValueProto.getType()); + Assert.assertEquals(25, keyValueProto.getLongV()); + } else if ("isEnabled".equals(keyValueProto.getKey())) { + Assert.assertEquals(TransportProtos.KeyValueType.BOOLEAN_V, keyValueProto.getType()); + Assert.assertTrue(keyValueProto.getBoolV()); + } else { + Assert.fail("Unexpected key: " + keyValueProto.getKey()); + } + } } } diff --git a/application/src/test/java/org/thingsboard/server/edge/BaseDeviceProfileEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/BaseDeviceProfileEdgeTest.java index d0f0f48477..8e83f570bc 100644 --- a/application/src/test/java/org/thingsboard/server/edge/BaseDeviceProfileEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/BaseDeviceProfileEdgeTest.java @@ -36,7 +36,10 @@ import org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryMappingC import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.AbstractLwM2MBootstrapServerCredential; import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MBootstrapServerCredential; import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.NoSecLwM2MBootstrapServerCredential; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.kv.DataType; +import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.transport.snmp.SnmpMapping; import org.thingsboard.server.common.data.transport.snmp.config.SnmpCommunicationConfig; import org.thingsboard.server.common.data.transport.snmp.config.impl.TelemetryQueryingSnmpCommunicationConfig; @@ -55,8 +58,11 @@ abstract public class BaseDeviceProfileEdgeTest extends AbstractEdgeTest { @Test public void testDeviceProfiles() throws Exception { + RuleChainId thermostatsRuleChainId = createEdgeRuleChainAndAssignToEdge("Thermostats Rule Chain"); + // create device profile DeviceProfile deviceProfile = this.createDeviceProfile("ONE_MORE_DEVICE_PROFILE", null); + deviceProfile.setDefaultEdgeRuleChainId(thermostatsRuleChainId); extendDeviceProfileData(deviceProfile); edgeImitator.expectMessageAmount(1); deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); @@ -67,13 +73,23 @@ abstract public class BaseDeviceProfileEdgeTest extends AbstractEdgeTest { Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); Assert.assertEquals(deviceProfile.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getIdMSB()); Assert.assertEquals(deviceProfile.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getIdLSB()); + Assert.assertEquals(thermostatsRuleChainId.getId().getMostSignificantBits(), deviceProfileUpdateMsg.getDefaultRuleChainIdMSB()); + Assert.assertEquals(thermostatsRuleChainId.getId().getLeastSignificantBits(), deviceProfileUpdateMsg.getDefaultRuleChainIdLSB()); // update device profile - OtaPackageInfo firmwareOtaPackageInfo = saveOtaPackageInfo(deviceProfile.getId()); edgeImitator.expectMessageAmount(1); + OtaPackageInfo firmwareOtaPackageInfo = saveOtaPackageInfo(deviceProfile.getId(), OtaPackageType.FIRMWARE); + Assert.assertTrue(edgeImitator.waitForMessages()); + + edgeImitator.expectMessageAmount(1); + OtaPackageInfo softwareOtaPackageInfo = saveOtaPackageInfo(deviceProfile.getId(), OtaPackageType.SOFTWARE); Assert.assertTrue(edgeImitator.waitForMessages()); + DashboardId thermostatsDashboardId = createDashboardAndAssignToEdge("Thermostats Dashboard"); + deviceProfile.setFirmwareId(firmwareOtaPackageInfo.getId()); + deviceProfile.setSoftwareId(softwareOtaPackageInfo.getId()); + deviceProfile.setDefaultDashboardId(thermostatsDashboardId); edgeImitator.expectMessageAmount(1); deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); Assert.assertTrue(edgeImitator.waitForMessages()); @@ -82,6 +98,10 @@ abstract public class BaseDeviceProfileEdgeTest extends AbstractEdgeTest { deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage; Assert.assertEquals(firmwareOtaPackageInfo.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getFirmwareIdMSB()); Assert.assertEquals(firmwareOtaPackageInfo.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getFirmwareIdLSB()); + Assert.assertEquals(softwareOtaPackageInfo.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getSoftwareIdMSB()); + Assert.assertEquals(softwareOtaPackageInfo.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getSoftwareIdLSB()); + Assert.assertEquals(thermostatsDashboardId.getId().getMostSignificantBits(), deviceProfileUpdateMsg.getDefaultDashboardIdMSB()); + Assert.assertEquals(thermostatsDashboardId.getId().getLeastSignificantBits(), deviceProfileUpdateMsg.getDefaultDashboardIdLSB()); // delete profile edgeImitator.expectMessageAmount(1); @@ -94,6 +114,9 @@ abstract public class BaseDeviceProfileEdgeTest extends AbstractEdgeTest { Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); Assert.assertEquals(deviceProfile.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getIdMSB()); Assert.assertEquals(deviceProfile.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getIdLSB()); + + unAssignFromEdgeAndDeleteRuleChain(thermostatsRuleChainId); + unAssignFromEdgeAndDeleteDashboard(thermostatsDashboardId); } @Test diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java index dba6546fc0..bea64f12af 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java @@ -29,6 +29,8 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.edge.EdgeService; @@ -37,6 +39,8 @@ import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService; import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; +import java.util.UUID; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; @@ -62,6 +66,8 @@ public class DefaultTbAlarmServiceTest { @MockBean protected AlarmService alarmService; @MockBean + protected AlarmCommentService alarmCommentService; + @MockBean protected AlarmSubscriptionService alarmSubscriptionService; @MockBean protected CustomerService customerService; @@ -88,8 +94,9 @@ public class DefaultTbAlarmServiceTest { var alarm = new Alarm(); alarm.setStatus(AlarmStatus.ACTIVE_UNACK); when(alarmSubscriptionService.ackAlarm(any(), any(), anyLong())).thenReturn(Futures.immediateFuture(true)); - service.ack(alarm, new User()); + service.ack(alarm, new User(new UserId(UUID.randomUUID()))); + verify(alarmCommentService, times(1)).createOrUpdateAlarmComment(any(), any()); verify(notificationEntityService, times(1)).notifyCreateOrUpdateAlarm(any(), any(), any()); verify(alarmSubscriptionService, times(1)).ackAlarm(any(), any(), anyLong()); } @@ -99,8 +106,9 @@ public class DefaultTbAlarmServiceTest { var alarm = new Alarm(); alarm.setStatus(AlarmStatus.ACTIVE_ACK); when(alarmSubscriptionService.clearAlarm(any(), any(), any(), anyLong())).thenReturn(Futures.immediateFuture(true)); - service.clear(alarm, new User()); + service.clear(alarm, new User(new UserId(UUID.randomUUID()))); + verify(alarmCommentService, times(1)).createOrUpdateAlarmComment(any(), any()); verify(notificationEntityService, times(1)).notifyCreateOrUpdateAlarm(any(), any(), any()); verify(alarmSubscriptionService, times(1)).clearAlarm(any(), any(), any(), anyLong()); } diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/alarmComment/DefaultTbAlarmCommentServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/alarmComment/DefaultTbAlarmCommentServiceTest.java new file mode 100644 index 0000000000..174d78c7ef --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/alarmComment/DefaultTbAlarmCommentServiceTest.java @@ -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. + */ +package org.thingsboard.server.service.entitiy.alarmComment; + +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AlarmCommentId; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.dao.alarm.AlarmCommentService; +import org.thingsboard.server.dao.alarm.AlarmService; +import org.thingsboard.server.dao.customer.CustomerService; +import org.thingsboard.server.service.entitiy.TbNotificationEntityService; +import org.thingsboard.server.service.entitiy.alarm.DefaultTbAlarmCommentService; +import org.thingsboard.server.service.executors.DbCallbackExecutorService; +import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; + +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@Slf4j +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = DefaultTbAlarmCommentService.class) +@TestPropertySource(properties = { + "server.log_controller_error_stack_trace=false" +}) +public class DefaultTbAlarmCommentServiceTest { + + @MockBean + protected DbCallbackExecutorService dbExecutor; + @MockBean + protected TbNotificationEntityService notificationEntityService; + @MockBean + protected AlarmService alarmService; + @MockBean + protected AlarmCommentService alarmCommentService; + @MockBean + protected AlarmSubscriptionService alarmSubscriptionService; + @MockBean + protected CustomerService customerService; + @MockBean + protected TbClusterService tbClusterService; + @SpyBean + DefaultTbAlarmCommentService service; + + @Test + public void testSave() throws ThingsboardException { + var alarm = new Alarm(); + var alarmComment = new AlarmComment(); + when(alarmCommentService.createOrUpdateAlarmComment(Mockito.any(), eq(alarmComment))).thenReturn(alarmComment); + service.saveAlarmComment(alarm, alarmComment, new User()); + + verify(notificationEntityService, times(1)).notifyAlarmComment(any(), any(), any(), any()); + } + + @Test + public void testDelete() { + var alarmId = new AlarmId(UUID.randomUUID()); + var alarmCommentId = new AlarmCommentId(UUID.randomUUID()); + + doNothing().when(alarmCommentService).deleteAlarmComment(Mockito.any(), eq(alarmCommentId)); + service.deleteAlarmComment(new Alarm(alarmId), new AlarmComment(alarmCommentId), new User()); + + verify(notificationEntityService, times(1)).notifyAlarmComment(any(), any(), any(), any()); + } +} \ No newline at end of file diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmCommentService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmCommentService.java new file mode 100644 index 0000000000..d4ffee4169 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmCommentService.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.alarm; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; +import org.thingsboard.server.common.data.id.AlarmCommentId; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; + +public interface AlarmCommentService { + AlarmComment createOrUpdateAlarmComment(TenantId tenantId, AlarmComment alarmComment); + + void deleteAlarmComment(TenantId tenantId, AlarmCommentId alarmCommentId); + + PageData findAlarmComments(TenantId tenantId, AlarmId alarmId, PageLink pageLink); + + ListenableFuture findAlarmCommentByIdAsync(TenantId tenantId, AlarmCommentId alarmCommentId); + + AlarmComment findAlarmCommentById(TenantId tenantId, AlarmCommentId alarmCommentId); + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java index 4940c74191..c91fb9d789 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java @@ -15,18 +15,22 @@ */ package org.thingsboard.server.dao.alarm; +import lombok.AllArgsConstructor; import lombok.Data; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.id.EntityId; import java.util.Collections; import java.util.List; @Data +@AllArgsConstructor public class AlarmOperationResult { private final Alarm alarm; private final boolean successful; private final boolean created; + private final AlarmSeverity oldSeverity; private final List propagatedEntitiesList; public AlarmOperationResult(Alarm alarm, boolean successful) { @@ -34,13 +38,6 @@ public class AlarmOperationResult { } public AlarmOperationResult(Alarm alarm, boolean successful, List propagatedEntitiesList) { - this(alarm, successful, false, propagatedEntitiesList); - } - - public AlarmOperationResult(Alarm alarm, boolean successful, boolean created, List propagatedEntitiesList) { - this.alarm = alarm; - this.successful = successful; - this.created = created; - this.propagatedEntitiesList = propagatedEntitiesList; + this(alarm, successful, false, null, propagatedEntitiesList); } } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java index 70f8f00ebb..793f020045 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java @@ -18,12 +18,12 @@ package org.thingsboard.server.dao.alarm; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.id.AlarmId; 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.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; 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 2a5b11f1ff..d0efc8cb5b 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 @@ -98,6 +98,11 @@ public class DeviceProfile extends SearchTextBased implements H @ApiModelProperty(position = 10, value = "Reference to the software OTA package. If present, the specified package will be used as default device software. ") private OtaPackageId softwareId; + @ApiModelProperty(position = 17, value = "Reference to the edge rule chain. " + + "If present, the specified edge rule chain will be used on the edge to process all messages related to device, including telemetry, attribute updates, etc. " + + "Otherwise, the edge root rule chain will be used to process those messages.") + private RuleChainId defaultEdgeRuleChainId; + private DeviceProfileId externalId; public DeviceProfile() { @@ -122,6 +127,7 @@ public class DeviceProfile extends SearchTextBased implements H this.provisionDeviceKey = deviceProfile.getProvisionDeviceKey(); this.firmwareId = deviceProfile.getFirmwareId(); this.softwareId = deviceProfile.getSoftwareId(); + this.defaultEdgeRuleChainId = deviceProfile.getDefaultEdgeRuleChainId(); this.externalId = deviceProfile.getExternalId(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.java new file mode 100644 index 0000000000..0393c630a9 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.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.data.alarm; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.id.AlarmCommentId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; + +@ApiModel +@Data +@Builder +@AllArgsConstructor +public class AlarmComment extends BaseData implements HasName { + @ApiModelProperty(position = 3, value = "JSON object with Alarm id.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + private EntityId alarmId; + @ApiModelProperty(position = 4, value = "JSON object with User id.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + private UserId userId; + @ApiModelProperty(position = 5, value = "Defines origination of comment. System type means comment was created by TB. OTHER type means comment was created by user.", example = "SYSTEM/OTHER", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + private AlarmCommentType type; + @ApiModelProperty(position = 6, value = "JSON object with text of comment.", dataType = "com.fasterxml.jackson.databind.JsonNode") + @NoXss + @Length(fieldName = "comment", max = 10000) + private transient JsonNode comment; + + @ApiModelProperty(position = 1, value = "JSON object with the alarm comment Id. " + + "Specify this field to update the alarm comment. " + + "Referencing non-existing alarm Id will cause error. " + + "Omit this field to create new alarm." ) + @Override + public AlarmCommentId getId() { + return super.getId(); + } + + @ApiModelProperty(position = 2, value = "Timestamp of the alarm comment creation, in milliseconds", example = "1634058704567", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + @Override + public long getCreatedTime() { + return super.getCreatedTime(); + } + + public AlarmComment() { + super(); + } + + public AlarmComment(AlarmCommentId id) { + super(id); + } + + @Override + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + @ApiModelProperty(position = 5, required = true, value = "representing comment text", example = "Please take a look") + public String getName() { + return comment.toString(); + } + + public AlarmComment(AlarmComment alarmComment) { + super(alarmComment.getId()); + this.createdTime = alarmComment.getCreatedTime(); + this.alarmId = alarmComment.getAlarmId(); + this.type = alarmComment.getType(); + this.comment = alarmComment.getComment(); + this.userId = alarmComment.getUserId(); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCommentInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCommentInfo.java new file mode 100644 index 0000000000..66300e4cdb --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCommentInfo.java @@ -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. + */ +package org.thingsboard.server.common.data.alarm; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@ApiModel +@Data +@EqualsAndHashCode(callSuper = true) +public class AlarmCommentInfo extends AlarmComment { + private static final long serialVersionUID = 2807343093519543377L; + + @ApiModelProperty(position = 19, value = "User first name", example = "John") + private String firstName; + + @ApiModelProperty(position = 19, value = "User last name", example = "Brown") + private String lastName; + + @ApiModelProperty(position = 19, value = "User email address", example = "johnBrown@gmail.com") + private String email; + + public AlarmCommentInfo() { + super(); + } + + public AlarmCommentInfo(AlarmComment alarmComment) { + super(alarmComment); + } + + public AlarmCommentInfo(AlarmComment alarmComment, String firstName, String lastName, String email) { + super(alarmComment); + this.firstName = firstName; + this.lastName = lastName; + this.email = email; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCommentType.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCommentType.java new file mode 100644 index 0000000000..e9d4c34105 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCommentType.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.common.data.alarm; + +public enum AlarmCommentType { + + SYSTEM, OTHER; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfile.java index 01657feed6..d882095c44 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfile.java @@ -68,6 +68,11 @@ public class AssetProfile extends SearchTextBased implements Has "Otherwise, the 'Main' queue will be used to store those messages.") private String defaultQueueName; + @ApiModelProperty(position = 13, value = "Reference to the edge rule chain. " + + "If present, the specified edge rule chain will be used on the edge to process all messages related to asset, including asset updates, telemetry, attribute updates, etc. " + + "Otherwise, the edge root rule chain will be used to process those messages.") + private RuleChainId defaultEdgeRuleChainId; + private AssetProfileId externalId; public AssetProfile() { @@ -88,6 +93,7 @@ public class AssetProfile extends SearchTextBased implements Has this.defaultRuleChainId = assetProfile.getDefaultRuleChainId(); this.defaultDashboardId = assetProfile.getDefaultDashboardId(); this.defaultQueueName = assetProfile.getDefaultQueueName(); + this.defaultEdgeRuleChainId = assetProfile.getDefaultEdgeRuleChainId(); this.externalId = assetProfile.getExternalId(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java index 41f6f30dbe..507903a5fd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java @@ -48,7 +48,10 @@ public enum ActionType { PROVISION_SUCCESS(false), PROVISION_FAILURE(false), ASSIGNED_TO_EDGE(false), // log edge name - UNASSIGNED_FROM_EDGE(false); + UNASSIGNED_FROM_EDGE(false), + ADDED_COMMENT(false), + UPDATED_COMMENT(false), + DELETED_COMMENT(false); private final boolean isRead; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeInstallInstructions.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeInstallInstructions.java new file mode 100644 index 0000000000..49d2b6e60b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeInstallInstructions.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.edge; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@ApiModel +@Data +@AllArgsConstructor +@NoArgsConstructor +public class EdgeInstallInstructions { + + @ApiModelProperty(position = 1, value = "Markdown with docker install instructions") + private String dockerInstallInstructions; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmCommentId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmCommentId.java new file mode 100644 index 0000000000..1ceeab2f4d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmCommentId.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.common.data.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.thingsboard.server.common.data.EntityType; + +import java.util.UUID; + +@ApiModel +public class AlarmCommentId extends UUIDBased { + + private static final long serialVersionUID = 1L; + + @JsonCreator + public AlarmCommentId(@JsonProperty("id") UUID id) { + super(id); + } + + public static AlarmCommentId fromString(String commentId) { + return new AlarmCommentId(UUID.fromString(commentId)); + } +} diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 249f251238..b962badbcd 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -203,6 +203,8 @@ message DeviceUpdateMsg { optional int64 firmwareIdMSB = 13; optional int64 firmwareIdLSB = 14; optional bytes deviceDataBytes = 15; + optional int64 softwareIdMSB = 16; + optional int64 softwareIdLSB = 17; } message DeviceProfileUpdateMsg { @@ -223,6 +225,10 @@ message DeviceProfileUpdateMsg { optional bytes image = 15; optional int64 firmwareIdMSB = 16; optional int64 firmwareIdLSB = 17; + optional int64 softwareIdMSB = 18; + optional int64 softwareIdLSB = 19; + optional int64 defaultDashboardIdMSB = 20; + optional int64 defaultDashboardIdLSB = 21; } message AssetProfileUpdateMsg { diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmCommentDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmCommentDao.java new file mode 100644 index 0000000000..8db3fbf71f --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmCommentDao.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.alarm; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; +import org.thingsboard.server.common.data.id.AlarmCommentId; +import org.thingsboard.server.common.data.id.AlarmId; +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.Dao; + +import java.util.UUID; + +public interface AlarmCommentDao extends Dao { + + AlarmComment createAlarmComment(TenantId tenantId, AlarmComment alarmComment); + + void deleteAlarmComment(TenantId tenantId, AlarmCommentId alarmCommentId); + + AlarmComment findAlarmCommentById(TenantId tenantId, UUID key); + + PageData findAlarmComments(TenantId tenantId, AlarmId id, PageLink pageLink); + + ListenableFuture findAlarmCommentByIdAsync(TenantId tenantId, UUID key); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmCommentService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmCommentService.java new file mode 100644 index 0000000000..7cc547536a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmCommentService.java @@ -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. + */ +package org.thingsboard.server.dao.alarm; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; +import org.thingsboard.server.common.data.alarm.AlarmCommentType; +import org.thingsboard.server.common.data.id.AlarmCommentId; +import org.thingsboard.server.common.data.id.AlarmId; +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.entity.AbstractEntityService; +import org.thingsboard.server.dao.service.DataValidator; + +import java.util.UUID; + +import static org.thingsboard.server.dao.service.Validator.validateId; + +@Service +@Slf4j +public class BaseAlarmCommentService extends AbstractEntityService implements AlarmCommentService{ + + @Autowired + private AlarmCommentDao alarmCommentDao; + + @Autowired + private DataValidator alarmCommentDataValidator; + + @Override + public AlarmComment createOrUpdateAlarmComment(TenantId tenantId, AlarmComment alarmComment) { + alarmCommentDataValidator.validate(alarmComment, c -> tenantId); + if (alarmComment.getId() == null) { + return createAlarmComment(tenantId, alarmComment); + } else { + return updateAlarmComment(tenantId, alarmComment); + } + } + + @Override + public void deleteAlarmComment(TenantId tenantId, AlarmCommentId alarmCommentId) { + log.debug("Deleting Alarm Comment with id: {}", alarmCommentId); + alarmCommentDao.deleteAlarmComment(tenantId, alarmCommentId); + } + + @Override + public PageData findAlarmComments(TenantId tenantId, AlarmId alarmId, PageLink pageLink) { + log.trace("Executing findAlarmComments by alarmId [{}]", alarmId); + return alarmCommentDao.findAlarmComments(tenantId, alarmId, pageLink); + } + + @Override + public ListenableFuture findAlarmCommentByIdAsync(TenantId tenantId, AlarmCommentId alarmCommentId) { + log.trace("Executing findAlarmCommentByIdAsync by alarmCommentId [{}]", alarmCommentId); + validateId(alarmCommentId, "Incorrect alarmCommentId " + alarmCommentId); + return alarmCommentDao.findAlarmCommentByIdAsync(tenantId, alarmCommentId.getId()); + } + + @Override + public AlarmComment findAlarmCommentById(TenantId tenantId, AlarmCommentId alarmCommentId) { + log.trace("Executing findAlarmCommentByIdAsync by alarmCommentId [{}]", alarmCommentId); + validateId(alarmCommentId, "Incorrect alarmCommentId " + alarmCommentId); + return alarmCommentDao.findById(tenantId, alarmCommentId.getId()); + } + + private AlarmComment createAlarmComment(TenantId tenantId, AlarmComment alarmComment) { + log.debug("New Alarm comment : {}", alarmComment); + if (alarmComment.getType() == null) { + alarmComment.setType(AlarmCommentType.OTHER); + } + if (alarmComment.getId() == null) { + UUID uuid = Uuids.timeBased(); + alarmComment.setId(new AlarmCommentId(uuid)); + alarmComment.setCreatedTime(Uuids.unixTimestamp(uuid)); + } + return alarmCommentDao.createAlarmComment(tenantId, alarmComment); + } + + private AlarmComment updateAlarmComment(TenantId tenantId, AlarmComment newAlarmComment) { + log.debug("Update Alarm comment : {}", newAlarmComment); + + AlarmComment existing = alarmCommentDao.findAlarmCommentById(tenantId, newAlarmComment.getId().getId()); + if (existing != null) { + if (newAlarmComment.getComment() != null) { + JsonNode comment = newAlarmComment.getComment(); + ((ObjectNode) comment).put("edited", "true"); + ((ObjectNode) comment).put("editedOn", System.currentTimeMillis()); + existing.setComment(comment); + } + return alarmCommentDao.save(tenantId, existing); + } + return null; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java index c76be82956..798a4c08dc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java @@ -165,7 +165,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ log.debug("New Alarm : {}", alarm); Alarm saved = alarmDao.save(alarm.getTenantId(), alarm); List propagatedEntitiesList = createEntityAlarmRecords(saved); - return new AlarmOperationResult(saved, true, true, propagatedEntitiesList); + return new AlarmOperationResult(saved, true, true, null, propagatedEntitiesList); } private List createEntityAlarmRecords(Alarm alarm) throws InterruptedException, ExecutionException { @@ -208,6 +208,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ boolean propagationEnabled = !oldAlarm.isPropagate() && newAlarm.isPropagate(); boolean propagationToOwnerEnabled = !oldAlarm.isPropagateToOwner() && newAlarm.isPropagateToOwner(); boolean propagationToTenantEnabled = !oldAlarm.isPropagateToTenant() && newAlarm.isPropagateToTenant(); + AlarmSeverity oldAlarmSeverity = oldAlarm.getSeverity(); Alarm result = alarmDao.save(newAlarm.getTenantId(), merge(oldAlarm, newAlarm)); List propagatedEntitiesList; if (propagationEnabled || propagationToOwnerEnabled || propagationToTenantEnabled) { @@ -220,7 +221,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ } else { propagatedEntitiesList = new ArrayList<>(getPropagationEntityIds(result)); } - return new AlarmOperationResult(result, true, propagatedEntitiesList); + return new AlarmOperationResult(result, true, false, oldAlarmSeverity, propagatedEntitiesList); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java index 3df41be1e3..e7f57b0eb0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java @@ -30,6 +30,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.audit.ActionStatus; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; @@ -182,6 +183,12 @@ public class AuditLogServiceImpl implements AuditLogService { } } break; + case ADDED_COMMENT: + case UPDATED_COMMENT: + case DELETED_COMMENT: + AlarmComment comment = extractParameter(AlarmComment.class, additionalInfo); + actionData.set("comment", comment.getComment()); + break; case DELETED: case ACTIVATED: case SUSPENDED: 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 3736a236e6..e0fdfe5dd4 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 @@ -188,6 +188,7 @@ public class ModelConstants { public static final String DEVICE_PROFILE_CERTIFICATE_HASH_PROPERTY = "certificate_hash"; public static final String DEVICE_PROFILE_CERTIFICATE_VALUE_PROPERTY = "certificate_value"; public static final String DEVICE_PROFILE_CERTIFICATE_REGEX_PATTERN_PROPERTY = "certificate_regex_pattern"; + public static final String DEVICE_PROFILE_DEFAULT_EDGE_RULE_CHAIN_ID_PROPERTY = "default_edge_rule_chain_id"; /** * Asset profile constants. @@ -201,6 +202,7 @@ public class ModelConstants { public static final String ASSET_PROFILE_DEFAULT_RULE_CHAIN_ID_PROPERTY = "default_rule_chain_id"; public static final String ASSET_PROFILE_DEFAULT_DASHBOARD_ID_PROPERTY = "default_dashboard_id"; public static final String ASSET_PROFILE_DEFAULT_QUEUE_NAME_PROPERTY = "default_queue_name"; + public static final String ASSET_PROFILE_DEFAULT_EDGE_RULE_CHAIN_ID_PROPERTY = "default_edge_rule_chain_id"; /** * Cassandra entityView constants. @@ -300,6 +302,14 @@ public class ModelConstants { public static final String ALARM_BY_ID_VIEW_NAME = "alarm_by_id"; + public static final String ALARM_COMMENT_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; + public static final String ALARM_COMMENT_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY; + public static final String ALARM_COMMENT_COLUMN_FAMILY_NAME = "alarm_comment"; + public static final String ALARM_COMMENT_ALARM_ID = "alarm_id"; + public static final String ALARM_COMMENT_USER_ID = USER_ID_PROPERTY; + public static final String ALARM_COMMENT_TYPE = "type"; + public static final String ALARM_COMMENT_COMMENT = "comment"; + /** * Cassandra entity relation constants. */ diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmCommentEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmCommentEntity.java new file mode 100644 index 0000000000..e81558cac5 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmCommentEntity.java @@ -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. + */ +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.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentType; +import org.thingsboard.server.common.data.id.AlarmCommentId; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.UserId; +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.MappedSuperclass; +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_COMMENT_ALARM_ID; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_COMMENT_COMMENT; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_COMMENT_TYPE; + +@Data +@EqualsAndHashCode(callSuper = true) +@TypeDef(name = "json", typeClass = JsonStringType.class) +@MappedSuperclass +public abstract class AbstractAlarmCommentEntity extends BaseSqlEntity implements BaseEntity { + + @Column(name = ALARM_COMMENT_ALARM_ID, columnDefinition = "uuid") + private UUID alarmId; + + @Column(name = ModelConstants.ALARM_COMMENT_USER_ID) + private UUID userId; + + @Column(name = ALARM_COMMENT_TYPE) + private AlarmCommentType type; + + @Type(type = "json") + @Column(name = ALARM_COMMENT_COMMENT) + private JsonNode comment; + + public AbstractAlarmCommentEntity() { + super(); + } + + public AbstractAlarmCommentEntity(AlarmComment alarmComment) { + if (alarmComment.getId() != null) { + this.setUuid(alarmComment.getUuidId()); + } + this.setCreatedTime(alarmComment.getCreatedTime()); + this.alarmId = alarmComment.getAlarmId().getId(); + if (alarmComment.getUserId() != null) { + this.userId = alarmComment.getUserId().getId(); + } + if (alarmComment.getType() != null) { + this.type = alarmComment.getType(); + } + this.setComment(alarmComment.getComment()); + } + + public AbstractAlarmCommentEntity(AlarmCommentEntity alarmCommentEntity) { + this.setId(alarmCommentEntity.getId()); + this.setCreatedTime(alarmCommentEntity.getCreatedTime()); + this.userId = alarmCommentEntity.getUserId(); + this.alarmId = alarmCommentEntity.getAlarmId(); + this.type = alarmCommentEntity.getType(); + this.comment = alarmCommentEntity.getComment(); + } + protected AlarmComment toAlarmComment() { + AlarmComment alarmComment = new AlarmComment(new AlarmCommentId(id)); + alarmComment.setCreatedTime(createdTime); + alarmComment.setAlarmId(new AlarmId(alarmId)); + if (userId != null) { + alarmComment.setUserId(new UserId(userId)); + } + alarmComment.setType(type); + alarmComment.setComment(comment); + return alarmComment; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmCommentEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmCommentEntity.java new file mode 100644 index 0000000000..6f9a507cbd --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmCommentEntity.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.dao.model.sql; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Entity; +import javax.persistence.Table; + +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_COMMENT_COLUMN_FAMILY_NAME; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@TypeDef(name = "json", typeClass = JsonStringType.class) +@Table(name = ALARM_COMMENT_COLUMN_FAMILY_NAME) + +public class AlarmCommentEntity extends AbstractAlarmCommentEntity { + + public AlarmCommentEntity() { + super(); + } + + public AlarmCommentEntity(AlarmCommentInfo alarmCommentInfo) { + super(alarmCommentInfo); + } + + public AlarmCommentEntity(AlarmComment alarmComment) { + super(alarmComment); + } + + @Override + public AlarmComment toData() { + return super.toAlarmComment(); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmCommentInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmCommentInfoEntity.java new file mode 100644 index 0000000000..1d19ec9388 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmCommentInfoEntity.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.model.sql; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; + +@Data +@EqualsAndHashCode(callSuper = true) +public class AlarmCommentInfoEntity extends AbstractAlarmCommentEntity { + + private String firstName; + private String lastName; + + private String email; + + public AlarmCommentInfoEntity() { + super(); + } + + public AlarmCommentInfoEntity(AlarmCommentEntity alarmCommentEntity) { + super(alarmCommentEntity); + } + + public AlarmCommentInfoEntity(AlarmCommentEntity alarmCommentEntity, String firstName, String lastName, String email) { + super(alarmCommentEntity); + this.firstName = firstName; + this.lastName = lastName; + this.email = email; + } + + @Override + public AlarmCommentInfo toData() { + return new AlarmCommentInfo(super.toAlarmComment(), this.firstName, this.lastName, this.email); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetProfileEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetProfileEntity.java index 36f7b40487..01c8d106ad 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetProfileEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetProfileEntity.java @@ -64,6 +64,9 @@ public final class AssetProfileEntity extends BaseSqlEntity implem @Column(name = ModelConstants.ASSET_PROFILE_DEFAULT_QUEUE_NAME_PROPERTY) private String defaultQueueName; + @Column(name = ModelConstants.ASSET_PROFILE_DEFAULT_EDGE_RULE_CHAIN_ID_PROPERTY, columnDefinition = "uuid") + private UUID defaultEdgeRuleChainId; + @Column(name = ModelConstants.EXTERNAL_ID_PROPERTY) private UUID externalId; @@ -90,6 +93,9 @@ public final class AssetProfileEntity extends BaseSqlEntity implem this.defaultDashboardId = assetProfile.getDefaultDashboardId().getId(); } this.defaultQueueName = assetProfile.getDefaultQueueName(); + if (assetProfile.getDefaultEdgeRuleChainId() != null) { + this.defaultEdgeRuleChainId = assetProfile.getDefaultEdgeRuleChainId().getId(); + } if (assetProfile.getExternalId() != null) { this.externalId = assetProfile.getExternalId().getId(); } @@ -127,6 +133,9 @@ public final class AssetProfileEntity extends BaseSqlEntity implem if (defaultDashboardId != null) { assetProfile.setDefaultDashboardId(new DashboardId(defaultDashboardId)); } + if (defaultEdgeRuleChainId != null) { + assetProfile.setDefaultEdgeRuleChainId(new RuleChainId(defaultEdgeRuleChainId)); + } if (externalId != null) { assetProfile.setExternalId(new AssetProfileId(externalId)); } 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 2aaaf61197..19bb4ccd91 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 @@ -103,6 +103,9 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl @Column(name = ModelConstants.DEVICE_PROFILE_SOFTWARE_ID_PROPERTY) private UUID softwareId; + @Column(name = ModelConstants.DEVICE_PROFILE_DEFAULT_EDGE_RULE_CHAIN_ID_PROPERTY, columnDefinition = "uuid") + private UUID defaultEdgeRuleChainId; + @Column(name = ModelConstants.EXTERNAL_ID_PROPERTY) private UUID externalId; @@ -152,6 +155,9 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl if (deviceProfile.getSoftwareId() != null) { this.softwareId = deviceProfile.getSoftwareId().getId(); } + if (deviceProfile.getDefaultEdgeRuleChainId() != null) { + this.defaultEdgeRuleChainId = deviceProfile.getDefaultEdgeRuleChainId().getId(); + } if (deviceProfile.getExternalId() != null) { this.externalId = deviceProfile.getExternalId().getId(); } @@ -204,6 +210,9 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl if (softwareId != null) { deviceProfile.setSoftwareId(new OtaPackageId(softwareId)); } + if (defaultEdgeRuleChainId != null) { + deviceProfile.setDefaultEdgeRuleChainId(new RuleChainId(defaultEdgeRuleChainId)); + } if (externalId != null) { deviceProfile.setExternalId(new DeviceProfileId(externalId)); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/StringLengthValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/StringLengthValidator.java index 6c12c0cebd..a90fba0ead 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/StringLengthValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/StringLengthValidator.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.service; +import com.fasterxml.jackson.databind.JsonNode; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.validation.Length; @@ -23,15 +24,21 @@ import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; @Slf4j -public class StringLengthValidator implements ConstraintValidator { +public class StringLengthValidator implements ConstraintValidator { private int max; @Override - public boolean isValid(String value, ConstraintValidatorContext context) { - if (StringUtils.isEmpty(value)) { + public boolean isValid(Object value, ConstraintValidatorContext context) { + String stringValue; + if (value instanceof CharSequence || value instanceof JsonNode) { + stringValue = value.toString(); + } else { return true; } - return value.length() <= max; + if (StringUtils.isEmpty(stringValue)) { + return true; + } + return stringValue.length() <= max; } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmCommentDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmCommentDataValidator.java new file mode 100644 index 0000000000..d85a58bc6d --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmCommentDataValidator.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.service.validator; + +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; + +@Component +@AllArgsConstructor +public class AlarmCommentDataValidator extends DataValidator { + + @Override + protected void validateDataImpl(TenantId tenantId, AlarmComment alarmComment) { + if (alarmComment.getComment() == null) { + throw new DataValidationException("Alarm comment should be specified!"); + } + if (alarmComment.getAlarmId() == null) { + throw new DataValidationException("Alarm id should be specified!"); + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmCommentRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmCommentRepository.java new file mode 100644 index 0000000000..19dfd6ec2e --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmCommentRepository.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.sql.alarm; + +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.query.Param; +import org.thingsboard.server.dao.model.sql.AlarmCommentEntity; +import org.thingsboard.server.dao.model.sql.AlarmCommentInfoEntity; + +import java.util.UUID; + +public interface AlarmCommentRepository extends JpaRepository { + + @Query(value = "SELECT new org.thingsboard.server.dao.model.sql.AlarmCommentInfoEntity(a, u.firstName, u.lastName, u.email) FROM AlarmCommentEntity a " + + "LEFT JOIN UserEntity u on u.id = a.userId " + + "WHERE a.alarmId = :alarmId ", + countQuery = "" + + "SELECT count(a) " + + "FROM AlarmCommentEntity a " + + "WHERE a.alarmId = :alarmId ") + Page findAllByAlarmId(@Param("alarmId") UUID alarmId, + Pageable pageable); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDao.java new file mode 100644 index 0000000000..ef43fd4df5 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDao.java @@ -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. + */ +package org.thingsboard.server.dao.sql.alarm; + +import com.google.common.util.concurrent.ListenableFuture; +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.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; +import org.thingsboard.server.common.data.id.AlarmCommentId; +import org.thingsboard.server.common.data.id.AlarmId; +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.DaoUtil; +import org.thingsboard.server.dao.alarm.AlarmCommentDao; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.model.sql.AlarmCommentEntity; +import org.thingsboard.server.dao.sql.JpaAbstractDao; +import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository; +import org.thingsboard.server.dao.util.SqlDao; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_COMMENT_COLUMN_FAMILY_NAME; + +@Slf4j +@Component +@SqlDao +@RequiredArgsConstructor +public class JpaAlarmCommentDao extends JpaAbstractDao implements AlarmCommentDao { + private final SqlPartitioningRepository partitioningRepository; + @Value("${sql.alarm_comments.partition_size:168}") + private int partitionSizeInHours; + + @Autowired + private AlarmCommentRepository alarmCommentRepository; + + @Override + public AlarmComment createAlarmComment(TenantId tenantId, AlarmComment alarmComment){ + log.trace("Saving entity {}", alarmComment); + partitioningRepository.createPartitionIfNotExists(ALARM_COMMENT_COLUMN_FAMILY_NAME, alarmComment.getCreatedTime(), TimeUnit.HOURS.toMillis(partitionSizeInHours)); + AlarmCommentEntity saved = alarmCommentRepository.save(new AlarmCommentEntity(alarmComment)); + return DaoUtil.getData(saved); + } + + @Override + public void deleteAlarmComment(TenantId tenantId, AlarmCommentId alarmCommentId){ + log.trace("Try to delete entity alarm comment by id using [{}]", alarmCommentId); + alarmCommentRepository.deleteById(alarmCommentId.getId()); + } + + @Override + public PageData findAlarmComments(TenantId tenantId, AlarmId id, PageLink pageLink){ + log.trace("Try to find alarm comments by alarm id using [{}]", id); + return DaoUtil.toPageData( + alarmCommentRepository.findAllByAlarmId(id.getId(), DaoUtil.toPageable(pageLink))); + } + + @Override + public AlarmComment findAlarmCommentById(TenantId tenantId, UUID key) { + log.trace("Try to find alarm comment by id using [{}]", key); + return DaoUtil.getData(alarmCommentRepository.findById(key)); + } + + @Override + public ListenableFuture findAlarmCommentByIdAsync(TenantId tenantId, UUID key) { + log.trace("Try to find alarm comment by id using [{}]", key); + return findByIdAsync(tenantId, key); + } + + @Override + protected Class getEntityClass() { + return AlarmCommentEntity.class; + } + + @Override + protected JpaRepository getRepository() { + return alarmCommentRepository; + } +} diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index e4c766e81d..dc72e4cdc3 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -79,3 +79,5 @@ CREATE INDEX IF NOT EXISTS idx_rule_node_external_id ON rule_node(rule_chain_id, CREATE INDEX IF NOT EXISTS idx_rule_node_type ON rule_node(type); CREATE INDEX IF NOT EXISTS idx_api_usage_state_entity_id ON api_usage_state(entity_id); + +CREATE INDEX IF NOT EXISTS idx_alarm_comment_alarm_id ON alarm_comment(alarm_id); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 9db17f87ee..3a33a05060 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -61,6 +61,16 @@ CREATE TABLE IF NOT EXISTS alarm ( propagate_to_tenant boolean ); +CREATE TABLE IF NOT EXISTS alarm_comment ( + id uuid NOT NULL, + created_time bigint NOT NULL, + alarm_id uuid NOT NULL, + user_id uuid, + type varchar(255) NOT NULL, + comment varchar(10000), + CONSTRAINT fk_alarm_comment_alarm_id FOREIGN KEY (alarm_id) REFERENCES alarm(id) ON DELETE CASCADE + ) PARTITION BY RANGE (created_time); + CREATE TABLE IF NOT EXISTS entity_alarm ( tenant_id uuid NOT NULL, entity_type varchar(32), @@ -237,11 +247,13 @@ CREATE TABLE IF NOT EXISTS asset_profile ( default_rule_chain_id uuid, default_dashboard_id uuid, default_queue_name varchar(255), + default_edge_rule_chain_id uuid, external_id uuid, CONSTRAINT asset_profile_name_unq_key UNIQUE (tenant_id, name), CONSTRAINT asset_profile_external_id_unq_key UNIQUE (tenant_id, external_id), CONSTRAINT fk_default_rule_chain_asset_profile FOREIGN KEY (default_rule_chain_id) REFERENCES rule_chain(id), - CONSTRAINT fk_default_dashboard_asset_profile FOREIGN KEY (default_dashboard_id) REFERENCES dashboard(id) + CONSTRAINT fk_default_dashboard_asset_profile FOREIGN KEY (default_dashboard_id) REFERENCES dashboard(id), + CONSTRAINT fk_default_edge_rule_chain_asset_profile FOREIGN KEY (default_edge_rule_chain_id) REFERENCES rule_chain(id) ); CREATE TABLE IF NOT EXISTS asset ( @@ -283,6 +295,7 @@ CREATE TABLE IF NOT EXISTS device_profile ( certificate_value varchar; certificate_hash varchar, certificate_regex_pattern varchar(255), + default_edge_rule_chain_id uuid, external_id uuid, CONSTRAINT device_profile_credentials_hash_unq_key UNIQUE (certificate_hash), CONSTRAINT device_profile_name_unq_key UNIQUE (tenant_id, name), @@ -291,7 +304,8 @@ CREATE TABLE IF NOT EXISTS device_profile ( 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_firmware_device_profile FOREIGN KEY (firmware_id) REFERENCES ota_package(id), - CONSTRAINT fk_software_device_profile FOREIGN KEY (software_id) REFERENCES ota_package(id) + CONSTRAINT fk_software_device_profile FOREIGN KEY (software_id) REFERENCES ota_package(id), + CONSTRAINT fk_default_edge_rule_chain_device_profile FOREIGN KEY (default_edge_rule_chain_id) REFERENCES rule_chain(id) ); DO @@ -778,10 +792,5 @@ 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), - two_fa_settings varchar -); -CREATE TABLE IF NOT EXISTS well_known_root_ca ( - certificate_hash varchar NOT NULL CONSTRAINT cert_pkey PRIMARY KEY, - certificate_value varchar ); 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 5ea3874d19..61147120bc 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 @@ -47,6 +47,7 @@ import org.thingsboard.server.common.data.id.HasId; 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.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -149,6 +150,8 @@ public abstract class AbstractServiceTest { @Autowired protected AlarmService alarmService; + @Autowired + protected AlarmCommentService alarmCommentService; @Autowired protected RuleChainService ruleChainService; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmCommentServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmCommentServiceTest.java new file mode 100644 index 0000000000..914c360d86 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmCommentServiceTest.java @@ -0,0 +1,164 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 com.datastax.oss.driver.api.core.uuid.Uuids; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.id.AssetId; +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.security.Authority; + +import java.util.UUID; +import java.util.concurrent.ExecutionException; + +import static org.thingsboard.server.common.data.alarm.AlarmCommentType.OTHER; + +public abstract class BaseAlarmCommentServiceTest extends AbstractServiceTest { + + public static final String TEST_ALARM = "TEST_ALARM"; + private TenantId tenantId; + private Alarm alarm; + private User user; + + @Before + public void before() { + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + Tenant savedTenant = tenantService.saveTenant(tenant); + Assert.assertNotNull(savedTenant); + tenantId = savedTenant.getId(); + + alarm = Alarm.builder().tenantId(tenantId).originator(new AssetId(Uuids.timeBased())) + .type(TEST_ALARM) + .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) + .startTs(System.currentTimeMillis()).build(); + alarm = alarmService.createOrUpdateAlarm(alarm).getAlarm(); + + user = new User(); + user.setAuthority(Authority.TENANT_ADMIN); + user.setTenantId(tenantId); + user.setEmail("tenant@thingsboard.org"); + user.setFirstName("John"); + user.setLastName("Brown"); + user = userService.saveUser(user); + } + + @After + public void after() { + alarmService.deleteAlarm(tenantId, alarm.getId()); + tenantService.deleteTenant(tenantId); + } + + + @Test + public void testSaveAndFetchAlarmComment() throws ExecutionException, InterruptedException { + AlarmComment alarmComment = AlarmComment.builder().alarmId(alarm.getId()) + .userId(user.getId()) + .type(OTHER) + .comment(JacksonUtil.newObjectNode().put("text", RandomStringUtils.randomAlphanumeric(10))) + .build(); + + AlarmComment createdComment = alarmCommentService.createOrUpdateAlarmComment(tenantId, alarmComment); + + Assert.assertNotNull(createdComment); + Assert.assertNotNull(createdComment.getId()); + + Assert.assertEquals(alarm.getId(), createdComment.getAlarmId()); + Assert.assertEquals(user.getId(), createdComment.getUserId()); + Assert.assertEquals(OTHER, createdComment.getType()); + Assert.assertTrue(createdComment.getCreatedTime() > 0); + + AlarmComment fetched = alarmCommentService.findAlarmCommentByIdAsync(tenantId, createdComment.getId()).get(); + Assert.assertEquals(createdComment, fetched); + + PageData alarmComments = alarmCommentService.findAlarmComments(tenantId, alarm.getId(), new PageLink(10, 0)); + Assert.assertNotNull(alarmComments.getData()); + Assert.assertEquals(1, alarmComments.getData().size()); + Assert.assertEquals(createdComment, new AlarmComment(alarmComments.getData().get(0))); + } + + @Test + public void testUpdateAlarmComment() throws ExecutionException, InterruptedException { + UserId userId = new UserId(UUID.randomUUID()); + AlarmComment alarmComment = AlarmComment.builder().alarmId(alarm.getId()) + .userId(userId) + .type(OTHER) + .comment(JacksonUtil.newObjectNode().put("text", RandomStringUtils.randomAlphanumeric(10))) + .build(); + + AlarmComment createdComment = alarmCommentService.createOrUpdateAlarmComment(tenantId, alarmComment); + + Assert.assertNotNull(createdComment); + Assert.assertNotNull(createdComment.getId()); + + //update comment + String newComment = "new comment"; + createdComment.setComment(JacksonUtil.newObjectNode().put("text", newComment)); + AlarmComment updatedComment = alarmCommentService.createOrUpdateAlarmComment(tenantId, createdComment); + + Assert.assertEquals(alarm.getId(), updatedComment.getAlarmId()); + Assert.assertEquals(userId, updatedComment.getUserId()); + Assert.assertEquals(OTHER, updatedComment.getType()); + Assert.assertTrue(updatedComment.getCreatedTime() > 0); + Assert.assertEquals(newComment, updatedComment.getComment().get("text").asText()); + Assert.assertEquals("true", updatedComment.getComment().get("edited").asText()); + Assert.assertNotNull(updatedComment.getComment().get("editedOn").asText()); + + AlarmComment fetched = alarmCommentService.findAlarmCommentByIdAsync(tenantId, createdComment.getId()).get(); + Assert.assertEquals(updatedComment, fetched); + + PageData alarmComments = alarmCommentService.findAlarmComments(tenantId, alarm.getId(), new PageLink(10, 0)); + Assert.assertNotNull(alarmComments.getData()); + Assert.assertEquals(1, alarmComments.getData().size()); + Assert.assertEquals(updatedComment, new AlarmComment(alarmComments.getData().get(0))); + } + + @Test + public void testDeleteAlarmComment() throws ExecutionException, InterruptedException { + UserId userId = new UserId(UUID.randomUUID()); + AlarmComment alarmComment = AlarmComment.builder().alarmId(alarm.getId()) + .userId(userId) + .type(OTHER) + .comment(JacksonUtil.newObjectNode().put("text", RandomStringUtils.randomAlphanumeric(10))) + .build(); + + AlarmComment createdComment = alarmCommentService.createOrUpdateAlarmComment(tenantId, alarmComment); + + Assert.assertNotNull(createdComment); + Assert.assertNotNull(createdComment.getId()); + + alarmCommentService.deleteAlarmComment(tenantId, createdComment.getId()); + + AlarmComment fetched = alarmCommentService.findAlarmCommentByIdAsync(tenantId, createdComment.getId()).get(); + + Assert.assertNull("Alarm comment was returned when it was expected to be null", fetched); + } +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/AlarmCommentServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/AlarmCommentServiceSqlTest.java new file mode 100644 index 0000000000..510b8659e9 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/AlarmCommentServiceSqlTest.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.BaseAlarmCommentServiceTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class AlarmCommentServiceSqlTest extends BaseAlarmCommentServiceTest { +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDaoTest.java new file mode 100644 index 0000000000..1a37e3af4b --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDaoTest.java @@ -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. + */ +package org.thingsboard.server.dao.sql.alarm; + +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentType; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.id.AlarmCommentId; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.AbstractJpaDaoTest; +import org.thingsboard.server.dao.alarm.AlarmCommentDao; +import org.thingsboard.server.dao.alarm.AlarmDao; + +import java.util.UUID; +import static org.junit.Assert.assertEquals; + +@Slf4j +public class JpaAlarmCommentDaoTest extends AbstractJpaDaoTest { + + @Autowired + private AlarmCommentDao alarmCommentDao; + @Autowired + private AlarmDao alarmDao; + + + @Test + public void testFindAlarmCommentsByAlarmId() { + log.info("Current system time in millis = {}", System.currentTimeMillis()); + UUID tenantId = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + UUID alarmId1 = UUID.randomUUID(); + UUID alarmId2 = UUID.randomUUID(); + UUID commentId1 = UUID.randomUUID(); + UUID commentId2 = UUID.randomUUID(); + UUID commentId3 = UUID.randomUUID(); + saveAlarm(alarmId1, UUID.randomUUID(), UUID.randomUUID(), "TEST_ALARM"); + saveAlarm(alarmId2, UUID.randomUUID(), UUID.randomUUID(), "TEST_ALARM"); + + saveAlarmComment(commentId1, alarmId1, userId, AlarmCommentType.OTHER); + saveAlarmComment(commentId2, alarmId1, userId, AlarmCommentType.OTHER); + saveAlarmComment(commentId3, alarmId2, userId, AlarmCommentType.OTHER); + + int count = alarmCommentDao.findAlarmComments(TenantId.fromUUID(tenantId), new AlarmId(alarmId1), new PageLink(10, 0)).getData().size(); + assertEquals(2, count); + } + + private void saveAlarm(UUID id, UUID tenantId, UUID deviceId, String type) { + Alarm alarm = new Alarm(); + alarm.setId(new AlarmId(id)); + alarm.setTenantId(TenantId.fromUUID(tenantId)); + alarm.setOriginator(new DeviceId(deviceId)); + alarm.setType(type); + alarm.setPropagate(true); + alarm.setStartTs(System.currentTimeMillis()); + alarm.setEndTs(System.currentTimeMillis()); + alarm.setStatus(AlarmStatus.ACTIVE_UNACK); + alarmDao.save(TenantId.fromUUID(tenantId), alarm); + } + private void saveAlarmComment(UUID id, UUID alarmId, UUID userId, AlarmCommentType type) { + AlarmComment alarmComment = new AlarmComment(); + alarmComment.setId(new AlarmCommentId(id)); + alarmComment.setAlarmId(TenantId.fromUUID(alarmId)); + alarmComment.setUserId(new UserId(userId)); + alarmComment.setType(type); + alarmComment.setComment(JacksonUtil.newObjectNode().put("text", RandomStringUtils.randomAlphanumeric(10))); + alarmCommentDao.createAlarmComment(TenantId.fromUUID(UUID.randomUUID()), alarmComment); + } +} diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index ec1ead3e6b..862314110c 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -81,6 +81,7 @@ import org.thingsboard.server.common.data.device.DeviceSearchQuery; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeInfo; +import org.thingsboard.server.common.data.edge.EdgeInstallInstructions; import org.thingsboard.server.common.data.edge.EdgeSearchQuery; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.id.AlarmId; @@ -3130,6 +3131,12 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { }).getBody(); } + public Optional getEdgeDockerInstallInstructions(EdgeId edgeId) { + ResponseEntity edgeInstallInstructionsResult = + restTemplate.getForEntity(baseURL + "/api/edge/instructions/{edgeId}", EdgeInstallInstructions.class, edgeId.getId()); + return Optional.ofNullable(edgeInstallInstructionsResult.getBody()); + } + public UUID saveEntitiesVersion(VersionCreateRequest request) { return restTemplate.postForEntity(baseURL + "/api/entities/vc/version", request, UUID.class).getBody(); } @@ -3146,6 +3153,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } } } + public PageData listEntityVersions(EntityId externalEntityId, String branch, PageLink pageLink) { Map params = new HashMap<>(); params.put("entityType", externalEntityId.getEntityType().name()); 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 1786fd08be..7133f91329 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 @@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; @@ -69,6 +70,7 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService; import java.util.List; import java.util.Set; +import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -201,6 +203,8 @@ public interface TbContext { * */ + void schedule(Runnable runnable, long delay, TimeUnit timeUnit); + void checkTenantEntity(EntityId entityId); boolean isLocalEntity(EntityId entityId); @@ -237,6 +241,8 @@ public interface TbContext { RuleEngineAlarmService getAlarmService(); + AlarmCommentService getAlarmCommentService(); + RuleChainService getRuleChainService(); RuleEngineRpcService getRpcService(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/DeduplicationId.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/DeduplicationId.java new file mode 100644 index 0000000000..0c3e7cbea2 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/DeduplicationId.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.rule.engine.deduplication; + +public enum DeduplicationId { + + ORIGINATOR, TENANT, CUSTOMER + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/DeduplicationStrategy.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/DeduplicationStrategy.java new file mode 100644 index 0000000000..d761a97b6d --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/DeduplicationStrategy.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.rule.engine.deduplication; + +public enum DeduplicationStrategy { + + FIRST, LAST, ALL + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java new file mode 100644 index 0000000000..de04ce1d3b --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java @@ -0,0 +1,237 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.rule.engine.deduplication; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.util.Pair; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +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.id.EntityId; +import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.util.TbPair; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +@RuleNode( + type = ComponentType.ACTION, + name = "deduplication", + configClazz = TbMsgDeduplicationNodeConfiguration.class, + nodeDescription = "Deduplicate messages for a configurable period based on a specified deduplication strategy.", + nodeDetails = "Rule node allows you to select one of the following strategy to deduplicate messages:

" + + "FIRST - return first message that arrived during deduplication period.

" + + "LAST - return last message that arrived during deduplication period.

" + + "ALL - return all messages as a single JSON array message. Where each element represents object with msg and metadata inner properties.

", + icon = "content_copy", + uiResources = {"static/rulenode/rulenode-core-config.js"}, + configDirective = "tbActionNodeMsgDeduplicationConfig" +) +@Slf4j +public class TbMsgDeduplicationNode implements TbNode { + + private static final String TB_MSG_DEDUPLICATION_TIMEOUT_MSG = "TbMsgDeduplicationNodeMsg"; + private static final int TB_MSG_DEDUPLICATION_TIMEOUT = 5000; + public static final int TB_MSG_DEDUPLICATION_RETRY_DELAY = 10; + + private TbMsgDeduplicationNodeConfiguration config; + + private final Map> deduplicationMap; + private long deduplicationInterval; + private long lastScheduledTs; + private DeduplicationId deduplicationId; + + public TbMsgDeduplicationNode() { + this.deduplicationMap = new HashMap<>(); + } + + @Override + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + this.config = TbNodeUtils.convert(configuration, TbMsgDeduplicationNodeConfiguration.class); + this.deduplicationInterval = TimeUnit.SECONDS.toMillis(config.getInterval()); + this.deduplicationId = config.getId(); + scheduleTickMsg(ctx); + } + + @Override + public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException { + if (TB_MSG_DEDUPLICATION_TIMEOUT_MSG.equals(msg.getType())) { + try { + processDeduplication(ctx); + } finally { + scheduleTickMsg(ctx); + } + } else { + processOnRegularMsg(ctx, msg); + } + } + + @Override + public void destroy() { + deduplicationMap.clear(); + } + + private void processOnRegularMsg(TbContext ctx, TbMsg msg) { + EntityId id = getDeduplicationId(ctx, msg); + List deduplicationMsgs = deduplicationMap.computeIfAbsent(id, k -> new LinkedList<>()); + if (deduplicationMsgs.size() < config.getMaxPendingMsgs()) { + log.trace("[{}][{}] Adding msg: [{}][{}] to the pending msgs map ...", ctx.getSelfId(), id, msg.getId(), msg.getMetaDataTs()); + deduplicationMsgs.add(msg); + ctx.ack(msg); + } else { + log.trace("[{}] Max limit of pending messages reached for deduplication id: [{}]", ctx.getSelfId(), id); + ctx.tellFailure(msg, new RuntimeException("[" + ctx.getSelfId() + "] Max limit of pending messages reached for deduplication id: [" + id + "]")); + } + } + + private EntityId getDeduplicationId(TbContext ctx, TbMsg msg) { + switch (deduplicationId) { + case ORIGINATOR: + return msg.getOriginator(); + case TENANT: + return ctx.getTenantId(); + case CUSTOMER: + return msg.getCustomerId(); + default: + throw new IllegalStateException("Unsupported deduplication id: " + deduplicationId); + } + } + + private void processDeduplication(TbContext ctx) { + if (deduplicationMap.isEmpty()) { + return; + } + List deduplicationResults = new ArrayList<>(); + long deduplicationTimeoutMs = System.currentTimeMillis(); + deduplicationMap.forEach((entityId, tbMsgs) -> { + if (tbMsgs.isEmpty()) { + return; + } + Optional> packBoundsOpt = findValidPack(tbMsgs, deduplicationTimeoutMs); + while (packBoundsOpt.isPresent()) { + TbPair packBounds = packBoundsOpt.get(); + if (DeduplicationStrategy.ALL.equals(config.getStrategy())) { + List pack = new ArrayList<>(); + for (Iterator iterator = tbMsgs.iterator(); iterator.hasNext(); ) { + TbMsg msg = iterator.next(); + long msgTs = msg.getMetaDataTs(); + if (msgTs >= packBounds.getFirst() && msgTs < packBounds.getSecond()) { + pack.add(msg); + iterator.remove(); + } + } + deduplicationResults.add(TbMsg.newMsg( + config.getQueueName(), + config.getOutMsgType(), + entityId, + getMetadata(), + getMergedData(pack))); + } else { + TbMsg resultMsg = null; + boolean searchMin = DeduplicationStrategy.FIRST.equals(config.getStrategy()); + for (Iterator iterator = tbMsgs.iterator(); iterator.hasNext(); ) { + TbMsg msg = iterator.next(); + long msgTs = msg.getMetaDataTs(); + if (msgTs >= packBounds.getFirst() && msgTs < packBounds.getSecond()) { + iterator.remove(); + if (resultMsg == null + || (searchMin && msg.getMetaDataTs() < resultMsg.getMetaDataTs()) + || (!searchMin && msg.getMetaDataTs() > resultMsg.getMetaDataTs())) { + resultMsg = msg; + } + } + } + deduplicationResults.add(resultMsg); + } + packBoundsOpt = findValidPack(tbMsgs, deduplicationTimeoutMs); + } + }); + deduplicationResults.forEach(outMsg -> enqueueForTellNextWithRetry(ctx, outMsg, 0)); + } + + private Optional> findValidPack(List msgs, long deduplicationTimeoutMs) { + Optional min = msgs.stream().min(Comparator.comparing(TbMsg::getMetaDataTs)); + return min.map(minTsMsg -> { + long packStartTs = minTsMsg.getMetaDataTs(); + long packEndTs = packStartTs + deduplicationInterval; + if (packEndTs <= deduplicationTimeoutMs) { + return new TbPair<>(packStartTs, packEndTs); + } + return null; + }); + } + + private void enqueueForTellNextWithRetry(TbContext ctx, TbMsg msg, int retryAttempt) { + if (config.getMaxRetries() > retryAttempt) { + ctx.enqueueForTellNext(msg, TbRelationTypes.SUCCESS, + () -> { + log.trace("[{}][{}][{}] Successfully enqueue deduplication result message!", ctx.getSelfId(), msg.getOriginator(), retryAttempt); + }, + throwable -> { + log.trace("[{}][{}][{}] Failed to enqueue deduplication output message due to: ", ctx.getSelfId(), msg.getOriginator(), retryAttempt, throwable); + ctx.schedule(() -> { + enqueueForTellNextWithRetry(ctx, msg, retryAttempt + 1); + }, TB_MSG_DEDUPLICATION_RETRY_DELAY, TimeUnit.SECONDS); + }); + } + } + + private void scheduleTickMsg(TbContext ctx) { + long curTs = System.currentTimeMillis(); + if (lastScheduledTs == 0L) { + lastScheduledTs = curTs; + } + lastScheduledTs += TB_MSG_DEDUPLICATION_TIMEOUT; + long curDelay = Math.max(0L, (lastScheduledTs - curTs)); + TbMsg tickMsg = ctx.newMsg(null, TB_MSG_DEDUPLICATION_TIMEOUT_MSG, ctx.getSelfId(), new TbMsgMetaData(), ""); + ctx.tellSelf(tickMsg, curDelay); + } + + private String getMergedData(List msgs) { + ArrayNode mergedData = JacksonUtil.OBJECT_MAPPER.createArrayNode(); + msgs.forEach(msg -> { + ObjectNode msgNode = JacksonUtil.newObjectNode(); + msgNode.set("msg", JacksonUtil.toJsonNode(msg.getData())); + msgNode.set("metadata", JacksonUtil.valueToTree(msg.getMetaData().getData())); + mergedData.add(msgNode); + }); + return JacksonUtil.toString(mergedData); + } + + private TbMsgMetaData getMetadata() { + TbMsgMetaData metaData = new TbMsgMetaData(); + metaData.putValue("ts", String.valueOf(System.currentTimeMillis())); + return metaData; + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNodeConfiguration.java new file mode 100644 index 0000000000..afb9cc62b6 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNodeConfiguration.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.rule.engine.deduplication; + +import lombok.Data; +import org.thingsboard.rule.engine.api.NodeConfiguration; + +@Data +public class TbMsgDeduplicationNodeConfiguration implements NodeConfiguration { + + private int interval; + private DeduplicationId id; + private DeduplicationStrategy strategy; + + // Advanced settings: + private int maxPendingMsgs; + private int maxRetries; + + // only for DeduplicationStrategy.ALL: + private String outMsgType; + private String queueName; + + @Override + public TbMsgDeduplicationNodeConfiguration defaultConfiguration() { + TbMsgDeduplicationNodeConfiguration configuration = new TbMsgDeduplicationNodeConfiguration(); + configuration.setInterval(60); + configuration.setId(DeduplicationId.ORIGINATOR); + configuration.setStrategy(DeduplicationStrategy.FIRST); + configuration.setMaxPendingMsgs(100); + configuration.setMaxRetries(3); + return configuration; + } +} 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 5a313da469..5c2efb7e5b 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 @@ -48,7 +48,6 @@ import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbActionNodeMsgDelayConfig" ) - public class TbMsgDelayNode implements TbNode { private static final String TB_MSG_DELAY_NODE_MSG = "TbMsgDelayNodeMsg"; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbMsgDeduplicationNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbMsgDeduplicationNodeTest.java new file mode 100644 index 0000000000..923d5667f4 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbMsgDeduplicationNodeTest.java @@ -0,0 +1,402 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.rule.engine.action; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; +import org.mockito.stubbing.Answer; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.api.TbRelationTypes; +import org.thingsboard.rule.engine.deduplication.DeduplicationStrategy; +import org.thingsboard.rule.engine.deduplication.TbMsgDeduplicationNode; +import org.thingsboard.rule.engine.deduplication.TbMsgDeduplicationNodeConfiguration; +import org.thingsboard.server.common.data.id.DeviceId; +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.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.session.SessionMsgType; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@Slf4j +public class TbMsgDeduplicationNodeTest { + + private static final String MAIN_QUEUE_NAME = "Main"; + private static final String HIGH_PRIORITY_QUEUE_NAME = "HighPriority"; + private static final String TB_MSG_DEDUPLICATION_TIMEOUT_MSG = "TbMsgDeduplicationNodeMsg"; + + private TbContext ctx; + + private final ThingsBoardThreadFactory factory = ThingsBoardThreadFactory.forName("de-duplication-node-test"); + private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(factory); + private final int deduplicationInterval = 1; + + private TenantId tenantId; + + private TbMsgDeduplicationNode node; + private TbMsgDeduplicationNodeConfiguration config; + private TbNodeConfiguration nodeConfiguration; + + private CountDownLatch awaitTellSelfLatch; + + @BeforeEach + public void init() throws TbNodeException { + ctx = mock(TbContext.class); + + tenantId = TenantId.fromUUID(UUID.randomUUID()); + RuleNodeId ruleNodeId = new RuleNodeId(UUID.randomUUID()); + + when(ctx.getSelfId()).thenReturn(ruleNodeId); + when(ctx.getTenantId()).thenReturn(tenantId); + + doAnswer((Answer) invocationOnMock -> { + String type = (String) (invocationOnMock.getArguments())[1]; + EntityId originator = (EntityId) (invocationOnMock.getArguments())[2]; + TbMsgMetaData metaData = (TbMsgMetaData) (invocationOnMock.getArguments())[3]; + String data = (String) (invocationOnMock.getArguments())[4]; + return TbMsg.newMsg(type, originator, metaData.copy(), data); + }).when(ctx).newMsg(isNull(), eq(TB_MSG_DEDUPLICATION_TIMEOUT_MSG), nullable(EntityId.class), any(TbMsgMetaData.class), any(String.class)); + node = spy(new TbMsgDeduplicationNode()); + config = new TbMsgDeduplicationNodeConfiguration().defaultConfiguration(); + } + + private void invokeTellSelf(int maxNumberOfInvocation) { + invokeTellSelf(maxNumberOfInvocation, false, 0); + } + + private void invokeTellSelf(int maxNumberOfInvocation, boolean delayScheduleTimeout, int delayMultiplier) { + AtomicLong scheduleTimeout = new AtomicLong(deduplicationInterval); + AtomicInteger scheduleCount = new AtomicInteger(0); + doAnswer((Answer) invocationOnMock -> { + scheduleCount.getAndIncrement(); + if (scheduleCount.get() <= maxNumberOfInvocation) { + TbMsg msg = (TbMsg) (invocationOnMock.getArguments())[0]; + executorService.schedule(() -> { + try { + node.onMsg(ctx, msg); + awaitTellSelfLatch.countDown(); + } catch (ExecutionException | InterruptedException | TbNodeException e) { + log.error("Failed to execute tellSelf method call due to: ", e); + } + }, scheduleTimeout.get(), TimeUnit.SECONDS); + if (delayScheduleTimeout) { + scheduleTimeout.set(scheduleTimeout.get() * delayMultiplier); + } + } + + return null; + }).when(ctx).tellSelf(ArgumentMatchers.any(TbMsg.class), ArgumentMatchers.anyLong()); + } + + @AfterEach + public void destroy() { + executorService.shutdown(); + node.destroy(); + } + + @Test + public void given_100_messages_strategy_first_then_verifyOutput() throws TbNodeException, ExecutionException, InterruptedException { + int wantedNumberOfTellSelfInvocation = 2; + int msgCount = 100; + awaitTellSelfLatch = new CountDownLatch(wantedNumberOfTellSelfInvocation); + invokeTellSelf(wantedNumberOfTellSelfInvocation); + + config.setInterval(deduplicationInterval); + config.setMaxPendingMsgs(msgCount); + nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + node.init(ctx, nodeConfiguration); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + long currentTimeMillis = System.currentTimeMillis(); + + List inputMsgs = getTbMsgs(deviceId, msgCount, currentTimeMillis, 500); + for (TbMsg msg : inputMsgs) { + node.onMsg(ctx, msg); + } + + TbMsg msgToReject = createMsg(deviceId, inputMsgs.get(inputMsgs.size() - 1).getMetaDataTs() + 2); + node.onMsg(ctx, msgToReject); + + awaitTellSelfLatch.await(); + + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + ArgumentCaptor successCaptor = ArgumentCaptor.forClass(Runnable.class); + ArgumentCaptor> failureCaptor = ArgumentCaptor.forClass(Consumer.class); + + verify(ctx, times(msgCount)).ack(any()); + verify(ctx, times(1)).tellFailure(eq(msgToReject), any()); + verify(node, times(msgCount + wantedNumberOfTellSelfInvocation + 1)).onMsg(eq(ctx), any()); + verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); + Assertions.assertEquals(inputMsgs.get(0), newMsgCaptor.getValue()); + } + + @Test + public void given_100_messages_strategy_last_then_verifyOutput() throws TbNodeException, ExecutionException, InterruptedException { + int wantedNumberOfTellSelfInvocation = 2; + int msgCount = 100; + awaitTellSelfLatch = new CountDownLatch(wantedNumberOfTellSelfInvocation); + invokeTellSelf(wantedNumberOfTellSelfInvocation); + + config.setStrategy(DeduplicationStrategy.LAST); + config.setInterval(deduplicationInterval); + config.setMaxPendingMsgs(msgCount); + nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + node.init(ctx, nodeConfiguration); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + long currentTimeMillis = System.currentTimeMillis(); + + List inputMsgs = getTbMsgs(deviceId, msgCount, currentTimeMillis, 500); + TbMsg msgWithLatestTs = getMsgWithLatestTs(inputMsgs); + + for (TbMsg msg : inputMsgs) { + node.onMsg(ctx, msg); + } + + TbMsg msgToReject = createMsg(deviceId, inputMsgs.get(inputMsgs.size() - 1).getMetaDataTs() + 2); + node.onMsg(ctx, msgToReject); + + awaitTellSelfLatch.await(); + + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + ArgumentCaptor successCaptor = ArgumentCaptor.forClass(Runnable.class); + ArgumentCaptor> failureCaptor = ArgumentCaptor.forClass(Consumer.class); + + verify(ctx, times(msgCount)).ack(any()); + verify(ctx, times(1)).tellFailure(eq(msgToReject), any()); + verify(node, times(msgCount + wantedNumberOfTellSelfInvocation + 1)).onMsg(eq(ctx), any()); + verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); + Assertions.assertEquals(msgWithLatestTs, newMsgCaptor.getValue()); + } + + @Test + public void given_100_messages_strategy_all_then_verifyOutput() throws TbNodeException, ExecutionException, InterruptedException { + int wantedNumberOfTellSelfInvocation = 2; + int msgCount = 100; + awaitTellSelfLatch = new CountDownLatch(wantedNumberOfTellSelfInvocation); + invokeTellSelf(wantedNumberOfTellSelfInvocation); + + config.setInterval(deduplicationInterval); + config.setStrategy(DeduplicationStrategy.ALL); + config.setOutMsgType(SessionMsgType.POST_ATTRIBUTES_REQUEST.name()); + config.setQueueName(HIGH_PRIORITY_QUEUE_NAME); + nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + node.init(ctx, nodeConfiguration); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + long currentTimeMillis = System.currentTimeMillis(); + + List inputMsgs = getTbMsgs(deviceId, msgCount, currentTimeMillis, 500); + for (TbMsg msg : inputMsgs) { + node.onMsg(ctx, msg); + } + + awaitTellSelfLatch.await(); + + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + ArgumentCaptor successCaptor = ArgumentCaptor.forClass(Runnable.class); + ArgumentCaptor> failureCaptor = ArgumentCaptor.forClass(Consumer.class); + + verify(ctx, times(msgCount)).ack(any()); + verify(node, times(msgCount + wantedNumberOfTellSelfInvocation)).onMsg(eq(ctx), any()); + verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); + + Assertions.assertEquals(1, newMsgCaptor.getAllValues().size()); + TbMsg outMessage = newMsgCaptor.getAllValues().get(0); + Assertions.assertEquals(getMergedData(inputMsgs), outMessage.getData()); + Assertions.assertEquals(deviceId, outMessage.getOriginator()); + Assertions.assertEquals(config.getOutMsgType(), outMessage.getType()); + Assertions.assertEquals(config.getQueueName(), outMessage.getQueueName()); + } + + @Test + public void given_100_messages_strategy_all_then_verifyOutput_2_packs() throws TbNodeException, ExecutionException, InterruptedException { + int wantedNumberOfTellSelfInvocation = 2; + int msgCount = 100; + awaitTellSelfLatch = new CountDownLatch(wantedNumberOfTellSelfInvocation); + invokeTellSelf(wantedNumberOfTellSelfInvocation, true, 3); + + config.setInterval(deduplicationInterval); + config.setStrategy(DeduplicationStrategy.ALL); + config.setOutMsgType(SessionMsgType.POST_ATTRIBUTES_REQUEST.name()); + config.setQueueName(HIGH_PRIORITY_QUEUE_NAME); + nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + node.init(ctx, nodeConfiguration); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + long currentTimeMillis = System.currentTimeMillis(); + + List firstMsgPack = getTbMsgs(deviceId, msgCount / 2, currentTimeMillis, 500); + for (TbMsg msg : firstMsgPack) { + node.onMsg(ctx, msg); + } + long firstPackDeduplicationPackEndTs = firstMsgPack.get(0).getMetaDataTs() + TimeUnit.SECONDS.toMillis(deduplicationInterval); + + List secondMsgPack = getTbMsgs(deviceId, msgCount / 2, firstPackDeduplicationPackEndTs, 500); + for (TbMsg msg : secondMsgPack) { + node.onMsg(ctx, msg); + } + + awaitTellSelfLatch.await(); + + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + ArgumentCaptor successCaptor = ArgumentCaptor.forClass(Runnable.class); + ArgumentCaptor> failureCaptor = ArgumentCaptor.forClass(Consumer.class); + + verify(ctx, times(msgCount)).ack(any()); + verify(node, times(msgCount + wantedNumberOfTellSelfInvocation)).onMsg(eq(ctx), any()); + verify(ctx, times(2)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); + + List resultMsgs = newMsgCaptor.getAllValues(); + Assertions.assertEquals(2, resultMsgs.size()); + + TbMsg firstMsg = resultMsgs.get(0); + Assertions.assertEquals(getMergedData(firstMsgPack), firstMsg.getData()); + Assertions.assertEquals(deviceId, firstMsg.getOriginator()); + Assertions.assertEquals(config.getOutMsgType(), firstMsg.getType()); + Assertions.assertEquals(config.getQueueName(), firstMsg.getQueueName()); + + TbMsg secondMsg = resultMsgs.get(1); + Assertions.assertEquals(getMergedData(secondMsgPack), secondMsg.getData()); + Assertions.assertEquals(deviceId, secondMsg.getOriginator()); + Assertions.assertEquals(config.getOutMsgType(), secondMsg.getType()); + Assertions.assertEquals(config.getQueueName(), secondMsg.getQueueName()); + } + + @Test + public void given_100_messages_strategy_last_then_verifyOutput_2_packs() throws TbNodeException, ExecutionException, InterruptedException { + int wantedNumberOfTellSelfInvocation = 2; + int msgCount = 100; + awaitTellSelfLatch = new CountDownLatch(wantedNumberOfTellSelfInvocation); + invokeTellSelf(wantedNumberOfTellSelfInvocation, true, 3); + + config.setInterval(deduplicationInterval); + config.setStrategy(DeduplicationStrategy.LAST); + nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + node.init(ctx, nodeConfiguration); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + long currentTimeMillis = System.currentTimeMillis(); + + List firstMsgPack = getTbMsgs(deviceId, msgCount / 2, currentTimeMillis, 500); + for (TbMsg msg : firstMsgPack) { + node.onMsg(ctx, msg); + } + long firstPackDeduplicationPackEndTs = firstMsgPack.get(0).getMetaDataTs() + TimeUnit.SECONDS.toMillis(deduplicationInterval); + TbMsg msgWithLatestTsInFirstPack = getMsgWithLatestTs(firstMsgPack); + + List secondMsgPack = getTbMsgs(deviceId, msgCount / 2, firstPackDeduplicationPackEndTs, 500); + for (TbMsg msg : secondMsgPack) { + node.onMsg(ctx, msg); + } + TbMsg msgWithLatestTsInSecondPack = getMsgWithLatestTs(secondMsgPack); + + awaitTellSelfLatch.await(); + + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + ArgumentCaptor successCaptor = ArgumentCaptor.forClass(Runnable.class); + ArgumentCaptor> failureCaptor = ArgumentCaptor.forClass(Consumer.class); + + verify(ctx, times(msgCount)).ack(any()); + verify(node, times(msgCount + wantedNumberOfTellSelfInvocation)).onMsg(eq(ctx), any()); + verify(ctx, times(2)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); + + List resultMsgs = newMsgCaptor.getAllValues(); + Assertions.assertEquals(2, resultMsgs.size()); + Assertions.assertTrue(resultMsgs.contains(msgWithLatestTsInFirstPack)); + Assertions.assertTrue(resultMsgs.contains(msgWithLatestTsInSecondPack)); + } + + private TbMsg getMsgWithLatestTs(List firstMsgPack) { + int indexOfLastMsgInArray = firstMsgPack.size() - 1; + int indexToSetMaxTs = new Random().nextInt(indexOfLastMsgInArray) + 1; + TbMsg currentMaxTsMsg = firstMsgPack.get(indexOfLastMsgInArray); + TbMsg newLastMsgOfArray = firstMsgPack.get(indexToSetMaxTs); + firstMsgPack.set(indexOfLastMsgInArray, newLastMsgOfArray); + firstMsgPack.set(indexToSetMaxTs, currentMaxTsMsg); + return currentMaxTsMsg; + } + + private List getTbMsgs(DeviceId deviceId, int msgCount, long currentTimeMillis, int initTsStep) { + List inputMsgs = new ArrayList<>(); + var ts = currentTimeMillis + initTsStep; + for (int i = 0; i < msgCount; i++) { + inputMsgs.add(createMsg(deviceId, ts)); + ts += 2; + } + return inputMsgs; + } + + private TbMsg createMsg(DeviceId deviceId, long ts) { + ObjectNode dataNode = JacksonUtil.newObjectNode(); + dataNode.put("deviceId", deviceId.getId().toString()); + TbMsgMetaData metaData = new TbMsgMetaData(); + metaData.putValue("ts", String.valueOf(ts)); + return TbMsg.newMsg( + MAIN_QUEUE_NAME, + SessionMsgType.POST_TELEMETRY_REQUEST.name(), + deviceId, + metaData, + JacksonUtil.toString(dataNode)); + } + + private String getMergedData(List msgs) { + ArrayNode mergedData = JacksonUtil.OBJECT_MAPPER.createArrayNode(); + msgs.forEach(msg -> { + ObjectNode msgNode = JacksonUtil.newObjectNode(); + msgNode.set("msg", JacksonUtil.toJsonNode(msg.getData())); + msgNode.set("metadata", JacksonUtil.valueToTree(msg.getMetaData().getData())); + mergedData.add(msgNode); + }); + return JacksonUtil.toString(mergedData); + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java index 8a53148204..08081ff961 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java @@ -56,6 +56,7 @@ import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; @@ -93,6 +94,8 @@ public class TenantIdLoaderTest { @Mock private RuleEngineAlarmService alarmService; @Mock + private AlarmCommentService alarmCommentService; + @Mock private RuleChainService ruleChainService; @Mock private EntityViewService entityViewService; diff --git a/ui-ngx/src/app/core/http/edge.service.ts b/ui-ngx/src/app/core/http/edge.service.ts index 27cfefb6d6..9f2d576d77 100644 --- a/ui-ngx/src/app/core/http/edge.service.ts +++ b/ui-ngx/src/app/core/http/edge.service.ts @@ -21,7 +21,7 @@ import { HttpClient } from '@angular/common/http'; import { PageLink, TimePageLink } from '@shared/models/page/page-link'; import { PageData } from '@shared/models/page/page-data'; import { EntitySubtype } from '@app/shared/models/entity-type.models'; -import { Edge, EdgeEvent, EdgeInfo, EdgeSearchQuery } from '@shared/models/edge.models'; +import { Edge, EdgeEvent, EdgeInfo, EdgeInstallInstructions, EdgeSearchQuery } from '@shared/models/edge.models'; import { EntityId } from '@shared/models/id/entity-id'; import { BulkImportRequest, BulkImportResult } from '@home/components/import-export/import-export.models'; @@ -113,4 +113,8 @@ export class EdgeService { public bulkImportEdges(entitiesData: BulkImportRequest, config?: RequestConfig): Observable { return this.http.post('/api/edge/bulk_import', entitiesData, defaultHttpOptionsFromConfig(config)); } + + public getEdgeDockerInstallInstructions(edgeId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/edge/instructions/${edgeId}`, defaultHttpOptionsFromConfig(config)); + } } 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 f15a969b19..ac478e775c 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 @@ -58,6 +58,12 @@ [queueType]="serviceType" formControlName="defaultQueueName"> + +
{{'device-profile.default-edge-rule-chain-hint' | translate}}
+
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 188fad26dc..4af92f2141 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 { RuleChainType } from '@shared/models/rule-chain.models'; export interface AddDeviceProfileDialogData { deviceProfileName: string; @@ -93,6 +94,8 @@ export class AddDeviceProfileDialogComponent extends serviceType = ServiceType.TB_RULE_ENGINE; + edgeRuleChainType = RuleChainType.EDGE; + constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: AddDeviceProfileDialogData, @@ -111,6 +114,7 @@ export class AddDeviceProfileDialogComponent extends defaultRuleChainId: [null, []], defaultDashboardId: [null, []], defaultQueueName: [null, []], + defaultEdgeRuleChainId: [null, []], description: ['', []] } ); @@ -205,6 +209,9 @@ export class AddDeviceProfileDialogComponent extends if (this.deviceProfileDetailsFormGroup.get('defaultDashboardId').value) { deviceProfile.defaultDashboardId = new DashboardId(this.deviceProfileDetailsFormGroup.get('defaultDashboardId').value); } + if (this.deviceProfileDetailsFormGroup.get('defaultEdgeRuleChainId').value) { + deviceProfile.defaultEdgeRuleChainId = new RuleChainId(this.deviceProfileDetailsFormGroup.get('defaultEdgeRuleChainId').value); + } this.deviceProfileService.saveDeviceProfile(deepTrim(deviceProfile)).subscribe( (savedDeviceProfile) => { this.dialogRef.close(savedDeviceProfile); diff --git a/ui-ngx/src/app/modules/home/components/profile/asset-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/asset-profile.component.html index 0c616c69be..61ce6395d4 100644 --- a/ui-ngx/src/app/modules/home/components/profile/asset-profile.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/asset-profile.component.html @@ -77,6 +77,12 @@ [queueType]="serviceType" formControlName="defaultQueueName"> + +
{{'asset-profile.default-edge-rule-chain-hint' | translate}}
+
{ serviceType = ServiceType.TB_RULE_ENGINE; + edgeRuleChainType = RuleChainType.EDGE; + TB_SERVICE_QUEUE = TB_SERVICE_QUEUE; assetProfileId: EntityId; @@ -73,6 +76,7 @@ export class AssetProfileComponent extends EntityComponent { defaultRuleChainId: [entity && entity.defaultRuleChainId ? entity.defaultRuleChainId.id : null, []], defaultDashboardId: [entity && entity.defaultDashboardId ? entity.defaultDashboardId.id : null, []], defaultQueueName: [entity ? entity.defaultQueueName : null, []], + defaultEdgeRuleChainId: [entity && entity.defaultEdgeRuleChainId ? entity.defaultEdgeRuleChainId.id : null, []], description: [entity ? entity.description : '', []], } ); @@ -86,6 +90,7 @@ export class AssetProfileComponent extends EntityComponent { 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({defaultEdgeRuleChainId: entity.defaultEdgeRuleChainId ? entity.defaultEdgeRuleChainId.id : null}, {emitEvent: false}); this.entityForm.patchValue({description: entity.description}, {emitEvent: false}); } @@ -96,6 +101,9 @@ export class AssetProfileComponent extends EntityComponent { if (formValue.defaultDashboardId) { formValue.defaultDashboardId = new DashboardId(formValue.defaultDashboardId); } + if (formValue.defaultEdgeRuleChainId) { + formValue.defaultEdgeRuleChainId = new RuleChainId(formValue.defaultEdgeRuleChainId); + } return super.prepareFormValue(formValue); } 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 0b06eec02c..ed28094f52 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 @@ -77,6 +77,12 @@ [queueType]="serviceType" formControlName="defaultQueueName"> + +
{{'device-profile.default-edge-rule-chain-hint' | translate}}
+
{ serviceType = ServiceType.TB_RULE_ENGINE; + edgeRuleChainType = RuleChainType.EDGE; + deviceProfileId: EntityId; otaUpdateType = OtaUpdateType; @@ -118,6 +121,7 @@ export class DeviceProfileComponent extends EntityComponent { defaultRuleChainId: [entity && entity.defaultRuleChainId ? entity.defaultRuleChainId.id : null, []], defaultDashboardId: [entity && entity.defaultDashboardId ? entity.defaultDashboardId.id : null, []], defaultQueueName: [entity ? entity.defaultQueueName : null, []], + defaultEdgeRuleChainId: [entity && entity.defaultEdgeRuleChainId ? entity.defaultEdgeRuleChainId.id : null, []], firmwareId: [entity ? entity.firmwareId : null], softwareId: [entity ? entity.softwareId : null], description: [entity ? entity.description : '', []], @@ -198,6 +202,7 @@ export class DeviceProfileComponent extends EntityComponent { 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({defaultEdgeRuleChainId: entity.defaultEdgeRuleChainId ? entity.defaultEdgeRuleChainId.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 +215,9 @@ export class DeviceProfileComponent extends EntityComponent { if (formValue.defaultDashboardId) { formValue.defaultDashboardId = new DashboardId(formValue.defaultDashboardId); } + if (formValue.defaultEdgeRuleChainId) { + formValue.defaultEdgeRuleChainId = new RuleChainId(formValue.defaultEdgeRuleChainId); + } const deviceProvisionConfiguration: DeviceProvisionConfiguration = formValue.profileData.provisionConfiguration; formValue.provisionType = deviceProvisionConfiguration.type; formValue.provisionDeviceKey = deviceProvisionConfiguration.provisionDeviceKey; diff --git a/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html index fd99e1bc54..2925fbfe94 100644 --- a/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html @@ -16,7 +16,7 @@ --> - {{ 'rulechain.rulechain-required' | translate }} + + + diff --git a/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts index 3db424012f..1a05585ddf 100644 --- a/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts @@ -45,16 +45,17 @@ export class RuleChainAutocompleteComponent implements ControlValueAccessor, OnI selectRuleChainFormGroup: FormGroup; - ruleChainLabel = 'rulechain.rulechain'; - modelValue: string | null; @Input() - labelText: string; + labelText: string = 'rulechain.rulechain'; @Input() requiredText: string; + @Input() + ruleChainType: RuleChainType = RuleChainType.CORE; + private requiredValue: boolean; get required(): boolean { return this.requiredValue; @@ -191,9 +192,8 @@ export class RuleChainAutocompleteComponent implements ControlValueAccessor, OnI fetchRuleChain(searchText?: string): Observable>> { this.searchText = searchText; - // @voba: at the moment device profiles are not supported by edge, so 'core' hardcoded return this.entityService.getEntitiesByNameFilter(EntityType.RULE_CHAIN, searchText, - 50, RuleChainType.CORE, {ignoreLoading: true}).pipe( + 50, this.ruleChainType, {ignoreLoading: true}).pipe( catchError(() => of([])) ); } diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.html b/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.html new file mode 100644 index 0000000000..fc375316e0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.html @@ -0,0 +1,43 @@ + +
+ +

info_outline + {{ 'edge.install-connect-instructions' | translate }}

+ + +
+ + +
+
+ +
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.ts new file mode 100644 index 0000000000..5afa385e93 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.ts @@ -0,0 +1,46 @@ +/// +/// Copyright © 2016-2022 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT 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 } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; +import { DialogComponent } from "@shared/components/dialog.component"; +import { Store } from "@ngrx/store"; +import { AppState } from "@core/core.state"; +import { Router } from "@angular/router"; + +export interface EdgeInstructionsData { + instructions: string; +} + +@Component({ + selector: 'tb-edge-instructions', + templateUrl: './edge-instructions-dialog.component.html' +}) +export class EdgeInstructionsDialogComponent extends DialogComponent { + + instructions: string = this.data.instructions; + + constructor(protected store: Store, + protected router: Router, + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: EdgeInstructionsData) { + super(store, router, dialogRef); + } + + cancel(): void { + this.dialogRef.close(null); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge.component.html b/ui-ngx/src/app/modules/home/pages/edge/edge.component.html index 6acc4cba0c..e1304c3e06 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge.component.html +++ b/ui-ngx/src/app/modules/home/pages/edge/edge.component.html @@ -112,6 +112,15 @@ edge.sync +
+ +
> { @@ -526,6 +530,23 @@ export class EdgesTableConfigResolver implements Resolve { + this.dialog.open(EdgeInstructionsDialogComponent, { + disableClose: false, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + instructions: edgeInstructionsTemplate.dockerInstallInstructions + } + }); + } + ) + } + onEdgeAction(action: EntityAction, config: EntityTableConfig): boolean { switch (action.action) { case 'open': @@ -558,6 +579,9 @@ export class EdgesTableConfigResolver implements Resolve, ExportableEntity defaultRuleChainId?: RuleChainId; defaultDashboardId?: DashboardId; defaultQueueName?: string; + defaultEdgeRuleChainId?: RuleChainId; } export interface AssetProfileInfo extends EntityInfoData { diff --git a/ui-ngx/src/app/shared/models/audit-log.models.ts b/ui-ngx/src/app/shared/models/audit-log.models.ts index 056c6aa569..e1f11ceee7 100644 --- a/ui-ngx/src/app/shared/models/audit-log.models.ts +++ b/ui-ngx/src/app/shared/models/audit-log.models.ts @@ -47,6 +47,9 @@ export enum ActionType { RELATIONS_DELETED = 'RELATIONS_DELETED', ALARM_ACK = 'ALARM_ACK', ALARM_CLEAR = 'ALARM_CLEAR', + ADDED_COMMENT = 'ADDED_COMMENT', + UPDATED_COMMENT = 'UPDATED_COMMENT', + DELETED_COMMENT = 'DELETED_COMMENT', LOGIN = 'LOGIN', LOGOUT = 'LOGOUT', LOCKOUT = 'LOCKOUT', @@ -85,6 +88,9 @@ export const actionTypeTranslations = new Map( [ActionType.RELATIONS_DELETED, 'audit-log.type-relations-delete'], [ActionType.ALARM_ACK, 'audit-log.type-alarm-ack'], [ActionType.ALARM_CLEAR, 'audit-log.type-alarm-clear'], + [ActionType.ADDED_COMMENT, 'audit-log.type-added-comment'], + [ActionType.UPDATED_COMMENT, 'audit-log.type-updated-comment'], + [ActionType.DELETED_COMMENT, 'audit-log.type-deleted-comment'], [ActionType.LOGIN, 'audit-log.type-login'], [ActionType.LOGOUT, 'audit-log.type-logout'], [ActionType.LOCKOUT, 'audit-log.type-lockout'], diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index d06963544e..2bc0c63c3e 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -579,6 +579,7 @@ export interface DeviceProfile extends BaseData, ExportableEnti firmwareId?: OtaPackageId; softwareId?: OtaPackageId; profileData: DeviceProfileData; + defaultEdgeRuleChainId?: RuleChainId; } export interface DeviceProfileInfo extends EntityInfoData { diff --git a/ui-ngx/src/app/shared/models/edge.models.ts b/ui-ngx/src/app/shared/models/edge.models.ts index 59f9d54318..f7502702bd 100644 --- a/ui-ngx/src/app/shared/models/edge.models.ts +++ b/ui-ngx/src/app/shared/models/edge.models.ts @@ -167,3 +167,7 @@ export interface EdgeEvent extends BaseData { uid: string; body: string; } + +export interface EdgeInstallInstructions { + dockerInstallInstructions: string; +} 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 dd28154103..1164e01bf5 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -728,6 +728,9 @@ "type-relations-delete": "All relation deleted", "type-alarm-ack": "Acknowledged", "type-alarm-clear": "Cleared", + "type-added-comment": "Added comment", + "type-updated-comment": "Updated comment", + "type-deleted-comment": "Deleted comment", "type-login": "Login", "type-logout": "Logout", "type-lockout": "Lockout", @@ -1280,6 +1283,8 @@ "description": "Description", "default": "Default", "default-rule-chain": "Default rule chain", + "default-edge-rule-chain": "Default edge rule chain", + "default-edge-rule-chain-hint": "Used on edge as rule chain to process incoming data for assets of this asset profile", "mobile-dashboard": "Mobile dashboard", "mobile-dashboard-hint": "Used by mobile application as a asset details dashboard", "select-queue-hint": "Select from a drop-down list.", @@ -1340,6 +1345,8 @@ "profile-configuration": "Profile configuration", "transport-configuration": "Transport configuration", "default-rule-chain": "Default rule chain", + "default-edge-rule-chain": "Default edge rule chain", + "default-edge-rule-chain-hint": "Used on edge as rule chain to process incoming data for devices of this device profile", "mobile-dashboard": "Mobile dashboard", "mobile-dashboard-hint": "Used by mobile application as a device details dashboard", "select-queue-hint": "Select from a drop-down list.", @@ -1746,6 +1753,7 @@ "make-private-edge-title": "Are you sure you want to make the edge '{{edgeName}}' private?", "make-private-edge-text": "After the confirmation the edge and all its data will be made private and won't be accessible by others.", "import": "Import edge", + "install-connect-instructions": "Install & Connect Instructions", "label": "Label", "load-entity-error": "Failed to load data. Entity has been deleted.", "assign-new-edge": "Assign new edge",