From d4c4aba09e6675d8ae367d902e995b2786a2713c Mon Sep 17 00:00:00 2001 From: dudnikmaksim Date: Wed, 15 Nov 2023 12:56:25 +0200 Subject: [PATCH 01/38] Make new created connector active --- .../widget/lib/gateway/gateway-connectors.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts index ec130600ac..9fa1f7b5f2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts @@ -210,7 +210,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie value }]; const attributesToDelete = []; - const scope = (this.initialConnector && this.activeConnectors.includes(this.initialConnector.name)) + const scope = (!this.initialConnector || this.activeConnectors.includes(this.initialConnector.name)) ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE; let updateActiveConnectors = false; From deeb0208a9590f59973ee1ea5e0b60b1bb79faa1 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 21 Nov 2023 16:51:27 +0200 Subject: [PATCH 02/38] Introduce edge upgrade documentation --- .../install}/centos/instructions.md | 2 +- .../install}/docker/instructions.md | 2 +- .../install}/docker/localhost_warning.md | 0 .../install}/ubuntu/instructions.md | 2 +- .../upgrade/centos/instructions.md | 30 +++ .../upgrade/docker/instructions.md | 15 ++ .../upgrade/docker/start_service.md | 7 + .../upgrade/docker/stop_service.md | 11 + .../instructions/upgrade/docker/upgrade_db.md | 53 +++++ .../upgrade/docker/upgrade_preparing.md | 26 +++ .../instructions/upgrade/start_service.md | 6 + .../edge/instructions/upgrade/stop_service.md | 6 + .../upgrade/ubuntu/instructions.md | 19 ++ .../edge/instructions/upgrade/upgrade_db.md | 8 + .../instructions/upgrade/upgrade_preparing.md | 37 ++++ .../server/controller/EdgeController.java | 35 +++- .../DefaultEdgeInstallService.java | 22 +- .../DefaultEdgeUpgradeService.java | 190 ++++++++++++++++++ .../edge/instructions/EdgeInstallService.java | 4 +- .../edge/instructions/EdgeUpgradeService.java | 25 +++ .../service/edge/rpc/EdgeGrpcSession.java | 9 +- ...nstructions.java => EdgeInstructions.java} | 6 +- .../thingsboard/rest/client/RestClient.java | 8 +- 23 files changed, 494 insertions(+), 29 deletions(-) rename application/src/main/data/json/edge/{install_instructions => instructions/install}/centos/instructions.md (98%) rename application/src/main/data/json/edge/{install_instructions => instructions/install}/docker/instructions.md (98%) rename application/src/main/data/json/edge/{install_instructions => instructions/install}/docker/localhost_warning.md (100%) rename application/src/main/data/json/edge/{install_instructions => instructions/install}/ubuntu/instructions.md (98%) create mode 100644 application/src/main/data/json/edge/instructions/upgrade/centos/instructions.md create mode 100644 application/src/main/data/json/edge/instructions/upgrade/docker/instructions.md create mode 100644 application/src/main/data/json/edge/instructions/upgrade/docker/start_service.md create mode 100644 application/src/main/data/json/edge/instructions/upgrade/docker/stop_service.md create mode 100644 application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md create mode 100644 application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md create mode 100644 application/src/main/data/json/edge/instructions/upgrade/start_service.md create mode 100644 application/src/main/data/json/edge/instructions/upgrade/stop_service.md create mode 100644 application/src/main/data/json/edge/instructions/upgrade/ubuntu/instructions.md create mode 100644 application/src/main/data/json/edge/instructions/upgrade/upgrade_db.md create mode 100644 application/src/main/data/json/edge/instructions/upgrade/upgrade_preparing.md create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeService.java rename common/data/src/main/java/org/thingsboard/server/common/data/edge/{EdgeInstallInstructions.java => EdgeInstructions.java} (90%) diff --git a/application/src/main/data/json/edge/install_instructions/centos/instructions.md b/application/src/main/data/json/edge/instructions/install/centos/instructions.md similarity index 98% rename from application/src/main/data/json/edge/install_instructions/centos/instructions.md rename to application/src/main/data/json/edge/instructions/install/centos/instructions.md index 126c79b641..9ad882c387 100644 --- a/application/src/main/data/json/edge/install_instructions/centos/instructions.md +++ b/application/src/main/data/json/edge/instructions/install/centos/instructions.md @@ -1,4 +1,4 @@ -Here is the list of commands, that can be used to quickly install ThingsBoard Edge on RHEL/CentOS 7/8 and connect to the cloud. +Here is the list of commands, that can be used to quickly install ThingsBoard Edge on RHEL/CentOS 7/8 and connect to the server. #### Prerequisites Before continue to installation execute the following commands in order to install necessary tools: diff --git a/application/src/main/data/json/edge/install_instructions/docker/instructions.md b/application/src/main/data/json/edge/instructions/install/docker/instructions.md similarity index 98% rename from application/src/main/data/json/edge/install_instructions/docker/instructions.md rename to application/src/main/data/json/edge/instructions/install/docker/instructions.md index e308a38076..cbfe5f8c4c 100644 --- a/application/src/main/data/json/edge/install_instructions/docker/instructions.md +++ b/application/src/main/data/json/edge/instructions/install/docker/instructions.md @@ -1,4 +1,4 @@ -Here is the list of commands, that can be used to quickly install ThingsBoard Edge using docker compose and connect to the cloud. +Here is the list of commands, that can be used to quickly install ThingsBoard Edge using docker compose and connect to the server. #### Prerequisites diff --git a/application/src/main/data/json/edge/install_instructions/docker/localhost_warning.md b/application/src/main/data/json/edge/instructions/install/docker/localhost_warning.md similarity index 100% rename from application/src/main/data/json/edge/install_instructions/docker/localhost_warning.md rename to application/src/main/data/json/edge/instructions/install/docker/localhost_warning.md diff --git a/application/src/main/data/json/edge/install_instructions/ubuntu/instructions.md b/application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md similarity index 98% rename from application/src/main/data/json/edge/install_instructions/ubuntu/instructions.md rename to application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md index 425c08662b..2a6d9c0bc3 100644 --- a/application/src/main/data/json/edge/install_instructions/ubuntu/instructions.md +++ b/application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md @@ -1,4 +1,4 @@ -Here is the list of commands, that can be used to quickly install ThingsBoard Edge on Ubuntu Server and connect to the cloud. +Here is the list of commands, that can be used to quickly install ThingsBoard Edge on Ubuntu Server and connect to the server. #### Install Java 11 (OpenJDK) ThingsBoard service is running on Java 11. Follow these instructions to install OpenJDK 11: diff --git a/application/src/main/data/json/edge/instructions/upgrade/centos/instructions.md b/application/src/main/data/json/edge/instructions/upgrade/centos/instructions.md new file mode 100644 index 0000000000..d0b2a1895d --- /dev/null +++ b/application/src/main/data/json/edge/instructions/upgrade/centos/instructions.md @@ -0,0 +1,30 @@ +#### Upgrading to ${TB_EDGE_VERSION_TITLE} +**NOTE**:These steps are applicable for ThingsBoard Edge ${CURRENT_TB_EDGE_VERSION} version. + +**ThingsBoard Edge package download** +```bash +wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_TAG}/tb-edge-${TB_EDGE_TAG}.rpm +{:copy-code} +``` + +#### ThingsBoard Edge service upgrade + +Stop ThingsBoard Edge service if it is running: + +```bash +sudo service tb-edge stop +{:copy-code} +``` + +```bash +sudo rpm -Uvh tb-edge-${TB_EDGE_TAG}.rpm +{:copy-code} +``` + +${UPGRADE_DB} + +Start the service +```bash +sudo service tb-edge start +{:copy-code} +``` diff --git a/application/src/main/data/json/edge/instructions/upgrade/docker/instructions.md b/application/src/main/data/json/edge/instructions/upgrade/docker/instructions.md new file mode 100644 index 0000000000..ad7f4b2c46 --- /dev/null +++ b/application/src/main/data/json/edge/instructions/upgrade/docker/instructions.md @@ -0,0 +1,15 @@ +#### Upgrading to ${TB_EDGE_VERSION} + +**NOTE**:These steps are applicable for ThingsBoard Edge ${CURRENT_TB_EDGE_VERSION} version. +Execute the following command to pull **${TB_EDGE_VERSION}** image: + +```bash +docker pull thingsboard/tb-edge:${TB_EDGE_VERSION} +{:copy-code} +``` + +${STOP_SERVICE} + +${UPGRADE_DB} + +Make sure your image is the set to tb-edge-${TB_EDGE_VERSION}. diff --git a/application/src/main/data/json/edge/instructions/upgrade/docker/start_service.md b/application/src/main/data/json/edge/instructions/upgrade/docker/start_service.md new file mode 100644 index 0000000000..cfdd9a66f2 --- /dev/null +++ b/application/src/main/data/json/edge/instructions/upgrade/docker/start_service.md @@ -0,0 +1,7 @@ +Execute the following commands to up this docker compose directly: + +```bash +docker compose up -d +docker compose logs -f mytbedge +{:copy-code} +``` diff --git a/application/src/main/data/json/edge/instructions/upgrade/docker/stop_service.md b/application/src/main/data/json/edge/instructions/upgrade/docker/stop_service.md new file mode 100644 index 0000000000..8f4aedb117 --- /dev/null +++ b/application/src/main/data/json/edge/instructions/upgrade/docker/stop_service.md @@ -0,0 +1,11 @@ +Set the terminal in the directory which contains the `docker-compose.yml` file and execute the following command to stop +and remove currently running TB Edge container (if it’s still running): + +```bash +make docker-compose.yml + +```bash +docker compose stop +docker compose rm mytbedge +{:copy-code} +``` diff --git a/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md new file mode 100644 index 0000000000..ae6181bf08 --- /dev/null +++ b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md @@ -0,0 +1,53 @@ +Create docker compose file for ThingsBoard Edge upgrade process: + +```bash +nano docker-compose-upgrade.yml +{:copy-code} +``` + +Add the following lines to the yml file: + +```bash +version: '3.0' +services: + mytbedge: + restart: on-failure + image: "thingsboard/tb-edge:${TB_EDGE_VERSION}" + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/tb-edge + volumes: + - ~/.mytb-edge-data:/data + - ~/.mytb-edge-logs:/var/log/tb-edge + entrypoint: upgrade-tb-edge.sh + 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} +``` + +Execute the following command to start upgrade process: + +```bash +docker compose -f docker-compose-upgrade.yml up +{:copy-code} +``` + +Once upgrade process successfully completed, exit from the docker-compose shell by this combination: + +```text +Ctrl + C +``` + +Execute the following command to stop TB Edge upgrade container: + +```bash +docker compose -f docker-compose-upgrade.yml stop +{:copy-code} +``` diff --git a/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md new file mode 100644 index 0000000000..c82daaee26 --- /dev/null +++ b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md @@ -0,0 +1,26 @@ +Here is the list of commands, that can be used to quickly upgrade ThingsBoard Edge on Docker (Linux or MacOS). + +#### Prepare for upgrading ThingsBoard Edge +Set the terminal in the directory which contains the `docker-compose.yml` file and execute the following command +to stop and remove currently running TB Edge container: + +```bash +docker compose stop +docker compose rm mytbedge +{:copy-code} +``` + +If you still rely on Docker Compose as docker-compose (with a hyphen) here is the list of the above commands: +docker-compose stop +docker-compose rm mytbedge + +#### Backup Database +Make a copy of the database folder before upgrading: + +```bash +sudo cp -r ~/.mytb-edge-data/db ~/.mytb-edge-db-BACKUP +{:copy-code} +``` + +```bash +make docker-compose.yml diff --git a/application/src/main/data/json/edge/instructions/upgrade/start_service.md b/application/src/main/data/json/edge/instructions/upgrade/start_service.md new file mode 100644 index 0000000000..0ce6a0b222 --- /dev/null +++ b/application/src/main/data/json/edge/instructions/upgrade/start_service.md @@ -0,0 +1,6 @@ +Start the service + +```bash +sudo service tb-edge start +{:copy-code} +``` diff --git a/application/src/main/data/json/edge/instructions/upgrade/stop_service.md b/application/src/main/data/json/edge/instructions/upgrade/stop_service.md new file mode 100644 index 0000000000..615d0f8a40 --- /dev/null +++ b/application/src/main/data/json/edge/instructions/upgrade/stop_service.md @@ -0,0 +1,6 @@ +Stop ThingsBoard Edge service if it is running: + +```bash +sudo service tb-edge stop +{:copy-code} +``` diff --git a/application/src/main/data/json/edge/instructions/upgrade/ubuntu/instructions.md b/application/src/main/data/json/edge/instructions/upgrade/ubuntu/instructions.md new file mode 100644 index 0000000000..5824875f47 --- /dev/null +++ b/application/src/main/data/json/edge/instructions/upgrade/ubuntu/instructions.md @@ -0,0 +1,19 @@ +#### Upgrading to ${TB_EDGE_VERSION} +**NOTE**:These steps are applicable for ThingsBoard Edge ${CURRENT_TB_EDGE_VERSION} version. + +**ThingsBoard Edge package download** +```bash +wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_TAG}/tb-edge-${TB_EDGE_TAG}.deb +{:copy-code} +``` + +#### ThingsBoard Edge service upgrade + +${STOP_SERVICE} + +```bash +sudo dpkg -i tb-edge-${TB_EDGE_TAG}.deb +{:copy-code} +``` + +${UPGRADE_DB} diff --git a/application/src/main/data/json/edge/instructions/upgrade/upgrade_db.md b/application/src/main/data/json/edge/instructions/upgrade/upgrade_db.md new file mode 100644 index 0000000000..d3a28fb20b --- /dev/null +++ b/application/src/main/data/json/edge/instructions/upgrade/upgrade_db.md @@ -0,0 +1,8 @@ +**NOTE**: Package installer may ask you to merge your tb-edge configuration. It is preferred to use **merge option** to make sure that all your previous parameters will not be overwritten. + +Execute regular upgrade script: + +```bash +sudo /usr/share/tb-edge/bin/install/upgrade.sh --fromVersion=${CURRENT_TB_EDGE_VERSION} +{:copy-code} +``` diff --git a/application/src/main/data/json/edge/instructions/upgrade/upgrade_preparing.md b/application/src/main/data/json/edge/instructions/upgrade/upgrade_preparing.md new file mode 100644 index 0000000000..105a94b778 --- /dev/null +++ b/application/src/main/data/json/edge/instructions/upgrade/upgrade_preparing.md @@ -0,0 +1,37 @@ +Here is the list of commands, that can be used to quickly upgrade ThingsBoard Edge on RHEL/CentOS 7/8. + +#### Prepare for upgrading ThingsBoard Edge + +Stop ThingsBoard Edge service: + +```bash +sudo systemctl stop tb-edge +{:copy-code} +``` + +#### Backup Database +Make a backup of the database before upgrading. +**Make sure you have enough space to place a backup of the database.** + +Check database size: + +```bash +sudo -u postgres psql -c "SELECT pg_size_pretty( pg_database_size('tb_edge') );" +{:copy-code} +``` + +Check free space: + +```bash +df -h / +{:copy-code} +``` + +If there is enough free space - make a backup: + +```bash +sudo -Hiu postgres pg_dump tb_edge > tb_edge.sql.bak +{:copy-code} +``` + +Check backup file created successfully. 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 fa16fbff81..c621dc777f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java @@ -39,7 +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.EdgeInstructions; import org.thingsboard.server.common.data.edge.EdgeSearchQuery; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -60,6 +60,7 @@ import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeBulkImportService; import org.thingsboard.server.service.edge.instructions.EdgeInstallService; +import org.thingsboard.server.service.edge.instructions.EdgeUpgradeService; import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.entitiy.edge.TbEdgeService; import org.thingsboard.server.service.security.model.SecurityUser; @@ -102,6 +103,7 @@ public class EdgeController extends BaseController { private final TbEdgeService tbEdgeService; private final Optional edgeRpcServiceOpt; private final Optional edgeInstallServiceOpt; + private final Optional edgeUpgradeServiceOpt; public static final String EDGE_ID = "edgeId"; public static final String EDGE_SECURITY_CHECK = "If the user has the authority of 'Tenant Administrator', the server checks that the edge is owned by the same tenant. " + @@ -553,16 +555,16 @@ public class EdgeController extends BaseController { return edgeBulkImportService.processBulkImport(request, user); } - @ApiOperation(value = "Get Edge Docker Install Instructions (getEdgeDockerInstallInstructions)", + @ApiOperation(value = "Get Edge Install Instructions (getEdgeInstallInstructions)", 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}", method = RequestMethod.GET) + @RequestMapping(value = "/edge/instructions/install/{edgeId}/{method}", method = RequestMethod.GET) @ResponseBody - public EdgeInstallInstructions getEdgeDockerInstallInstructions( + public EdgeInstructions getEdgeInstallInstructions( @ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) @PathVariable("edgeId") String strEdgeId, - @ApiParam(value = "Installation method ('docker', 'ubuntu' or 'centos')") + @ApiParam(value = "Installation method ('docker', 'ubuntu' or 'centos')", allowableValues = "docker,ubuntu,centos") @PathVariable("method") String installationMethod, HttpServletRequest request) throws ThingsboardException { if (isEdgesEnabled() && edgeInstallServiceOpt.isPresent()) { @@ -574,4 +576,27 @@ public class EdgeController extends BaseController { throw new ThingsboardException("Edges support disabled", ThingsboardErrorCode.GENERAL); } } + + @ApiOperation(value = "Get Edge Upgrade Instructions (getEdgeUpgradeInstructions)", + 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/upgrade/{edgeId}/{edgeVersion}/{method}", method = RequestMethod.GET) + @ResponseBody + public EdgeInstructions getEdgeUpgradeInstructions( + @ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) + @PathVariable("edgeId") String strEdgeId, + @ApiParam(value = "Edge version", required = true) + @PathVariable("edgeVersion") String edgeVersion, + @ApiParam(value = "Installation method ('docker', 'ubuntu' or 'centos')", allowableValues = "docker,ubuntu,centos") + @PathVariable("method") String method) throws Exception { + if (isEdgesEnabled() && edgeUpgradeServiceOpt.isPresent()) { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + edgeId = checkNotNull(edgeId); + Edge edge = checkEdgeId(edgeId, Operation.READ); + return checkNotNull(edgeUpgradeServiceOpt.get().getUpgradeInstructions(getTenantId(), edge, edgeVersion, method)); + } else { + throw new ThingsboardException("Edges support disabled", ThingsboardErrorCode.GENERAL); + } + } } 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 index dd17f83317..24340f7a97 100644 --- 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 @@ -21,7 +21,7 @@ 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.edge.EdgeInstructions; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.install.InstallScripts; @@ -40,8 +40,8 @@ import java.nio.file.Paths; public class DefaultEdgeInstallService implements EdgeInstallService { private static final String EDGE_DIR = "edge"; - - private static final String EDGE_INSTALL_INSTRUCTIONS_DIR = "install_instructions"; + private static final String INSTRUCTIONS_DIR = "instructions"; + private static final String INSTALL_DIR = "install"; private final InstallScripts installScripts; @@ -55,7 +55,7 @@ public class DefaultEdgeInstallService implements EdgeInstallService { private String appVersion; @Override - public EdgeInstallInstructions getInstallInstructions(TenantId tenantId, Edge edge, String installationMethod, HttpServletRequest request) { + public EdgeInstructions getInstallInstructions(TenantId tenantId, Edge edge, String installationMethod, HttpServletRequest request) { switch (installationMethod.toLowerCase()) { case "docker": return getDockerInstallInstructions(edge, request); @@ -68,7 +68,7 @@ public class DefaultEdgeInstallService implements EdgeInstallService { } } - private EdgeInstallInstructions getDockerInstallInstructions(Edge edge, HttpServletRequest request) { + private EdgeInstructions getDockerInstallInstructions(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")) { @@ -83,26 +83,26 @@ public class DefaultEdgeInstallService implements EdgeInstallService { edgeVersion = edgeVersion.replace("-SNAPSHOT", ""); dockerInstallInstructions = dockerInstallInstructions.replace("${TB_EDGE_VERSION}", edgeVersion); dockerInstallInstructions = replacePlaceholders(dockerInstallInstructions, edge); - return new EdgeInstallInstructions(dockerInstallInstructions); + return new EdgeInstructions(dockerInstallInstructions); } - private EdgeInstallInstructions getUbuntuInstallInstructions(Edge edge, HttpServletRequest request) { + private EdgeInstructions getUbuntuInstallInstructions(Edge edge, HttpServletRequest request) { String ubuntuInstallInstructions = readFile(resolveFile("ubuntu", "instructions.md")); ubuntuInstallInstructions = replacePlaceholders(ubuntuInstallInstructions, edge); ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${BASE_URL}", request.getServerName()); String edgeVersion = appVersion.replace("-SNAPSHOT", ""); ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${TB_EDGE_VERSION}", edgeVersion); - return new EdgeInstallInstructions(ubuntuInstallInstructions); + return new EdgeInstructions(ubuntuInstallInstructions); } - private EdgeInstallInstructions getCentosInstallInstructions(Edge edge, HttpServletRequest request) { + private EdgeInstructions getCentosInstallInstructions(Edge edge, HttpServletRequest request) { String centosInstallInstructions = readFile(resolveFile("centos", "instructions.md")); centosInstallInstructions = replacePlaceholders(centosInstallInstructions, edge); centosInstallInstructions = centosInstallInstructions.replace("${BASE_URL}", request.getServerName()); String edgeVersion = appVersion.replace("-SNAPSHOT", ""); centosInstallInstructions = centosInstallInstructions.replace("${TB_EDGE_VERSION}", edgeVersion); - return new EdgeInstallInstructions(centosInstallInstructions); + return new EdgeInstructions(centosInstallInstructions); } private String replacePlaceholders(String instructions, Edge edge) { @@ -127,6 +127,6 @@ public class DefaultEdgeInstallService implements EdgeInstallService { } private Path getEdgeInstallInstructionsDir() { - return Paths.get(installScripts.getDataDir(), InstallScripts.JSON_DIR, EDGE_DIR, EDGE_INSTALL_INSTRUCTIONS_DIR); + return Paths.get(installScripts.getDataDir(), InstallScripts.JSON_DIR, EDGE_DIR, INSTRUCTIONS_DIR, INSTALL_DIR); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeService.java new file mode 100644 index 0000000000..a1d737757d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeService.java @@ -0,0 +1,190 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.AllArgsConstructor; +import lombok.Data; +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.DataConstants; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeInstructions; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.install.InstallScripts; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; + +@Service +@Slf4j +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") +@TbCoreComponent +public class DefaultEdgeUpgradeService implements EdgeUpgradeService { + + private static final HashMap upgradeVersionHashMap; + + static { + upgradeVersionHashMap = new HashMap<>(); + upgradeVersionHashMap.put("3.6.0", new UpgradeInfo(true, "3.6.1")); + upgradeVersionHashMap.put("3.6.1", new UpgradeInfo(true, "3.6.2")); + upgradeVersionHashMap.put("3.6.2", new UpgradeInfo(true, null)); + } + + private static final String EDGE_DIR = "edge"; + private static final String INSTRUCTIONS_DIR = "instructions"; + private static final String UPGRADE_DIR = "upgrade"; + + private final InstallScripts installScripts; + private final AttributesService attributesService; + + @Value("${app.version:unknown}") + private String appVersion; + + @Override + public EdgeInstructions getUpgradeInstructions(TenantId tenantId, Edge edge, String edgeVersion, String upgradeMethod) { + String tbVersion = appVersion.replace("-SNAPSHOT", ""); + String currentEdgeVersion = convertEdgeVersionToDocsFormat(edgeVersion); + switch (upgradeMethod.toLowerCase()) { + case "docker": + return getDockerUpgradeInstructions(tenantId, edge.getId(), tbVersion, currentEdgeVersion); + case "ubuntu": + case "centos": + return getLinuxUpgradeInstructions(tenantId, edge.getId(), tbVersion, currentEdgeVersion, upgradeMethod.toLowerCase()); + default: + throw new IllegalArgumentException("Unsupported upgrade method for Edge: " + upgradeMethod); + } + } + + private EdgeInstructions getDockerUpgradeInstructions(TenantId tenantId, EdgeId edgeId, String tbVersion, String currentEdgeVersion) { + UpgradeInfo upgradeInfo = upgradeVersionHashMap.get(currentEdgeVersion); + if (upgradeInfo.getNextVersion() == null || tbVersion.equals(currentEdgeVersion)) { + return null; + } + boolean stoppedService = false; + StringBuilder result = new StringBuilder(readFile(resolveFile("docker", "upgrade_preparing.md"))); + while (upgradeInfo.getNextVersion() != null || !tbVersion.equals(currentEdgeVersion)) { + String edgeVersion = upgradeInfo.getNextVersion(); + String ubuntuUpgradeInstructions = readFile(resolveFile("docker", "instructions.md")); + if (upgradeInfo.isUpgradeDb()) { + String upgradeDb = readFile(resolveFile("docker", "upgrade_db.md")); + ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${UPGRADE_DB}", upgradeDb); + } + if (!stoppedService) { + stoppedService = true; + String stopService = readFile(resolveFile("docker", "stop_service.md")); + ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${STOP_SERVICE}", stopService); + } else { + ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${STOP_SERVICE}", ""); + } + ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${TB_EDGE_VERSION}", edgeVersion + "EDGE"); + ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${CURRENT_TB_EDGE_VERSION}", currentEdgeVersion + "EDGE"); + currentEdgeVersion = edgeVersion; + upgradeInfo = upgradeVersionHashMap.get(upgradeInfo.getNextVersion()); + result.append(ubuntuUpgradeInstructions); + } + String startService = readFile(resolveFile("docker", "start_service.md")); + result.append(startService); + AttributeKvEntry attributeKvEntry = new BaseAttributeKvEntry(new StringDataEntry("edgeVersion", currentEdgeVersion), System.currentTimeMillis()); + attributesService.save(tenantId, edgeId, DataConstants.SERVER_SCOPE, attributeKvEntry); + return new EdgeInstructions(result.toString()); + } + + private EdgeInstructions getLinuxUpgradeInstructions(TenantId tenantId, EdgeId edgeId, String tbVersion, String currentEdgeVersion, String os) { + UpgradeInfo upgradeInfo = upgradeVersionHashMap.get(currentEdgeVersion); + if (upgradeInfo.getNextVersion() == null || tbVersion.equals(currentEdgeVersion)) { + return null; + } + boolean stoppedService = false; + StringBuilder result = new StringBuilder(readFile(resolveFile("upgrade_preparing.md"))); + while (upgradeInfo.getNextVersion() != null || !tbVersion.equals(currentEdgeVersion)) { + String edgeVersion = upgradeInfo.getNextVersion(); + String ubuntuUpgradeInstructions = readFile(resolveFile(os, "instructions.md")); + if (upgradeInfo.isUpgradeDb()) { + String upgradeDb = readFile(resolveFile("upgrade_db.md")); + ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${UPGRADE_DB}", upgradeDb); + } + if (!stoppedService) { + stoppedService = true; + String stopService = readFile(resolveFile("stop_service.md")); + ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${STOP_SERVICE}", stopService); + } else { + ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${STOP_SERVICE}", ""); + } + ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${TB_EDGE_TAG}", getTagVersion(edgeVersion)); + ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${CURRENT_TB_EDGE_TAG}", getTagVersion(currentEdgeVersion)); + ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${TB_EDGE_VERSION}", edgeVersion); + ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${CURRENT_TB_EDGE_VERSION}", currentEdgeVersion); + ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${TB_EDGE_VERSION_TITLE}", edgeVersion + "EDGE"); + currentEdgeVersion = edgeVersion; + upgradeInfo = upgradeVersionHashMap.get(upgradeInfo.getNextVersion()); + result.append(ubuntuUpgradeInstructions); + } + String startService = readFile(resolveFile("start_service.md")); + result.append(startService); + AttributeKvEntry attributeKvEntry = new BaseAttributeKvEntry(new StringDataEntry("edgeVersion", convertDocsFormatToEdgeVersion(currentEdgeVersion)), System.currentTimeMillis()); + attributesService.save(tenantId, edgeId, DataConstants.SERVER_SCOPE, attributeKvEntry); + return new EdgeInstructions(result.toString()); + } + + private String getTagVersion(String version) { + return version.endsWith(".0") ? version.substring(0, version.length() - 2) : version; + } + + private String convertEdgeVersionToDocsFormat(String edgeVersion) { + return edgeVersion.replace("_", ".").substring(2); + } + + private String convertDocsFormatToEdgeVersion(String edgeVersion) { + return "V_" + edgeVersion.replace(".", "_"); + } + + private String readFile(Path file) { + try { + return Files.readString(file); + } 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, INSTRUCTIONS_DIR, UPGRADE_DIR); + } + + @AllArgsConstructor + @Data + public static class UpgradeInfo { + private boolean upgradeDb; + private String nextVersion; + } +} 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 index 20cac25e33..cba8703920 100644 --- 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 @@ -16,12 +16,12 @@ 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.edge.EdgeInstructions; import org.thingsboard.server.common.data.id.TenantId; import javax.servlet.http.HttpServletRequest; public interface EdgeInstallService { - EdgeInstallInstructions getInstallInstructions(TenantId tenantId, Edge edge, String installationMethod, HttpServletRequest request); + EdgeInstructions getInstallInstructions(TenantId tenantId, Edge edge, String installationMethod, HttpServletRequest request); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeService.java new file mode 100644 index 0000000000..0f4394ace9 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeService.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.EdgeInstructions; +import org.thingsboard.server.common.data.id.TenantId; + +public interface EdgeUpgradeService { + + EdgeInstructions getUpgradeInstructions(TenantId tenantId, Edge edge, String edgeVersion, String upgradeMethod) throws Exception; +} 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 7318b1eb24..32d648c045 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 @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; @@ -774,7 +775,7 @@ public final class EdgeGrpcSession implements Closeable { try { if (edge.getSecret().equals(request.getEdgeSecret())) { sessionOpenListener.accept(edge.getId(), this); - this.edgeVersion = request.getEdgeVersion(); + this.edgeVersion = processGetAndSaveEdgeVersion(request.getEdgeVersion()); return ConnectResponseMsg.newBuilder() .setResponseCode(ConnectResponseCode.ACCEPTED) .setErrorMsg("") @@ -800,6 +801,12 @@ public final class EdgeGrpcSession implements Closeable { .setConfiguration(EdgeConfiguration.getDefaultInstance()).build(); } + private EdgeVersion processGetAndSaveEdgeVersion(EdgeVersion edgeVersion) { + AttributeKvEntry attributeKvEntry = new BaseAttributeKvEntry(new StringDataEntry("edgeVersion", edgeVersion.name()), System.currentTimeMillis()); + ctx.getAttributesService().save(this.tenantId, this.edge.getId(), DataConstants.SERVER_SCOPE, attributeKvEntry); + return edgeVersion; + } + @Override public void close() { log.debug("[{}][{}] Closing session", this.tenantId, sessionId); 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/EdgeInstructions.java similarity index 90% rename from common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeInstallInstructions.java rename to common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeInstructions.java index 8343058250..2c3a66c002 100644 --- 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/EdgeInstructions.java @@ -25,8 +25,8 @@ import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor -public class EdgeInstallInstructions { +public class EdgeInstructions { - @ApiModelProperty(position = 1, value = "Markdown with install instructions") - private String installInstructions; + @ApiModelProperty(position = 1, value = "Markdown with install/upgrade instructions") + private String instructions; } 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 fb878a33b6..d90a58fde8 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 @@ -84,7 +84,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.EdgeInstructions; import org.thingsboard.server.common.data.edge.EdgeSearchQuery; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.id.AlarmCommentId; @@ -3241,9 +3241,9 @@ public class RestClient implements Closeable { }).getBody(); } - public Optional getEdgeDockerInstallInstructions(EdgeId edgeId) { - ResponseEntity edgeInstallInstructionsResult = - restTemplate.getForEntity(baseURL + "/api/edge/instructions/{edgeId}", EdgeInstallInstructions.class, edgeId.getId()); + public Optional getEdgeDockerInstallInstructions(EdgeId edgeId, String method) { + ResponseEntity edgeInstallInstructionsResult = + restTemplate.getForEntity(baseURL + "/api/edge/instructions/install/{edgeId}/{method}", EdgeInstructions.class, edgeId.getId(), method); return Optional.ofNullable(edgeInstallInstructionsResult.getBody()); } From 652501faf3dd0f10d97ae29829aa2ae8db70528a Mon Sep 17 00:00:00 2001 From: dudnikmaksim Date: Wed, 22 Nov 2023 11:37:04 +0200 Subject: [PATCH 03/38] PROD-2636, PROD-2634, PROD-2633 --- .../data/json/demo/dashboards/gateway.json | 314 ++++++++++++++++-- .../gateway-service-rpc.component.html | 8 +- .../gateway-service-rpc.component.scss | 22 ++ .../gateway/gateway-service-rpc.component.ts | 11 +- .../assets/locale/locale.constant-en_US.json | 2 +- 5 files changed, 329 insertions(+), 28 deletions(-) diff --git a/application/src/main/data/json/demo/dashboards/gateway.json b/application/src/main/data/json/demo/dashboards/gateway.json index b35b480ef2..fc5c651a33 100644 --- a/application/src/main/data/json/demo/dashboards/gateway.json +++ b/application/src/main/data/json/demo/dashboards/gateway.json @@ -250,9 +250,9 @@ "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", "type": "customPretty", - "customHtml": "
\r\n \r\n

Add gateway

\r\n \r\n \r\n
\r\n \r\n \r\n
\r\n
\r\n
\r\n \r\n Name\r\n \r\n \r\n Gateway name is required.\r\n \r\n \r\n
\r\n
\r\n \r\n
\r\n
\r\n
\r\n \r\n \r\n
\r\n
\r\n", + "customHtml": "
\r\n \r\n

Add gateway

\r\n \r\n \r\n
\r\n \r\n \r\n
\r\n
\r\n
\r\n \r\n Name\r\n \r\n \r\n Gateway name is required.\r\n \r\n \r\n
\r\n
\r\n \r\n
\r\n
\r\n
\r\n \r\n \r\n
\r\n
\r\n", "customCss": ".add-entity-form {\r\n min-width: 400px !important;\r\n}\r\n\r\n.add-entity-form .boolean-value-input {\r\n padding-left: 5px;\r\n}\r\n\r\n.add-entity-form .boolean-value-input .checkbox-label {\r\n margin-bottom: 8px;\r\n color: rgba(0,0,0,0.54);\r\n font-size: 12px;\r\n}\r\n\r\n.relations-list .header {\r\n padding-right: 5px;\r\n padding-bottom: 5px;\r\n padding-left: 5px;\r\n}\r\n\r\n.relations-list .header .cell {\r\n padding-right: 5px;\r\n padding-left: 5px;\r\n font-size: 12px;\r\n font-weight: 700;\r\n color: rgba(0, 0, 0, .54);\r\n white-space: nowrap;\r\n}\r\n\r\n.relations-list .mat-form-field-infix {\r\n width: auto !important;\r\n}\r\n\r\n.relations-list .body {\r\n padding-right: 5px;\r\n padding-bottom: 15px;\r\n padding-left: 5px;\r\n}\r\n\r\n.relations-list .body .row {\r\n padding-top: 5px;\r\n}\r\n\r\n.relations-list .body .cell {\r\n padding-right: 5px;\r\n padding-left: 5px;\r\n}\r\n\r\n.relations-list .body .md-button {\r\n margin: 0;\r\n}\r\n\r\n", - "customFunction": "let $injector = widgetContext.$scope.$injector;\r\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\r\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\r\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\r\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\r\nlet entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService'));\r\nlet userSettingsService = $injector.get(widgetContext.servicesMap.get('userSettingsService'));\r\n\r\nopenAddEntityDialog();\r\n\r\nfunction openAddEntityDialog() {\r\n customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();\r\n}\r\n\r\nfunction AddEntityDialogController(instance) {\r\n let vm = instance;\r\n let userSettings;\r\n userSettingsService.loadUserSettings().subscribe(settings=> {\r\n userSettings = settings;\r\n if (!userSettings.createdGatewaysCount) userSettings.createdGatewaysCount = 0;\r\n });\r\n \r\n\r\n vm.addEntityFormGroup = vm.fb.group({\r\n entityName: ['', [vm.validators.required]],\r\n entityType: ['DEVICE'],\r\n entityLabel: [''],\r\n type: ['', [vm.validators.required]],\r\n });\r\n\r\n vm.cancel = function() {\r\n vm.dialogRef.close(null);\r\n };\r\n\r\n\r\n vm.save = function() {\r\n vm.addEntityFormGroup.markAsPristine();\r\n saveEntityObservable().subscribe(\r\n function (device) {\r\n widgetContext.updateAliases();\r\n userSettingsService.putUserSettings({ createdGatewaysCount: ++userSettings.createdGatewaysCount }).subscribe(_=>{\r\n });\r\n vm.dialogRef.close(null);\r\n goToConfigState(device);\r\n }\r\n );\r\n };\r\n \r\n function goToConfigState(device) {\r\n const stateParams = {};\r\n stateParams.entityId = device.id;\r\n stateParams.entityName = device.name;\r\n const newStateParams = {\r\n targetEntityParamName: 'default',\r\n new_gateway: {\r\n entityId: device.id,\r\n entityName: device.name\r\n }\r\n }\r\n const params = {...stateParams, ...newStateParams};\r\n widgetContext.stateController.openState('gateway_details', params, false);\r\n }\r\n\r\n function saveEntityObservable() {\r\n const formValues = vm.addEntityFormGroup.value;\r\n let entity = {\r\n name: formValues.entityName,\r\n type: formValues.type,\r\n label: formValues.entityLabel,\r\n additionalInfo: {\r\n gateway: true\r\n }\r\n };\r\n return deviceService.saveDevice(entity);\r\n }\r\n}\r\n", + "customFunction": "let $injector = widgetContext.$scope.$injector;\r\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\r\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\r\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\r\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\r\nlet entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService'));\r\nlet userSettingsService = $injector.get(widgetContext.servicesMap.get('userSettingsService'));\r\n\r\nopenAddEntityDialog();\r\n\r\nfunction openAddEntityDialog() {\r\n customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();\r\n}\r\n\r\nfunction AddEntityDialogController(instance) {\r\n let vm = instance;\r\n let userSettings;\r\n userSettingsService.loadUserSettings().subscribe(settings=> {\r\n userSettings = settings;\r\n if (!userSettings.createdGatewaysCount) userSettings.createdGatewaysCount = 0;\r\n });\r\n \r\n\r\n vm.addEntityFormGroup = vm.fb.group({\r\n entityName: ['', [vm.validators.required]],\r\n entityType: ['DEVICE'],\r\n entityLabel: [''],\r\n type: ['', [vm.validators.required]],\r\n });\r\n\r\n vm.cancel = function() {\r\n vm.dialogRef.close(null);\r\n };\r\n\r\n\r\n vm.save = function($event) {\r\n vm.addEntityFormGroup.markAsPristine();\r\n saveEntityObservable().subscribe(\r\n function (device) {\r\n widgetContext.updateAliases();\r\n userSettingsService.putUserSettings({ createdGatewaysCount: ++userSettings.createdGatewaysCount }).subscribe(_=>{\r\n });\r\n vm.dialogRef.close(null);\r\n openCommandDialog(device, $event);\r\n }\r\n );\r\n };\r\n \r\n function openCommandDialog(device, $event) {\r\n vm.device = device;\r\n let openCommandAction = widgetContext.actionsApi.getActionDescriptors(\"actionCellButton\").find(action => action.name == \"Docker commands\");\r\n widgetContext.actionsApi.handleWidgetAction($event, openCommandAction, device.id, device.name, {newDevice: true});\r\n setTimeout(function() {\r\n document.querySelector(\".dashboard-state-dialog .mat-mdc-button-touch-target\").addEventListener('click', goToConfigState);\r\n document.querySelector(\".dashboard-state-dialog .mat-mdc-button.mat-primary\").addEventListener('click', goToConfigState);\r\n }, 500);\r\n }\r\n\r\n \r\n function goToConfigState() {\r\n document.querySelector(\".dashboard-state-dialog .mat-mdc-button-touch-target\").removeEventListener('click', goToConfigState);\r\n document.querySelector(\".dashboard-state-dialog .mat-mdc-button.mat-primary\").removeEventListener('click', goToConfigState);\r\n const stateParams = {};\r\n stateParams.entityId = vm.device.id;\r\n stateParams.entityName = vm.device.name;\r\n const newStateParams = {\r\n targetEntityParamName: 'default',\r\n new_gateway: {\r\n entityId: vm.device.id,\r\n entityName: vm.device.name\r\n }\r\n }\r\n const params = {...stateParams, ...newStateParams};\r\n widgetContext.stateController.openState('gateway_details', params, false);\r\n }\r\n\r\n function saveEntityObservable() {\r\n const formValues = vm.addEntityFormGroup.value;\r\n let entity = {\r\n name: formValues.entityName,\r\n type: formValues.type,\r\n label: formValues.entityLabel,\r\n additionalInfo: {\r\n gateway: true\r\n }\r\n };\r\n return deviceService.saveDevice(entity);\r\n }\r\n}\r\n", "customResources": [], "openInSeparateDialog": false, "openInPopover": false, @@ -2142,7 +2142,7 @@ "settings": { "useMarkdownTextFunction": false, "markdownTextPattern": "
\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n", - "applyDefaultMarkdownStyle": true, + "applyDefaultMarkdownStyle": false, "markdownCss": ".mat-mdc-form-field-subscript-wrapper {\n display: none !important;\n}" }, "title": "New Markdown/HTML Card", @@ -2316,7 +2316,22 @@ "pageSize": 1024, "noDataDisplayMessage": "", "enableDataExport": false, - "borderRadius": "" + "borderRadius": "", + "actions": { + "actionCellButton": [ + { + "name": "Show Device Info", + "icon": "info", + "useShowWidgetActionFunction": null, + "showWidgetActionFunction": "return true;", + "type": "custom", + "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "openInSeparateDialog": false, + "openInPopover": false, + "id": "45e4507d-3adc-bb31-8b2b-1ba09bbd56ac" + } + ] + } }, "row": 0, "col": 0, @@ -2468,7 +2483,22 @@ "pageSize": 1024, "noDataDisplayMessage": "", "enableDataExport": false, - "borderRadius": "4px" + "borderRadius": "4px", + "actions": { + "actionCellButton": [ + { + "name": "Show Device Info", + "icon": "info", + "useShowWidgetActionFunction": null, + "showWidgetActionFunction": "return true;", + "type": "custom", + "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "openInSeparateDialog": false, + "openInPopover": false, + "id": "852eccce-98eb-24db-c783-bdd62566f906" + } + ] + } }, "row": 0, "col": 0, @@ -2619,7 +2649,22 @@ "widgetCss": ".status {\r\n border-radius: 20px;\r\n font-weight: 500;\r\n padding: 5px 15px;\r\n }\r\n\r\n .status-active {\r\n color: green;\r\n background: rgba(0, 128, 0, 0.1);\r\n }\r\n\r\n .status-inactive {\r\n color: red;\r\n background: rgba(255, 0, 0, 0.1);\r\n }\r\n", "pageSize": 1024, "noDataDisplayMessage": "", - "enableDataExport": false + "enableDataExport": false, + "actions": { + "actionCellButton": [ + { + "name": "Show Device Info", + "icon": "info", + "useShowWidgetActionFunction": null, + "showWidgetActionFunction": "return true;", + "type": "custom", + "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "openInSeparateDialog": false, + "openInPopover": false, + "id": "3c31ba62-e760-2bea-4c8d-d32784a86c24" + } + ] + } }, "row": 0, "col": 0, @@ -2770,7 +2815,22 @@ "widgetCss": ".status {\r\n border-radius: 20px;\r\n font-weight: 500;\r\n padding: 5px 15px;\r\n }\r\n\r\n .status-active {\r\n color: green;\r\n background: rgba(0, 128, 0, 0.1);\r\n }\r\n\r\n .status-inactive {\r\n color: red;\r\n background: rgba(255, 0, 0, 0.1);\r\n }\r\n", "pageSize": 1024, "noDataDisplayMessage": "", - "enableDataExport": false + "enableDataExport": false, + "actions": { + "actionCellButton": [ + { + "name": "Show Device Info", + "icon": "info", + "useShowWidgetActionFunction": null, + "showWidgetActionFunction": "return true;", + "type": "custom", + "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "openInSeparateDialog": false, + "openInPopover": false, + "id": "4b55ea81-93bf-4206-9166-3e0bdc1dd9f3" + } + ] + } }, "row": 0, "col": 0, @@ -2921,7 +2981,22 @@ "widgetCss": ".status {\r\n border-radius: 20px;\r\n font-weight: 500;\r\n padding: 5px 15px;\r\n }\r\n\r\n .status-active {\r\n color: green;\r\n background: rgba(0, 128, 0, 0.1);\r\n }\r\n\r\n .status-inactive {\r\n color: red;\r\n background: rgba(255, 0, 0, 0.1);\r\n }\r\n", "pageSize": 1024, "noDataDisplayMessage": "", - "enableDataExport": false + "enableDataExport": false, + "actions": { + "actionCellButton": [ + { + "name": "Show Device Info", + "icon": "info", + "useShowWidgetActionFunction": null, + "showWidgetActionFunction": "return true;", + "type": "custom", + "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "openInSeparateDialog": false, + "openInPopover": false, + "id": "babf88d0-a118-e2b5-f10e-3a5970c8a65b" + } + ] + } }, "row": 0, "col": 0, @@ -3072,7 +3147,22 @@ "widgetCss": ".status {\r\n border-radius: 20px;\r\n font-weight: 500;\r\n padding: 5px 15px;\r\n }\r\n\r\n .status-active {\r\n color: green;\r\n background: rgba(0, 128, 0, 0.1);\r\n }\r\n\r\n .status-inactive {\r\n color: red;\r\n background: rgba(255, 0, 0, 0.1);\r\n }\r\n", "pageSize": 1024, "noDataDisplayMessage": "", - "enableDataExport": false + "enableDataExport": false, + "actions": { + "actionCellButton": [ + { + "name": "Show Device Info", + "icon": "info", + "useShowWidgetActionFunction": null, + "showWidgetActionFunction": "return true;", + "type": "custom", + "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "openInSeparateDialog": false, + "openInPopover": false, + "id": "94de7690-f91d-b032-6771-85af99abd749" + } + ] + } }, "row": 0, "col": 0, @@ -3223,7 +3313,22 @@ "widgetCss": ".status {\r\n border-radius: 20px;\r\n font-weight: 500;\r\n padding: 5px 15px;\r\n }\r\n\r\n .status-active {\r\n color: green;\r\n background: rgba(0, 128, 0, 0.1);\r\n }\r\n\r\n .status-inactive {\r\n color: red;\r\n background: rgba(255, 0, 0, 0.1);\r\n }\r\n", "pageSize": 1024, "noDataDisplayMessage": "", - "enableDataExport": false + "enableDataExport": false, + "actions": { + "actionCellButton": [ + { + "name": "Show Device Info", + "icon": "info", + "useShowWidgetActionFunction": null, + "showWidgetActionFunction": "return true;", + "type": "custom", + "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "openInSeparateDialog": false, + "openInPopover": false, + "id": "18414f44-1c65-536a-14de-eaf21a7d56bd" + } + ] + } }, "row": 0, "col": 0, @@ -3374,7 +3479,22 @@ "widgetCss": ".status {\r\n border-radius: 20px;\r\n font-weight: 500;\r\n padding: 5px 15px;\r\n }\r\n\r\n .status-active {\r\n color: green;\r\n background: rgba(0, 128, 0, 0.1);\r\n }\r\n\r\n .status-inactive {\r\n color: red;\r\n background: rgba(255, 0, 0, 0.1);\r\n }\r\n", "pageSize": 1024, "noDataDisplayMessage": "", - "enableDataExport": false + "enableDataExport": false, + "actions": { + "actionCellButton": [ + { + "name": "Show Device Info", + "icon": "info", + "useShowWidgetActionFunction": null, + "showWidgetActionFunction": "return true;", + "type": "custom", + "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "openInSeparateDialog": false, + "openInPopover": false, + "id": "794974da-c9d2-a9f7-be47-c9eb642094e8" + } + ] + } }, "row": 0, "col": 0, @@ -3525,7 +3645,22 @@ "widgetCss": ".status {\r\n border-radius: 20px;\r\n font-weight: 500;\r\n padding: 5px 15px;\r\n }\r\n\r\n .status-active {\r\n color: green;\r\n background: rgba(0, 128, 0, 0.1);\r\n }\r\n\r\n .status-inactive {\r\n color: red;\r\n background: rgba(255, 0, 0, 0.1);\r\n }\r\n", "pageSize": 1024, "noDataDisplayMessage": "", - "enableDataExport": false + "enableDataExport": false, + "actions": { + "actionCellButton": [ + { + "name": "Show Device Info", + "icon": "info", + "useShowWidgetActionFunction": null, + "showWidgetActionFunction": "return true;", + "type": "custom", + "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "openInSeparateDialog": false, + "openInPopover": false, + "id": "2add705b-3e53-8559-8126-380cac686fb0" + } + ] + } }, "row": 0, "col": 0, @@ -3676,7 +3811,22 @@ "widgetCss": ".status {\r\n border-radius: 20px;\r\n font-weight: 500;\r\n padding: 5px 15px;\r\n }\r\n\r\n .status-active {\r\n color: green;\r\n background: rgba(0, 128, 0, 0.1);\r\n }\r\n\r\n .status-inactive {\r\n color: red;\r\n background: rgba(255, 0, 0, 0.1);\r\n }\r\n", "pageSize": 1024, "noDataDisplayMessage": "", - "enableDataExport": false + "enableDataExport": false, + "actions": { + "actionCellButton": [ + { + "name": "Show Device Info", + "icon": "info", + "useShowWidgetActionFunction": null, + "showWidgetActionFunction": "return true;", + "type": "custom", + "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "openInSeparateDialog": false, + "openInPopover": false, + "id": "7e1ba820-9992-d52a-579b-20485abb3926" + } + ] + } }, "row": 0, "col": 0, @@ -3827,7 +3977,22 @@ "widgetCss": ".status {\r\n border-radius: 20px;\r\n font-weight: 500;\r\n padding: 5px 15px;\r\n }\r\n\r\n .status-active {\r\n color: green;\r\n background: rgba(0, 128, 0, 0.1);\r\n }\r\n\r\n .status-inactive {\r\n color: red;\r\n background: rgba(255, 0, 0, 0.1);\r\n }\r\n", "pageSize": 1024, "noDataDisplayMessage": "", - "enableDataExport": false + "enableDataExport": false, + "actions": { + "actionCellButton": [ + { + "name": "Show Device Info", + "icon": "info", + "useShowWidgetActionFunction": null, + "showWidgetActionFunction": "return true;", + "type": "custom", + "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "openInSeparateDialog": false, + "openInPopover": false, + "id": "91af27c1-b37c-2276-6022-a332e41b2b33" + } + ] + } }, "row": 0, "col": 0, @@ -3978,7 +4143,22 @@ "widgetCss": ".status {\r\n border-radius: 20px;\r\n font-weight: 500;\r\n padding: 5px 15px;\r\n }\r\n\r\n .status-active {\r\n color: green;\r\n background: rgba(0, 128, 0, 0.1);\r\n }\r\n\r\n .status-inactive {\r\n color: red;\r\n background: rgba(255, 0, 0, 0.1);\r\n }\r\n", "pageSize": 1024, "noDataDisplayMessage": "", - "enableDataExport": false + "enableDataExport": false, + "actions": { + "actionCellButton": [ + { + "name": "Show Device Info", + "icon": "info", + "useShowWidgetActionFunction": null, + "showWidgetActionFunction": "return true;", + "type": "custom", + "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "openInSeparateDialog": false, + "openInPopover": false, + "id": "26cf8696-054b-13ec-7984-6fc5df20e6f1" + } + ] + } }, "row": 0, "col": 0, @@ -4129,7 +4309,22 @@ "widgetCss": ".status {\r\n border-radius: 20px;\r\n font-weight: 500;\r\n padding: 5px 15px;\r\n }\r\n\r\n .status-active {\r\n color: green;\r\n background: rgba(0, 128, 0, 0.1);\r\n }\r\n\r\n .status-inactive {\r\n color: red;\r\n background: rgba(255, 0, 0, 0.1);\r\n }\r\n", "pageSize": 1024, "noDataDisplayMessage": "", - "enableDataExport": false + "enableDataExport": false, + "actions": { + "actionCellButton": [ + { + "name": "Show Device Info", + "icon": "info", + "useShowWidgetActionFunction": null, + "showWidgetActionFunction": "return true;", + "type": "custom", + "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "openInSeparateDialog": false, + "openInPopover": false, + "id": "1dcfaf24-32be-cd19-62d6-86d12cc6a7ef" + } + ] + } }, "row": 0, "col": 0, @@ -4280,7 +4475,22 @@ "widgetCss": ".status {\r\n border-radius: 20px;\r\n font-weight: 500;\r\n padding: 5px 15px;\r\n }\r\n\r\n .status-active {\r\n color: green;\r\n background: rgba(0, 128, 0, 0.1);\r\n }\r\n\r\n .status-inactive {\r\n color: red;\r\n background: rgba(255, 0, 0, 0.1);\r\n }\r\n", "pageSize": 1024, "noDataDisplayMessage": "", - "enableDataExport": false + "enableDataExport": false, + "actions": { + "actionCellButton": [ + { + "name": "Show Device Info", + "icon": "info", + "useShowWidgetActionFunction": null, + "showWidgetActionFunction": "return true;", + "type": "custom", + "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "openInSeparateDialog": false, + "openInPopover": false, + "id": "ad2bc817-f3c4-150c-4672-8fe0c38aee8d" + } + ] + } }, "row": 0, "col": 0, @@ -4431,7 +4641,22 @@ "widgetCss": ".status {\r\n border-radius: 20px;\r\n font-weight: 500;\r\n padding: 5px 15px;\r\n }\r\n\r\n .status-active {\r\n color: green;\r\n background: rgba(0, 128, 0, 0.1);\r\n }\r\n\r\n .status-inactive {\r\n color: red;\r\n background: rgba(255, 0, 0, 0.1);\r\n }\r\n", "pageSize": 1024, "noDataDisplayMessage": "", - "enableDataExport": false + "enableDataExport": false, + "actions": { + "actionCellButton": [ + { + "name": "Show Device Info", + "icon": "info", + "useShowWidgetActionFunction": null, + "showWidgetActionFunction": "return true;", + "type": "custom", + "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "openInSeparateDialog": false, + "openInPopover": false, + "id": "d1ad84cd-bd9c-4dca-e4a0-f444ae8598bd" + } + ] + } }, "row": 0, "col": 0, @@ -4582,7 +4807,22 @@ "widgetCss": ".status {\r\n border-radius: 20px;\r\n font-weight: 500;\r\n padding: 5px 15px;\r\n }\r\n\r\n .status-active {\r\n color: green;\r\n background: rgba(0, 128, 0, 0.1);\r\n }\r\n\r\n .status-inactive {\r\n color: red;\r\n background: rgba(255, 0, 0, 0.1);\r\n }\r\n", "pageSize": 1024, "noDataDisplayMessage": "", - "enableDataExport": false + "enableDataExport": false, + "actions": { + "actionCellButton": [ + { + "name": "Show Device Info", + "icon": "info", + "useShowWidgetActionFunction": null, + "showWidgetActionFunction": "return true;", + "type": "custom", + "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "openInSeparateDialog": false, + "openInPopover": false, + "id": "bf80eef9-b879-9a08-40a4-488dbdefa125" + } + ] + } }, "row": 0, "col": 0, @@ -4733,7 +4973,22 @@ "widgetCss": ".status {\r\n border-radius: 20px;\r\n font-weight: 500;\r\n padding: 5px 15px;\r\n }\r\n\r\n .status-active {\r\n color: green;\r\n background: rgba(0, 128, 0, 0.1);\r\n }\r\n\r\n .status-inactive {\r\n color: red;\r\n background: rgba(255, 0, 0, 0.1);\r\n }\r\n", "pageSize": 1024, "noDataDisplayMessage": "", - "enableDataExport": false + "enableDataExport": false, + "actions": { + "actionCellButton": [ + { + "name": "Show Device Info", + "icon": "info", + "useShowWidgetActionFunction": null, + "showWidgetActionFunction": "return true;", + "type": "custom", + "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "openInSeparateDialog": false, + "openInPopover": false, + "id": "b5a406b3-cc0a-8a09-9aec-3f8befae5fb8" + } + ] + } }, "row": 0, "col": 0, @@ -4884,7 +5139,22 @@ "widgetCss": ".status {\r\n border-radius: 20px;\r\n font-weight: 500;\r\n padding: 5px 15px;\r\n }\r\n\r\n .status-active {\r\n color: green;\r\n background: rgba(0, 128, 0, 0.1);\r\n }\r\n\r\n .status-inactive {\r\n color: red;\r\n background: rgba(255, 0, 0, 0.1);\r\n }\r\n", "pageSize": 1024, "noDataDisplayMessage": "", - "enableDataExport": false + "enableDataExport": false, + "actions": { + "actionCellButton": [ + { + "name": "Show Device Info", + "icon": "info", + "useShowWidgetActionFunction": null, + "showWidgetActionFunction": "return true;", + "type": "custom", + "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "openInSeparateDialog": false, + "openInPopover": false, + "id": "ec1dfba3-4b43-2491-8948-f602337f8a3b" + } + ] + } }, "row": 0, "col": 0, @@ -6596,4 +6866,4 @@ }, "externalId": null, "name": "Gateway" -} +} \ No newline at end of file diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html index 4aec0fd467..8a96f1849b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html @@ -36,7 +36,7 @@ {{ 'gateway.statistics.command' | translate }} - + {{ 'widget-config.datasource-parameters' | translate }} @@ -56,8 +56,10 @@
- {{ 'gateway.rpc-command-result' | translate }} - + {{ 'gateway.rpc-command-result' | translate }} +
schedule + {{ resultTime | date: 'yyyy/MM/dd HH:mm:ss' }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss index 345688d451..5dab1d91c1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss @@ -24,6 +24,7 @@ .command-form { flex-wrap: nowrap; padding: 0 5px 5px; + & > button { margin-top: 10px; } @@ -34,9 +35,30 @@ display: flex; flex-direction: column; flex: 1; + & > span { font-weight: 600; + position: relative; + font-size: 14px; + margin-bottom: 10px; + + + .result-time { + font-weight: 400; + font-size: 14px; + line-height: 32px; + position: absolute; + left: 0; + top: 25px; + z-index: 5; + color: rgba(0, 0, 0, 0.54); + + span { + padding-left: 10px; + } + } } + tb-json-content { flex: 1; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts index a8ad61e1ec..d19624e257 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts @@ -37,6 +37,8 @@ export class GatewayServiceRPCComponent implements AfterViewInit { contentTypes = ContentType; + resultTime: number | null; + @Input() dialogRef: MatDialogRef; @@ -76,12 +78,17 @@ export class GatewayServiceRPCComponent implements AfterViewInit { } sendCommand() { + this.resultTime = null; const formValues = this.commandForm.value; const commandPrefix = this.isConnector ? `${this.connectorType}_` : 'gateway_'; this.ctx.controlApi.sendTwoWayCommand(commandPrefix+formValues.command.toLowerCase(), formValues.params,formValues.time).subscribe({ - next: resp => this.commandForm.get('result').setValue(JSON.stringify(resp)), + next: resp => { + this.resultTime = new Date().getTime(); + this.commandForm.get('result').setValue(JSON.stringify(resp)) + }, error: error => { - console.log(error); + this.resultTime = new Date().getTime(); + console.error(error); this.commandForm.get('result').setValue(JSON.stringify(error.error)); } }); 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 ef660731bf..fcf42dc0e2 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2682,7 +2682,7 @@ "connectors-table-key": "Key", "connectors-table-class": "Class", "rpc-command-send": "Send", - "rpc-command-result": "Result", + "rpc-command-result": "Response", "rpc-command-edit-params": "Edit parameters", "gateway-configuration": "General Configuration", "docker-label": "In order to run ThingsBoard IoT gateway in docker with credentials for this device you can use the following commands.", From 855b9c054b2680973491ed1e616d416e9faf79e7 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 22 Nov 2023 14:01:10 +0200 Subject: [PATCH 04/38] Remove edge from upgradeEdgeController. We modify edgeVersion only on edgeConnect --- .../server/controller/EdgeController.java | 9 ++------ .../DefaultEdgeUpgradeService.java | 22 +++++-------------- .../edge/instructions/EdgeUpgradeService.java | 3 +-- 3 files changed, 8 insertions(+), 26 deletions(-) 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 c621dc777f..26a7938dfb 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java @@ -581,20 +581,15 @@ public class EdgeController extends BaseController { 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/upgrade/{edgeId}/{edgeVersion}/{method}", method = RequestMethod.GET) + @RequestMapping(value = "/edge/instructions/upgrade/{edgeVersion}/{method}", method = RequestMethod.GET) @ResponseBody public EdgeInstructions getEdgeUpgradeInstructions( - @ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) - @PathVariable("edgeId") String strEdgeId, @ApiParam(value = "Edge version", required = true) @PathVariable("edgeVersion") String edgeVersion, @ApiParam(value = "Installation method ('docker', 'ubuntu' or 'centos')", allowableValues = "docker,ubuntu,centos") @PathVariable("method") String method) throws Exception { if (isEdgesEnabled() && edgeUpgradeServiceOpt.isPresent()) { - EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); - edgeId = checkNotNull(edgeId); - Edge edge = checkEdgeId(edgeId, Operation.READ); - return checkNotNull(edgeUpgradeServiceOpt.get().getUpgradeInstructions(getTenantId(), edge, edgeVersion, method)); + return checkNotNull(edgeUpgradeServiceOpt.get().getUpgradeInstructions(getTenantId(), edgeVersion, method)); } else { throw new ThingsboardException("Edges support disabled", ThingsboardErrorCode.GENERAL); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeService.java index a1d737757d..dbd08faef8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeService.java @@ -22,15 +22,8 @@ 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.DataConstants; -import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeInstructions; -import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; -import org.thingsboard.server.common.data.kv.StringDataEntry; -import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.install.InstallScripts; @@ -61,27 +54,26 @@ public class DefaultEdgeUpgradeService implements EdgeUpgradeService { private static final String UPGRADE_DIR = "upgrade"; private final InstallScripts installScripts; - private final AttributesService attributesService; @Value("${app.version:unknown}") private String appVersion; @Override - public EdgeInstructions getUpgradeInstructions(TenantId tenantId, Edge edge, String edgeVersion, String upgradeMethod) { + public EdgeInstructions getUpgradeInstructions(TenantId tenantId, String edgeVersion, String upgradeMethod) { String tbVersion = appVersion.replace("-SNAPSHOT", ""); String currentEdgeVersion = convertEdgeVersionToDocsFormat(edgeVersion); switch (upgradeMethod.toLowerCase()) { case "docker": - return getDockerUpgradeInstructions(tenantId, edge.getId(), tbVersion, currentEdgeVersion); + return getDockerUpgradeInstructions(tenantId, tbVersion, currentEdgeVersion); case "ubuntu": case "centos": - return getLinuxUpgradeInstructions(tenantId, edge.getId(), tbVersion, currentEdgeVersion, upgradeMethod.toLowerCase()); + return getLinuxUpgradeInstructions(tenantId, tbVersion, currentEdgeVersion, upgradeMethod.toLowerCase()); default: throw new IllegalArgumentException("Unsupported upgrade method for Edge: " + upgradeMethod); } } - private EdgeInstructions getDockerUpgradeInstructions(TenantId tenantId, EdgeId edgeId, String tbVersion, String currentEdgeVersion) { + private EdgeInstructions getDockerUpgradeInstructions(TenantId tenantId, String tbVersion, String currentEdgeVersion) { UpgradeInfo upgradeInfo = upgradeVersionHashMap.get(currentEdgeVersion); if (upgradeInfo.getNextVersion() == null || tbVersion.equals(currentEdgeVersion)) { return null; @@ -110,12 +102,10 @@ public class DefaultEdgeUpgradeService implements EdgeUpgradeService { } String startService = readFile(resolveFile("docker", "start_service.md")); result.append(startService); - AttributeKvEntry attributeKvEntry = new BaseAttributeKvEntry(new StringDataEntry("edgeVersion", currentEdgeVersion), System.currentTimeMillis()); - attributesService.save(tenantId, edgeId, DataConstants.SERVER_SCOPE, attributeKvEntry); return new EdgeInstructions(result.toString()); } - private EdgeInstructions getLinuxUpgradeInstructions(TenantId tenantId, EdgeId edgeId, String tbVersion, String currentEdgeVersion, String os) { + private EdgeInstructions getLinuxUpgradeInstructions(TenantId tenantId, String tbVersion, String currentEdgeVersion, String os) { UpgradeInfo upgradeInfo = upgradeVersionHashMap.get(currentEdgeVersion); if (upgradeInfo.getNextVersion() == null || tbVersion.equals(currentEdgeVersion)) { return null; @@ -147,8 +137,6 @@ public class DefaultEdgeUpgradeService implements EdgeUpgradeService { } String startService = readFile(resolveFile("start_service.md")); result.append(startService); - AttributeKvEntry attributeKvEntry = new BaseAttributeKvEntry(new StringDataEntry("edgeVersion", convertDocsFormatToEdgeVersion(currentEdgeVersion)), System.currentTimeMillis()); - attributesService.save(tenantId, edgeId, DataConstants.SERVER_SCOPE, attributeKvEntry); return new EdgeInstructions(result.toString()); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeService.java index 0f4394ace9..878e38a468 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeService.java @@ -15,11 +15,10 @@ */ package org.thingsboard.server.service.edge.instructions; -import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeInstructions; import org.thingsboard.server.common.data.id.TenantId; public interface EdgeUpgradeService { - EdgeInstructions getUpgradeInstructions(TenantId tenantId, Edge edge, String edgeVersion, String upgradeMethod) throws Exception; + EdgeInstructions getUpgradeInstructions(TenantId tenantId, String edgeVersion, String upgradeMethod) throws Exception; } From 7fad29e05971c0ba8233aac3be2fefbcabbd42ea Mon Sep 17 00:00:00 2001 From: deaflynx Date: Wed, 22 Nov 2023 16:59:21 +0200 Subject: [PATCH 05/38] edge upgrade instructions ui implementation --- common/edge-api/src/main/proto/edge.proto | 1 + ui-ngx/src/app/core/http/edge.service.ts | 10 ++++-- .../edge-instructions-dialog.component.ts | 34 +++++++++++++++---- .../home/pages/edge/edge.component.html | 27 +++++++++++---- .../modules/home/pages/edge/edge.component.ts | 29 +++++++++++++++- .../pages/edge/edges-table-config.resolver.ts | 10 ++++-- ui-ngx/src/app/shared/models/edge.models.ts | 6 ++-- .../assets/locale/locale.constant-en_US.json | 1 + 8 files changed, 95 insertions(+), 23 deletions(-) diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 71e84304ed..854ce29935 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -36,6 +36,7 @@ enum EdgeVersion { V_3_4_0 = 2; V_3_6_0 = 3; V_3_6_1 = 4; + V_3_6_2 = 5; } /** diff --git a/ui-ngx/src/app/core/http/edge.service.ts b/ui-ngx/src/app/core/http/edge.service.ts index aeb4f7d16b..6eb1e2f029 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, EdgeInstallInstructions, EdgeSearchQuery } from '@shared/models/edge.models'; +import { Edge, EdgeEvent, EdgeInfo, EdgeInstructions, 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'; @@ -114,7 +114,11 @@ export class EdgeService { return this.http.post('/api/edge/bulk_import', entitiesData, defaultHttpOptionsFromConfig(config)); } - public getEdgeInstallInstructions(edgeId: string, method: string = 'ubuntu', config?: RequestConfig): Observable { - return this.http.get(`/api/edge/instructions/${edgeId}/${method}`, defaultHttpOptionsFromConfig(config)); + public getEdgeInstallInstructions(edgeId: string, method: string = 'ubuntu', config?: RequestConfig): Observable { + return this.http.get(`/api/edge/instructions/install/${edgeId}/${method}`, defaultHttpOptionsFromConfig(config)); + } + + public getEdgeUpgradeInstructions(edgeVersion: string, method: string = 'ubuntu', config?: RequestConfig): Observable { + return this.http.get(`/api/edge/instructions/upgrade/${edgeVersion}/${method}`, defaultHttpOptionsFromConfig(config)); } } 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 index 99da1dfb14..c618df96bb 100644 --- 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 @@ -21,12 +21,21 @@ import { AppState } from '@core/core.state'; import { Router } from '@angular/router'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { ActionPreferencesPutUserSettings } from '@core/auth/auth.actions'; -import { EdgeInfo, EdgeInstructionsMethod } from '@shared/models/edge.models'; +import { + EdgeInfo, + EdgeInstructions, + EdgeInstructionsMethod, + edgeVersionAttributeKey +} from '@shared/models/edge.models'; import { EdgeService } from '@core/http/edge.service'; +import { AttributeService } from '@core/http/attribute.service'; +import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; +import { mergeMap, Observable } from 'rxjs'; export interface EdgeInstructionsDialogData { edge: EdgeInfo; afterAdd: boolean; + upgradeAvailable: boolean; } @Component({ @@ -49,6 +58,7 @@ export class EdgeInstructionsDialogComponent extends DialogComponent, + private attributeService: AttributeService, private edgeService: EdgeService) { super(store, router, dialogRef); @@ -85,12 +95,22 @@ export class EdgeInstructionsDialogComponent extends DialogComponent { - this.contentData[method] = res.installInstructions; - this.loadedInstructions = true; - } - ); + let edgeInstructions$: Observable; + if (this.data.upgradeAvailable) { + edgeInstructions$ = this.attributeService.getEntityAttributes(this.data.edge.id, AttributeScope.SERVER_SCOPE, [edgeVersionAttributeKey]) + .pipe(mergeMap(attributes => { + if (attributes.length) { + const edgeVersion = attributes[0].value; + return this.edgeService.getEdgeUpgradeInstructions(edgeVersion, method); + } + })); + } else { + edgeInstructions$ = this.edgeService.getEdgeInstallInstructions(this.data.edge.id.id, method); + } + edgeInstructions$.subscribe(res => { + this.contentData[method] = res.instructions; + this.loadedInstructions = true; + }); } } } 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 9c9a78f91a..3d0baad650 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 @@ -114,13 +114,26 @@
- +
+ + + + + + +
diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge.component.ts b/ui-ngx/src/app/modules/home/pages/edge/edge.component.ts index 78c8df6c08..b7cb2b1752 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge.component.ts +++ b/ui-ngx/src/app/modules/home/pages/edge/edge.component.ts @@ -20,12 +20,15 @@ import { AppState } from '@core/core.state'; import { EntityComponent } from '@home/components/entity/entity.component'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { EntityType } from '@shared/models/entity-type.models'; -import { EdgeInfo } from '@shared/models/edge.models'; +import { EdgeInfo, edgeVersionAttributeKey } from '@shared/models/edge.models'; import { TranslateService } from '@ngx-translate/core'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { generateSecret, guid } from '@core/utils'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { environment as env } from '@env/environment'; +import { AttributeService } from '@core/http/attribute.service'; +import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; @Component({ selector: 'tb-edge', @@ -37,9 +40,11 @@ export class EdgeComponent extends EntityComponent { entityType = EntityType; edgeScope: 'tenant' | 'customer' | 'customer_user'; + upgradeAvailable: boolean = false; constructor(protected store: Store, protected translate: TranslateService, + private attributeService: AttributeService, @Inject('entity') protected entityValue: EdgeInfo, @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, public fb: UntypedFormBuilder, @@ -95,6 +100,7 @@ export class EdgeComponent extends EntityComponent { } }); this.generateRoutingKeyAndSecret(entity, this.entityForm); + this.checkEdgeVersion(); } updateFormState() { @@ -133,4 +139,25 @@ export class EdgeComponent extends EntityComponent { form.get('secret').patchValue(generateSecret(20), {emitEvent: false}); } } + + checkEdgeVersion() { + this.attributeService.getEntityAttributes(this.entity.id, AttributeScope.SERVER_SCOPE, [edgeVersionAttributeKey]) + .subscribe(attributes => { + if (attributes?.length) { + const edgeVersion = attributes[0].value; + const tbVersion = 'V_' + env.tbVersion.replaceAll('.', '_'); + this.upgradeAvailable = this.versionUpgradeSupported(edgeVersion) && (edgeVersion !== tbVersion); + } else { + this.upgradeAvailable = false; + } + } + ); + } + + private versionUpgradeSupported(edgeVersion: string): boolean { + const edgeVersionArray = edgeVersion.split('_'); + const major = parseInt(edgeVersionArray[1]); + const minor = parseInt(edgeVersionArray[2]); + return major >= 3 && minor >= 6; + } } diff --git a/ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts index 6a80388b52..fa08c0daa0 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts @@ -558,7 +558,7 @@ export class EdgesTableConfigResolver implements Resolve { if (afterAdd) { @@ -610,9 +611,12 @@ export class EdgesTableConfigResolver implements Resolve { body: string; } -export interface EdgeInstallInstructions { - installInstructions: string; +export interface EdgeInstructions { + instructions: string; } export enum EdgeInstructionsMethod { @@ -187,3 +187,5 @@ export enum EdgeInstructionsMethod { centos, docker } + +export const edgeVersionAttributeKey = 'edgeVersion'; 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 37bc5d449f..4bd0361c36 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2017,6 +2017,7 @@ "sync-process-started-successfully": "Sync process started successfully!", "missing-related-rule-chains-title": "Edge has missing related rule chain(s)", "missing-related-rule-chains-text": "Assigned to edge rule chain(s) use rule nodes that forward message(s) to rule chain(s) that are not assigned to this edge.

List of missing rule chain(s):
{{missingRuleChains}}", + "upgrade-instructions": "Upgrade Instructions", "widget-datasource-error": "This widget supports only EDGE entity datasource" }, "edge-event": { From 6a1e5f315f966f6c6de5de36616af8b1384fc5a8 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 22 Nov 2023 17:01:33 +0200 Subject: [PATCH 06/38] Remove unused parameters in service --- .../server/controller/EdgeController.java | 4 ++-- .../DefaultEdgeInstallService.java | 3 +-- .../DefaultEdgeUpgradeService.java | 19 +++++++------------ .../edge/instructions/EdgeInstallService.java | 3 +-- .../edge/instructions/EdgeUpgradeService.java | 3 +-- 5 files changed, 12 insertions(+), 20 deletions(-) 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 26a7938dfb..f85529dbfe 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java @@ -571,7 +571,7 @@ public class EdgeController extends BaseController { EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); edgeId = checkNotNull(edgeId); Edge edge = checkEdgeId(edgeId, Operation.READ); - return checkNotNull(edgeInstallServiceOpt.get().getInstallInstructions(getTenantId(), edge, installationMethod, request)); + return checkNotNull(edgeInstallServiceOpt.get().getInstallInstructions(edge, installationMethod, request)); } else { throw new ThingsboardException("Edges support disabled", ThingsboardErrorCode.GENERAL); } @@ -589,7 +589,7 @@ public class EdgeController extends BaseController { @ApiParam(value = "Installation method ('docker', 'ubuntu' or 'centos')", allowableValues = "docker,ubuntu,centos") @PathVariable("method") String method) throws Exception { if (isEdgesEnabled() && edgeUpgradeServiceOpt.isPresent()) { - return checkNotNull(edgeUpgradeServiceOpt.get().getUpgradeInstructions(getTenantId(), edgeVersion, method)); + return checkNotNull(edgeUpgradeServiceOpt.get().getUpgradeInstructions(edgeVersion, method)); } else { throw new ThingsboardException("Edges support disabled", ThingsboardErrorCode.GENERAL); } 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 index 24340f7a97..8ebc5322c7 100644 --- 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 @@ -22,7 +22,6 @@ 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.EdgeInstructions; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.install.InstallScripts; @@ -55,7 +54,7 @@ public class DefaultEdgeInstallService implements EdgeInstallService { private String appVersion; @Override - public EdgeInstructions getInstallInstructions(TenantId tenantId, Edge edge, String installationMethod, HttpServletRequest request) { + public EdgeInstructions getInstallInstructions(Edge edge, String installationMethod, HttpServletRequest request) { switch (installationMethod.toLowerCase()) { case "docker": return getDockerInstallInstructions(edge, request); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeService.java index dbd08faef8..338c1fb8b7 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeService.java @@ -16,14 +16,13 @@ package org.thingsboard.server.service.edge.instructions; import lombok.AllArgsConstructor; -import lombok.Data; +import lombok.Getter; 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.EdgeInstructions; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.install.InstallScripts; @@ -59,21 +58,21 @@ public class DefaultEdgeUpgradeService implements EdgeUpgradeService { private String appVersion; @Override - public EdgeInstructions getUpgradeInstructions(TenantId tenantId, String edgeVersion, String upgradeMethod) { + public EdgeInstructions getUpgradeInstructions(String edgeVersion, String upgradeMethod) { String tbVersion = appVersion.replace("-SNAPSHOT", ""); String currentEdgeVersion = convertEdgeVersionToDocsFormat(edgeVersion); switch (upgradeMethod.toLowerCase()) { case "docker": - return getDockerUpgradeInstructions(tenantId, tbVersion, currentEdgeVersion); + return getDockerUpgradeInstructions(tbVersion, currentEdgeVersion); case "ubuntu": case "centos": - return getLinuxUpgradeInstructions(tenantId, tbVersion, currentEdgeVersion, upgradeMethod.toLowerCase()); + return getLinuxUpgradeInstructions(tbVersion, currentEdgeVersion, upgradeMethod.toLowerCase()); default: throw new IllegalArgumentException("Unsupported upgrade method for Edge: " + upgradeMethod); } } - private EdgeInstructions getDockerUpgradeInstructions(TenantId tenantId, String tbVersion, String currentEdgeVersion) { + private EdgeInstructions getDockerUpgradeInstructions(String tbVersion, String currentEdgeVersion) { UpgradeInfo upgradeInfo = upgradeVersionHashMap.get(currentEdgeVersion); if (upgradeInfo.getNextVersion() == null || tbVersion.equals(currentEdgeVersion)) { return null; @@ -105,7 +104,7 @@ public class DefaultEdgeUpgradeService implements EdgeUpgradeService { return new EdgeInstructions(result.toString()); } - private EdgeInstructions getLinuxUpgradeInstructions(TenantId tenantId, String tbVersion, String currentEdgeVersion, String os) { + private EdgeInstructions getLinuxUpgradeInstructions(String tbVersion, String currentEdgeVersion, String os) { UpgradeInfo upgradeInfo = upgradeVersionHashMap.get(currentEdgeVersion); if (upgradeInfo.getNextVersion() == null || tbVersion.equals(currentEdgeVersion)) { return null; @@ -148,10 +147,6 @@ public class DefaultEdgeUpgradeService implements EdgeUpgradeService { return edgeVersion.replace("_", ".").substring(2); } - private String convertDocsFormatToEdgeVersion(String edgeVersion) { - return "V_" + edgeVersion.replace(".", "_"); - } - private String readFile(Path file) { try { return Files.readString(file); @@ -170,7 +165,7 @@ public class DefaultEdgeUpgradeService implements EdgeUpgradeService { } @AllArgsConstructor - @Data + @Getter public static class UpgradeInfo { private boolean upgradeDb; private String nextVersion; 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 index cba8703920..996e14f16e 100644 --- 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 @@ -17,11 +17,10 @@ package org.thingsboard.server.service.edge.instructions; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeInstructions; -import org.thingsboard.server.common.data.id.TenantId; import javax.servlet.http.HttpServletRequest; public interface EdgeInstallService { - EdgeInstructions getInstallInstructions(TenantId tenantId, Edge edge, String installationMethod, HttpServletRequest request); + EdgeInstructions getInstallInstructions(Edge edge, String installationMethod, HttpServletRequest request); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeService.java index 878e38a468..b7747655ef 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeService.java @@ -16,9 +16,8 @@ package org.thingsboard.server.service.edge.instructions; import org.thingsboard.server.common.data.edge.EdgeInstructions; -import org.thingsboard.server.common.data.id.TenantId; public interface EdgeUpgradeService { - EdgeInstructions getUpgradeInstructions(TenantId tenantId, String edgeVersion, String upgradeMethod) throws Exception; + EdgeInstructions getUpgradeInstructions(String edgeVersion, String upgradeMethod); } From 60fb14a708304430647038c9b262afc0d052baf8 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 23 Nov 2023 14:42:22 +0200 Subject: [PATCH 07/38] Improve logic for docker upgrade --- .../upgrade/centos/instructions.md | 23 +++--------- .../upgrade/docker/instructions.md | 5 --- .../upgrade/docker/start_service.md | 16 +++++++++ .../upgrade/docker/stop_service.md | 11 ------ .../instructions/upgrade/docker/upgrade_db.md | 2 ++ .../upgrade/docker/upgrade_preparing.md | 10 +++--- .../instructions/upgrade/docker/upgrade_rm.md | 5 +++ .../instructions/upgrade/start_service.md | 2 +- .../edge/instructions/upgrade/stop_service.md | 6 ---- .../upgrade/ubuntu/instructions.md | 12 +++---- .../instructions/upgrade/upgrade_preparing.md | 7 ++-- .../DefaultEdgeUpgradeService.java | 36 +++++++++---------- 12 files changed, 56 insertions(+), 79 deletions(-) delete mode 100644 application/src/main/data/json/edge/instructions/upgrade/docker/stop_service.md create mode 100644 application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_rm.md delete mode 100644 application/src/main/data/json/edge/instructions/upgrade/stop_service.md diff --git a/application/src/main/data/json/edge/instructions/upgrade/centos/instructions.md b/application/src/main/data/json/edge/instructions/upgrade/centos/instructions.md index d0b2a1895d..c0e721ace2 100644 --- a/application/src/main/data/json/edge/instructions/upgrade/centos/instructions.md +++ b/application/src/main/data/json/edge/instructions/upgrade/centos/instructions.md @@ -1,30 +1,15 @@ -#### Upgrading to ${TB_EDGE_VERSION_TITLE} -**NOTE**:These steps are applicable for ThingsBoard Edge ${CURRENT_TB_EDGE_VERSION} version. +#### Upgrading to ${TB_EDGE_VERSION}EDGE -**ThingsBoard Edge package download** +**ThingsBoard Edge package download:** ```bash wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_TAG}/tb-edge-${TB_EDGE_TAG}.rpm {:copy-code} ``` +##### ThingsBoard Edge service upgrade -#### ThingsBoard Edge service upgrade - -Stop ThingsBoard Edge service if it is running: - -```bash -sudo service tb-edge stop -{:copy-code} -``` - +Install package: ```bash sudo rpm -Uvh tb-edge-${TB_EDGE_TAG}.rpm {:copy-code} ``` - ${UPGRADE_DB} - -Start the service -```bash -sudo service tb-edge start -{:copy-code} -``` diff --git a/application/src/main/data/json/edge/instructions/upgrade/docker/instructions.md b/application/src/main/data/json/edge/instructions/upgrade/docker/instructions.md index ad7f4b2c46..f1b9931e8b 100644 --- a/application/src/main/data/json/edge/instructions/upgrade/docker/instructions.md +++ b/application/src/main/data/json/edge/instructions/upgrade/docker/instructions.md @@ -1,6 +1,5 @@ #### Upgrading to ${TB_EDGE_VERSION} -**NOTE**:These steps are applicable for ThingsBoard Edge ${CURRENT_TB_EDGE_VERSION} version. Execute the following command to pull **${TB_EDGE_VERSION}** image: ```bash @@ -8,8 +7,4 @@ docker pull thingsboard/tb-edge:${TB_EDGE_VERSION} {:copy-code} ``` -${STOP_SERVICE} - ${UPGRADE_DB} - -Make sure your image is the set to tb-edge-${TB_EDGE_VERSION}. diff --git a/application/src/main/data/json/edge/instructions/upgrade/docker/start_service.md b/application/src/main/data/json/edge/instructions/upgrade/docker/start_service.md index cfdd9a66f2..b9e5786211 100644 --- a/application/src/main/data/json/edge/instructions/upgrade/docker/start_service.md +++ b/application/src/main/data/json/edge/instructions/upgrade/docker/start_service.md @@ -1,3 +1,19 @@ +Modify ‘main’ docker compose (`docker-compose.yml`) file for ThingsBoard Edge and update version of the image: +```bash +nano docker-compose.yml +{:copy-code} +``` + +```text +version: '3.0' +services: +mytbedge: +restart: always +image: "thingsboard/tb-edge:${TB_EDGE_VERSION}" +... +``` + +Make sure your image is the set to tb-edge-${TB_EDGE_VERSION}. Execute the following commands to up this docker compose directly: ```bash diff --git a/application/src/main/data/json/edge/instructions/upgrade/docker/stop_service.md b/application/src/main/data/json/edge/instructions/upgrade/docker/stop_service.md deleted file mode 100644 index 8f4aedb117..0000000000 --- a/application/src/main/data/json/edge/instructions/upgrade/docker/stop_service.md +++ /dev/null @@ -1,11 +0,0 @@ -Set the terminal in the directory which contains the `docker-compose.yml` file and execute the following command to stop -and remove currently running TB Edge container (if it’s still running): - -```bash -make docker-compose.yml - -```bash -docker compose stop -docker compose rm mytbedge -{:copy-code} -``` diff --git a/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md index ae6181bf08..ac38031517 100644 --- a/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md +++ b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md @@ -1,3 +1,5 @@ +${CLEAR_DOCKER_UPGRADE} + Create docker compose file for ThingsBoard Edge upgrade process: ```bash diff --git a/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md index c82daaee26..04034dea5e 100644 --- a/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md +++ b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md @@ -10,17 +10,15 @@ docker compose rm mytbedge {:copy-code} ``` -If you still rely on Docker Compose as docker-compose (with a hyphen) here is the list of the above commands: +**OPTIONAL:** If you still rely on Docker Compose as docker-compose (with a hyphen) here is the list of the above commands: +```text docker-compose stop docker-compose rm mytbedge - -#### Backup Database +``` +##### Backup Database Make a copy of the database folder before upgrading: ```bash sudo cp -r ~/.mytb-edge-data/db ~/.mytb-edge-db-BACKUP {:copy-code} ``` - -```bash -make docker-compose.yml diff --git a/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_rm.md b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_rm.md new file mode 100644 index 0000000000..4957f9333f --- /dev/null +++ b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_rm.md @@ -0,0 +1,5 @@ +Delete docker compose file, if already exists: +```bash +rm docker-compose-upgrade.yml +{:copy-code} +``` diff --git a/application/src/main/data/json/edge/instructions/upgrade/start_service.md b/application/src/main/data/json/edge/instructions/upgrade/start_service.md index 0ce6a0b222..c26c183154 100644 --- a/application/src/main/data/json/edge/instructions/upgrade/start_service.md +++ b/application/src/main/data/json/edge/instructions/upgrade/start_service.md @@ -1,6 +1,6 @@ Start the service ```bash -sudo service tb-edge start +sudo systemctl tb-edge start {:copy-code} ``` diff --git a/application/src/main/data/json/edge/instructions/upgrade/stop_service.md b/application/src/main/data/json/edge/instructions/upgrade/stop_service.md deleted file mode 100644 index 615d0f8a40..0000000000 --- a/application/src/main/data/json/edge/instructions/upgrade/stop_service.md +++ /dev/null @@ -1,6 +0,0 @@ -Stop ThingsBoard Edge service if it is running: - -```bash -sudo service tb-edge stop -{:copy-code} -``` diff --git a/application/src/main/data/json/edge/instructions/upgrade/ubuntu/instructions.md b/application/src/main/data/json/edge/instructions/upgrade/ubuntu/instructions.md index 5824875f47..59d243c63c 100644 --- a/application/src/main/data/json/edge/instructions/upgrade/ubuntu/instructions.md +++ b/application/src/main/data/json/edge/instructions/upgrade/ubuntu/instructions.md @@ -1,19 +1,15 @@ -#### Upgrading to ${TB_EDGE_VERSION} -**NOTE**:These steps are applicable for ThingsBoard Edge ${CURRENT_TB_EDGE_VERSION} version. +#### Upgrading to ${TB_EDGE_VERSION}EDGE -**ThingsBoard Edge package download** +**ThingsBoard Edge package download:** ```bash wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_TAG}/tb-edge-${TB_EDGE_TAG}.deb {:copy-code} ``` +##### ThingsBoard Edge service upgrade -#### ThingsBoard Edge service upgrade - -${STOP_SERVICE} - +Install package: ```bash sudo dpkg -i tb-edge-${TB_EDGE_TAG}.deb {:copy-code} ``` - ${UPGRADE_DB} diff --git a/application/src/main/data/json/edge/instructions/upgrade/upgrade_preparing.md b/application/src/main/data/json/edge/instructions/upgrade/upgrade_preparing.md index 105a94b778..bdd6c4ea18 100644 --- a/application/src/main/data/json/edge/instructions/upgrade/upgrade_preparing.md +++ b/application/src/main/data/json/edge/instructions/upgrade/upgrade_preparing.md @@ -1,4 +1,4 @@ -Here is the list of commands, that can be used to quickly upgrade ThingsBoard Edge on RHEL/CentOS 7/8. +Here is the list of commands, that can be used to quickly upgrade ThingsBoard Edge on ${OS} #### Prepare for upgrading ThingsBoard Edge @@ -9,9 +9,8 @@ sudo systemctl stop tb-edge {:copy-code} ``` -#### Backup Database -Make a backup of the database before upgrading. -**Make sure you have enough space to place a backup of the database.** +##### Backup Database +Make a backup of the database before upgrading. **Make sure you have enough space to place a backup of the database.** Check database size: diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeService.java index 338c1fb8b7..8586ad5b4d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeService.java @@ -74,10 +74,10 @@ public class DefaultEdgeUpgradeService implements EdgeUpgradeService { private EdgeInstructions getDockerUpgradeInstructions(String tbVersion, String currentEdgeVersion) { UpgradeInfo upgradeInfo = upgradeVersionHashMap.get(currentEdgeVersion); - if (upgradeInfo.getNextVersion() == null || tbVersion.equals(currentEdgeVersion)) { - return null; + if (upgradeInfo == null || upgradeInfo.getNextVersion() == null || tbVersion.equals(currentEdgeVersion)) { + return new EdgeInstructions("Edge upgrade instruction for " + currentEdgeVersion + "EDGE is not available."); } - boolean stoppedService = false; + boolean rmUpgradeCompose = false; StringBuilder result = new StringBuilder(readFile(resolveFile("docker", "upgrade_preparing.md"))); while (upgradeInfo.getNextVersion() != null || !tbVersion.equals(currentEdgeVersion)) { String edgeVersion = upgradeInfo.getNextVersion(); @@ -85,13 +85,15 @@ public class DefaultEdgeUpgradeService implements EdgeUpgradeService { if (upgradeInfo.isUpgradeDb()) { String upgradeDb = readFile(resolveFile("docker", "upgrade_db.md")); ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${UPGRADE_DB}", upgradeDb); + } else { + ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${UPGRADE_DB}", ""); } - if (!stoppedService) { - stoppedService = true; - String stopService = readFile(resolveFile("docker", "stop_service.md")); - ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${STOP_SERVICE}", stopService); + if (!rmUpgradeCompose) { + rmUpgradeCompose = true; + ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${CLEAR_DOCKER_UPGRADE}", ""); } else { - ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${STOP_SERVICE}", ""); + String rmUpgrade = readFile(resolveFile("docker", "upgrade_rm.md")); + ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${CLEAR_DOCKER_UPGRADE}", rmUpgrade); } ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${TB_EDGE_VERSION}", edgeVersion + "EDGE"); ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${CURRENT_TB_EDGE_VERSION}", currentEdgeVersion + "EDGE"); @@ -100,36 +102,32 @@ public class DefaultEdgeUpgradeService implements EdgeUpgradeService { result.append(ubuntuUpgradeInstructions); } String startService = readFile(resolveFile("docker", "start_service.md")); + startService = startService.replace("${TB_EDGE_VERSION}", currentEdgeVersion + "EDGE"); result.append(startService); return new EdgeInstructions(result.toString()); } private EdgeInstructions getLinuxUpgradeInstructions(String tbVersion, String currentEdgeVersion, String os) { UpgradeInfo upgradeInfo = upgradeVersionHashMap.get(currentEdgeVersion); - if (upgradeInfo.getNextVersion() == null || tbVersion.equals(currentEdgeVersion)) { - return null; + if (upgradeInfo == null || upgradeInfo.getNextVersion() == null || tbVersion.equals(currentEdgeVersion)) { + return new EdgeInstructions("Edge upgrade instruction for " + currentEdgeVersion + "EDGE is not available."); } - boolean stoppedService = false; - StringBuilder result = new StringBuilder(readFile(resolveFile("upgrade_preparing.md"))); + String upgrade_preparing = readFile(resolveFile("upgrade_preparing.md")); + upgrade_preparing = upgrade_preparing.replace("${OS}", os.equals("centos") ? "RHEL/CentOS 7/8" : "Ubuntu"); + StringBuilder result = new StringBuilder(upgrade_preparing); while (upgradeInfo.getNextVersion() != null || !tbVersion.equals(currentEdgeVersion)) { String edgeVersion = upgradeInfo.getNextVersion(); String ubuntuUpgradeInstructions = readFile(resolveFile(os, "instructions.md")); if (upgradeInfo.isUpgradeDb()) { String upgradeDb = readFile(resolveFile("upgrade_db.md")); ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${UPGRADE_DB}", upgradeDb); - } - if (!stoppedService) { - stoppedService = true; - String stopService = readFile(resolveFile("stop_service.md")); - ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${STOP_SERVICE}", stopService); } else { - ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${STOP_SERVICE}", ""); + ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${UPGRADE_DB}", ""); } ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${TB_EDGE_TAG}", getTagVersion(edgeVersion)); ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${CURRENT_TB_EDGE_TAG}", getTagVersion(currentEdgeVersion)); ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${TB_EDGE_VERSION}", edgeVersion); ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${CURRENT_TB_EDGE_VERSION}", currentEdgeVersion); - ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${TB_EDGE_VERSION_TITLE}", edgeVersion + "EDGE"); currentEdgeVersion = edgeVersion; upgradeInfo = upgradeVersionHashMap.get(upgradeInfo.getNextVersion()); result.append(ubuntuUpgradeInstructions); From 7817816f8e075ea3a46dcb67f96f09c2524d0579 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 23 Nov 2023 14:55:53 +0200 Subject: [PATCH 08/38] Fix ApiOperation text to be correct --- .../org/thingsboard/server/controller/EdgeController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 f85529dbfe..a1291d9440 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java @@ -556,7 +556,7 @@ public class EdgeController extends BaseController { } @ApiOperation(value = "Get Edge Install Instructions (getEdgeInstallInstructions)", - notes = "Get a docker install instructions for provided edge id." + TENANT_AUTHORITY_PARAGRAPH, + notes = "Get an install instructions for provided edge id." + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/edge/instructions/install/{edgeId}/{method}", method = RequestMethod.GET) @@ -578,7 +578,7 @@ public class EdgeController extends BaseController { } @ApiOperation(value = "Get Edge Upgrade Instructions (getEdgeUpgradeInstructions)", - notes = "Get a docker install instructions for provided edge id." + TENANT_AUTHORITY_PARAGRAPH, + notes = "Get an upgrade instructions for provided edge id." + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/edge/instructions/upgrade/{edgeVersion}/{method}", method = RequestMethod.GET) From aa769b4f372ad99c935cc16022d784cf7c6043e9 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Fri, 24 Nov 2023 12:10:55 +0200 Subject: [PATCH 09/38] edge upgrade instructions minor fixes --- .../modules/home/pages/edge/edges-table-config.resolver.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts index fa08c0daa0..4429515705 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts @@ -558,7 +558,7 @@ export class EdgesTableConfigResolver implements Resolve Date: Fri, 24 Nov 2023 12:22:01 +0200 Subject: [PATCH 10/38] Fix edge install test --- .../org/thingsboard/server/controller/EdgeControllerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java index 379dafef94..c41d835251 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java @@ -1151,7 +1151,7 @@ public class EdgeControllerTest extends AbstractControllerTest { 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() + "/docker", String.class); + String installInstructions = doGet("/api/edge/instructions/install/" + savedEdge.getId().getId().toString() + "/docker", String.class); Assert.assertTrue(installInstructions.contains("l7q4zsjplzwhk16geqxy")); Assert.assertTrue(installInstructions.contains("7390c3a6-69b0-9910-d155-b90aca4b772e")); } From 109305f66a41a1f0ecefa055373e595ea9f5af43 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 27 Nov 2023 11:54:22 +0200 Subject: [PATCH 11/38] Refactor EdgeInstructions: renaming, improve logic, add test --- .../json/edge/instructions/upgrade/upgrade_db.md | 2 +- .../server/controller/EdgeController.java | 14 +++++++------- ... => DefaultEdgeInstallInstructionsService.java} | 2 +- .../instructions/DefaultEdgeUpgradeService.java | 8 ++++---- ...ce.java => EdgeInstallInstructionsService.java} | 2 +- ...ce.java => EdgeUpgradeInstructionsService.java} | 2 +- .../server/service/edge/rpc/EdgeGrpcSession.java | 8 ++++---- .../server/controller/EdgeControllerTest.java | 14 +++++++++++++- .../org/thingsboard/edge/rpc/EdgeGrpcClient.java | 2 +- .../org/thingsboard/rest/client/RestClient.java | 8 +++++++- 10 files changed, 40 insertions(+), 22 deletions(-) rename application/src/main/java/org/thingsboard/server/service/edge/instructions/{DefaultEdgeInstallService.java => DefaultEdgeInstallInstructionsService.java} (98%) rename application/src/main/java/org/thingsboard/server/service/edge/instructions/{EdgeInstallService.java => EdgeInstallInstructionsService.java} (95%) rename application/src/main/java/org/thingsboard/server/service/edge/instructions/{EdgeUpgradeService.java => EdgeUpgradeInstructionsService.java} (94%) diff --git a/application/src/main/data/json/edge/instructions/upgrade/upgrade_db.md b/application/src/main/data/json/edge/instructions/upgrade/upgrade_db.md index d3a28fb20b..6e7beb1425 100644 --- a/application/src/main/data/json/edge/instructions/upgrade/upgrade_db.md +++ b/application/src/main/data/json/edge/instructions/upgrade/upgrade_db.md @@ -3,6 +3,6 @@ Execute regular upgrade script: ```bash -sudo /usr/share/tb-edge/bin/install/upgrade.sh --fromVersion=${CURRENT_TB_EDGE_VERSION} +sudo /usr/share/tb-edge/bin/install/upgrade.sh --fromVersion=${FROM_TB_EDGE_VERSION} {:copy-code} ``` 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 a1291d9440..7556ab589b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java @@ -59,8 +59,8 @@ import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeBulkImportService; -import org.thingsboard.server.service.edge.instructions.EdgeInstallService; -import org.thingsboard.server.service.edge.instructions.EdgeUpgradeService; +import org.thingsboard.server.service.edge.instructions.EdgeInstallInstructionsService; +import org.thingsboard.server.service.edge.instructions.EdgeUpgradeInstructionsService; import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.entitiy.edge.TbEdgeService; import org.thingsboard.server.service.security.model.SecurityUser; @@ -102,8 +102,8 @@ public class EdgeController extends BaseController { private final EdgeBulkImportService edgeBulkImportService; private final TbEdgeService tbEdgeService; private final Optional edgeRpcServiceOpt; - private final Optional edgeInstallServiceOpt; - private final Optional edgeUpgradeServiceOpt; + private final Optional edgeInstallServiceOpt; + private final Optional edgeUpgradeServiceOpt; public static final String EDGE_ID = "edgeId"; public static final String EDGE_SECURITY_CHECK = "If the user has the authority of 'Tenant Administrator', the server checks that the edge is owned by the same tenant. " + @@ -564,7 +564,7 @@ public class EdgeController extends BaseController { public EdgeInstructions getEdgeInstallInstructions( @ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) @PathVariable("edgeId") String strEdgeId, - @ApiParam(value = "Installation method ('docker', 'ubuntu' or 'centos')", allowableValues = "docker,ubuntu,centos") + @ApiParam(value = "Installation method ('docker', 'ubuntu' or 'centos')", allowableValues = "docker, ubuntu, centos") @PathVariable("method") String installationMethod, HttpServletRequest request) throws ThingsboardException { if (isEdgesEnabled() && edgeInstallServiceOpt.isPresent()) { @@ -578,7 +578,7 @@ public class EdgeController extends BaseController { } @ApiOperation(value = "Get Edge Upgrade Instructions (getEdgeUpgradeInstructions)", - notes = "Get an upgrade instructions for provided edge id." + TENANT_AUTHORITY_PARAGRAPH, + notes = "Get an upgrade instructions for provided edge version." + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/edge/instructions/upgrade/{edgeVersion}/{method}", method = RequestMethod.GET) @@ -586,7 +586,7 @@ public class EdgeController extends BaseController { public EdgeInstructions getEdgeUpgradeInstructions( @ApiParam(value = "Edge version", required = true) @PathVariable("edgeVersion") String edgeVersion, - @ApiParam(value = "Installation method ('docker', 'ubuntu' or 'centos')", allowableValues = "docker,ubuntu,centos") + @ApiParam(value = "Upgrade method ('docker', 'ubuntu' or 'centos')", allowableValues = "docker, ubuntu, centos") @PathVariable("method") String method) throws Exception { if (isEdgesEnabled() && edgeUpgradeServiceOpt.isPresent()) { return checkNotNull(edgeUpgradeServiceOpt.get().getUpgradeInstructions(edgeVersion, method)); 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/DefaultEdgeInstallInstructionsService.java similarity index 98% rename from application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallService.java rename to application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java index 8ebc5322c7..9da2b2abe8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java @@ -36,7 +36,7 @@ import java.nio.file.Paths; @RequiredArgsConstructor @ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") @TbCoreComponent -public class DefaultEdgeInstallService implements EdgeInstallService { +public class DefaultEdgeInstallInstructionsService implements EdgeInstallInstructionsService { private static final String EDGE_DIR = "edge"; private static final String INSTRUCTIONS_DIR = "instructions"; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeService.java index 8586ad5b4d..7ad9e94977 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeService.java @@ -37,7 +37,7 @@ import java.util.HashMap; @RequiredArgsConstructor @ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") @TbCoreComponent -public class DefaultEdgeUpgradeService implements EdgeUpgradeService { +public class DefaultEdgeUpgradeService implements EdgeUpgradeInstructionsService { private static final HashMap upgradeVersionHashMap; @@ -96,7 +96,7 @@ public class DefaultEdgeUpgradeService implements EdgeUpgradeService { ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${CLEAR_DOCKER_UPGRADE}", rmUpgrade); } ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${TB_EDGE_VERSION}", edgeVersion + "EDGE"); - ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${CURRENT_TB_EDGE_VERSION}", currentEdgeVersion + "EDGE"); + ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${FROM_TB_EDGE_VERSION}", currentEdgeVersion + "EDGE"); currentEdgeVersion = edgeVersion; upgradeInfo = upgradeVersionHashMap.get(upgradeInfo.getNextVersion()); result.append(ubuntuUpgradeInstructions); @@ -125,9 +125,9 @@ public class DefaultEdgeUpgradeService implements EdgeUpgradeService { ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${UPGRADE_DB}", ""); } ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${TB_EDGE_TAG}", getTagVersion(edgeVersion)); - ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${CURRENT_TB_EDGE_TAG}", getTagVersion(currentEdgeVersion)); + ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${FROM_TB_EDGE_TAG}", getTagVersion(currentEdgeVersion)); ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${TB_EDGE_VERSION}", edgeVersion); - ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${CURRENT_TB_EDGE_VERSION}", currentEdgeVersion); + ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${FROM_TB_EDGE_VERSION}", currentEdgeVersion); currentEdgeVersion = edgeVersion; upgradeInfo = upgradeVersionHashMap.get(upgradeInfo.getNextVersion()); result.append(ubuntuUpgradeInstructions); 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/EdgeInstallInstructionsService.java similarity index 95% rename from application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallService.java rename to application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallInstructionsService.java index 996e14f16e..af97d7a393 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallInstructionsService.java @@ -20,7 +20,7 @@ import org.thingsboard.server.common.data.edge.EdgeInstructions; import javax.servlet.http.HttpServletRequest; -public interface EdgeInstallService { +public interface EdgeInstallInstructionsService { EdgeInstructions getInstallInstructions(Edge edge, String installationMethod, HttpServletRequest request); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java similarity index 94% rename from application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeService.java rename to application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java index b7747655ef..61abd113dc 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java @@ -17,7 +17,7 @@ package org.thingsboard.server.service.edge.instructions; import org.thingsboard.server.common.data.edge.EdgeInstructions; -public interface EdgeUpgradeService { +public interface EdgeUpgradeInstructionsService { EdgeInstructions getUpgradeInstructions(String edgeVersion, String upgradeMethod); } 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 32d648c045..4593384594 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 @@ -775,7 +775,8 @@ public final class EdgeGrpcSession implements Closeable { try { if (edge.getSecret().equals(request.getEdgeSecret())) { sessionOpenListener.accept(edge.getId(), this); - this.edgeVersion = processGetAndSaveEdgeVersion(request.getEdgeVersion()); + this.edgeVersion = request.getEdgeVersion(); + processSaveEdgeVersionAsAttribute(request.getEdgeVersion().name()); return ConnectResponseMsg.newBuilder() .setResponseCode(ConnectResponseCode.ACCEPTED) .setErrorMsg("") @@ -801,10 +802,9 @@ public final class EdgeGrpcSession implements Closeable { .setConfiguration(EdgeConfiguration.getDefaultInstance()).build(); } - private EdgeVersion processGetAndSaveEdgeVersion(EdgeVersion edgeVersion) { - AttributeKvEntry attributeKvEntry = new BaseAttributeKvEntry(new StringDataEntry("edgeVersion", edgeVersion.name()), System.currentTimeMillis()); + private void processSaveEdgeVersionAsAttribute(String edgeVersion) { + AttributeKvEntry attributeKvEntry = new BaseAttributeKvEntry(new StringDataEntry("edgeVersion", edgeVersion), System.currentTimeMillis()); ctx.getAttributesService().save(this.tenantId, this.edge.getId(), DataConstants.SERVER_SCOPE, attributeKvEntry); - return edgeVersion; } @Override diff --git a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java index c41d835251..836b5903a5 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java @@ -68,6 +68,7 @@ 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.EdgeVersion; import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg; @@ -125,7 +126,7 @@ public class EdgeControllerTest extends AbstractControllerTest { } @After - public void teardownEdgeTest() throws Exception { + public void teardownEdgeTest() { executor.shutdownNow(); } @@ -1155,4 +1156,15 @@ public class EdgeControllerTest extends AbstractControllerTest { Assert.assertTrue(installInstructions.contains("l7q4zsjplzwhk16geqxy")); Assert.assertTrue(installInstructions.contains("7390c3a6-69b0-9910-d155-b90aca4b772e")); } + + @Test + public void testGetEdgeUpgradeInstructions() throws Exception { + Edge edge = constructEdge("Edge for Test Docker Upgrade Instructions", "default"); + Edge savedEdge = doPost("/api/edge", edge, Edge.class); + String body = "{\"edgeVersion\": \"V_3_6_0\"}"; + doPostAsync("/api/plugins/telemetry/EDGE/" + savedEdge.getId().getId() + "/attributes/SERVER_SCOPE", body, String.class, status().isOk()); + String upgradeInstructions = doGet("/api/edge/instructions/upgrade/" + EdgeVersion.V_3_6_0.name() + "/docker", String.class); + Assert.assertTrue(upgradeInstructions.contains("Upgrading to 3.6.1EDGE")); + Assert.assertTrue(upgradeInstructions.contains("Upgrading to 3.6.2EDGE")); + } } diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java index a9b12865a2..973c50c1d2 100644 --- a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java @@ -111,7 +111,7 @@ public class EdgeGrpcClient implements EdgeRpcClient { .setConnectRequestMsg(ConnectRequestMsg.newBuilder() .setEdgeRoutingKey(edgeKey) .setEdgeSecret(edgeSecret) - .setEdgeVersion(EdgeVersion.V_3_6_1) + .setEdgeVersion(EdgeVersion.V_3_6_2) .setMaxInboundMessageSize(maxInboundMessageSize) .build()) .build()); 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 d90a58fde8..de35623a4e 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 @@ -3241,12 +3241,18 @@ public class RestClient implements Closeable { }).getBody(); } - public Optional getEdgeDockerInstallInstructions(EdgeId edgeId, String method) { + public Optional getEdgeInstallInstructions(EdgeId edgeId, String method) { ResponseEntity edgeInstallInstructionsResult = restTemplate.getForEntity(baseURL + "/api/edge/instructions/install/{edgeId}/{method}", EdgeInstructions.class, edgeId.getId(), method); return Optional.ofNullable(edgeInstallInstructionsResult.getBody()); } + public Optional getEdgeUpgradeInstructions(String edgeVersion, String method) { + ResponseEntity edgeUpgradeInstructionsResult = + restTemplate.getForEntity(baseURL + "/api/edge/instructions/upgrade/{edgeVersion}/{method}", EdgeInstructions.class, edgeVersion, method); + return Optional.ofNullable(edgeUpgradeInstructionsResult.getBody()); + } + public UUID saveEntitiesVersion(VersionCreateRequest request) { return restTemplate.postForEntity(baseURL + "/api/entities/vc/version", request, UUID.class).getBody(); } From 2aa4d5bcf15fb45ea874343ffca8780e220743f7 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Fri, 1 Dec 2023 14:53:46 +0200 Subject: [PATCH 12/38] Improve code using dynamic approach of updating Install or Update EdgeInstructions --- ...DefaultEdgeInstallInstructionsService.java | 5 ++ ...efaultEdgeUpgradeInstructionsService.java} | 47 ++++++++++--------- .../EdgeInstallInstructionsService.java | 2 + .../EdgeUpgradeInstructionsService.java | 7 +++ .../service/update/DefaultUpdateService.java | 24 ++++++++++ ...ientSparkplugBAttributesInProfileTest.java | 2 +- .../MqttV5ClientSparkplugBAttributesTest.java | 2 +- .../common/data/EdgeUpgradeMessage.java | 33 +++++++++++++ .../server/common/data/UpgradeInfo.java | 11 +++++ 9 files changed, 110 insertions(+), 23 deletions(-) rename application/src/main/java/org/thingsboard/server/service/edge/instructions/{DefaultEdgeUpgradeService.java => DefaultEdgeUpgradeInstructionsService.java} (85%) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/EdgeUpgradeMessage.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/UpgradeInfo.java diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java index 9da2b2abe8..49f24ac454 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java @@ -67,6 +67,11 @@ public class DefaultEdgeInstallInstructionsService implements EdgeInstallInstruc } } + @Override + public void updateApplicationVersion(String version) { + appVersion = version; + } + private EdgeInstructions getDockerInstallInstructions(Edge edge, HttpServletRequest request) { String dockerInstallInstructions = readFile(resolveFile("docker", "instructions.md")); String baseUrl = request.getServerName(); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java similarity index 85% rename from application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeService.java rename to application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java index 7ad9e94977..66f12a7ab1 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java @@ -15,13 +15,12 @@ */ package org.thingsboard.server.service.edge.instructions; -import lombok.AllArgsConstructor; -import lombok.Getter; 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.UpgradeInfo; import org.thingsboard.server.common.data.edge.EdgeInstructions; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.install.InstallScripts; @@ -31,15 +30,16 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.HashMap; +import java.util.Map; @Service @Slf4j @RequiredArgsConstructor @ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") @TbCoreComponent -public class DefaultEdgeUpgradeService implements EdgeUpgradeInstructionsService { +public class DefaultEdgeUpgradeInstructionsService implements EdgeUpgradeInstructionsService { - private static final HashMap upgradeVersionHashMap; + private static final Map upgradeVersionHashMap; static { upgradeVersionHashMap = new HashMap<>(); @@ -72,17 +72,29 @@ public class DefaultEdgeUpgradeService implements EdgeUpgradeInstructionsService } } + @Override + public void updateApplicationVersion(String version) { + appVersion = version; + } + + @Override + public void updateInstructionMap(Map map) { + for (String key : map.keySet()) { + upgradeVersionHashMap.put(key, map.get(key)); + } + } + private EdgeInstructions getDockerUpgradeInstructions(String tbVersion, String currentEdgeVersion) { UpgradeInfo upgradeInfo = upgradeVersionHashMap.get(currentEdgeVersion); - if (upgradeInfo == null || upgradeInfo.getNextVersion() == null || tbVersion.equals(currentEdgeVersion)) { + if (upgradeInfo == null || upgradeInfo.getNextEdgeVersion() == null || tbVersion.equals(currentEdgeVersion)) { return new EdgeInstructions("Edge upgrade instruction for " + currentEdgeVersion + "EDGE is not available."); } boolean rmUpgradeCompose = false; StringBuilder result = new StringBuilder(readFile(resolveFile("docker", "upgrade_preparing.md"))); - while (upgradeInfo.getNextVersion() != null || !tbVersion.equals(currentEdgeVersion)) { - String edgeVersion = upgradeInfo.getNextVersion(); + while (upgradeInfo.getNextEdgeVersion() != null || !tbVersion.equals(currentEdgeVersion)) { + String edgeVersion = upgradeInfo.getNextEdgeVersion(); String ubuntuUpgradeInstructions = readFile(resolveFile("docker", "instructions.md")); - if (upgradeInfo.isUpgradeDb()) { + if (upgradeInfo.isRequiresUpdateDb()) { String upgradeDb = readFile(resolveFile("docker", "upgrade_db.md")); ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${UPGRADE_DB}", upgradeDb); } else { @@ -98,7 +110,7 @@ public class DefaultEdgeUpgradeService implements EdgeUpgradeInstructionsService ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${TB_EDGE_VERSION}", edgeVersion + "EDGE"); ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${FROM_TB_EDGE_VERSION}", currentEdgeVersion + "EDGE"); currentEdgeVersion = edgeVersion; - upgradeInfo = upgradeVersionHashMap.get(upgradeInfo.getNextVersion()); + upgradeInfo = upgradeVersionHashMap.get(upgradeInfo.getNextEdgeVersion()); result.append(ubuntuUpgradeInstructions); } String startService = readFile(resolveFile("docker", "start_service.md")); @@ -109,16 +121,16 @@ public class DefaultEdgeUpgradeService implements EdgeUpgradeInstructionsService private EdgeInstructions getLinuxUpgradeInstructions(String tbVersion, String currentEdgeVersion, String os) { UpgradeInfo upgradeInfo = upgradeVersionHashMap.get(currentEdgeVersion); - if (upgradeInfo == null || upgradeInfo.getNextVersion() == null || tbVersion.equals(currentEdgeVersion)) { + if (upgradeInfo == null || upgradeInfo.getNextEdgeVersion() == null || tbVersion.equals(currentEdgeVersion)) { return new EdgeInstructions("Edge upgrade instruction for " + currentEdgeVersion + "EDGE is not available."); } String upgrade_preparing = readFile(resolveFile("upgrade_preparing.md")); upgrade_preparing = upgrade_preparing.replace("${OS}", os.equals("centos") ? "RHEL/CentOS 7/8" : "Ubuntu"); StringBuilder result = new StringBuilder(upgrade_preparing); - while (upgradeInfo.getNextVersion() != null || !tbVersion.equals(currentEdgeVersion)) { - String edgeVersion = upgradeInfo.getNextVersion(); + while (upgradeInfo.getNextEdgeVersion() != null || !tbVersion.equals(currentEdgeVersion)) { + String edgeVersion = upgradeInfo.getNextEdgeVersion(); String ubuntuUpgradeInstructions = readFile(resolveFile(os, "instructions.md")); - if (upgradeInfo.isUpgradeDb()) { + if (upgradeInfo.isRequiresUpdateDb()) { String upgradeDb = readFile(resolveFile("upgrade_db.md")); ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${UPGRADE_DB}", upgradeDb); } else { @@ -129,7 +141,7 @@ public class DefaultEdgeUpgradeService implements EdgeUpgradeInstructionsService ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${TB_EDGE_VERSION}", edgeVersion); ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${FROM_TB_EDGE_VERSION}", currentEdgeVersion); currentEdgeVersion = edgeVersion; - upgradeInfo = upgradeVersionHashMap.get(upgradeInfo.getNextVersion()); + upgradeInfo = upgradeVersionHashMap.get(upgradeInfo.getNextEdgeVersion()); result.append(ubuntuUpgradeInstructions); } String startService = readFile(resolveFile("start_service.md")); @@ -161,11 +173,4 @@ public class DefaultEdgeUpgradeService implements EdgeUpgradeInstructionsService private Path getEdgeInstallInstructionsDir() { return Paths.get(installScripts.getDataDir(), InstallScripts.JSON_DIR, EDGE_DIR, INSTRUCTIONS_DIR, UPGRADE_DIR); } - - @AllArgsConstructor - @Getter - public static class UpgradeInfo { - private boolean upgradeDb; - private String nextVersion; - } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallInstructionsService.java index af97d7a393..2c1ef4fe87 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallInstructionsService.java @@ -23,4 +23,6 @@ import javax.servlet.http.HttpServletRequest; public interface EdgeInstallInstructionsService { EdgeInstructions getInstallInstructions(Edge edge, String installationMethod, HttpServletRequest request); + + void updateApplicationVersion(String version); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java index 61abd113dc..4c67c1ab9c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java @@ -15,9 +15,16 @@ */ package org.thingsboard.server.service.edge.instructions; +import org.thingsboard.server.common.data.UpgradeInfo; import org.thingsboard.server.common.data.edge.EdgeInstructions; +import java.util.Map; + public interface EdgeUpgradeInstructionsService { EdgeInstructions getUpgradeInstructions(String edgeVersion, String upgradeMethod); + + void updateInstructionMap(Map upgradeVersions); + + void updateApplicationVersion(String version); } diff --git a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java index f5e3daf05d..7ddc4b0e9d 100644 --- a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java @@ -27,17 +27,21 @@ import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.EdgeUpgradeMessage; import org.thingsboard.server.common.data.UpdateMessage; import org.thingsboard.server.common.data.notification.rule.trigger.NewPlatformVersionTrigger; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.instructions.EdgeInstallInstructionsService; +import org.thingsboard.server.service.edge.instructions.EdgeUpgradeInstructionsService; import javax.annotation.PreDestroy; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.HashMap; import java.util.UUID; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -65,12 +69,20 @@ public class DefaultUpdateService implements UpdateService { @Autowired private NotificationRuleProcessor notificationRuleProcessor; + @Autowired + private EdgeInstallInstructionsService edgeInstallInstructionsService; + + @Autowired + private EdgeUpgradeInstructionsService edgeUpgradeInstructionsService; + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1, ThingsBoardThreadFactory.forName("tb-update-service")); private ScheduledFuture checkUpdatesFuture = null; private final RestTemplate restClient = new RestTemplate(); private UpdateMessage updateMessage; + private EdgeUpgradeMessage edgeUpgradeMessage; + private String edgeInstallVersion; private String platform; private String version; @@ -82,6 +94,7 @@ public class DefaultUpdateService implements UpdateService { updateMessage = new UpdateMessage(false, version, "", "", "https://thingsboard.io/docs/reference/releases", "https://thingsboard.io/docs/reference/releases"); + edgeUpgradeMessage = new EdgeUpgradeMessage(new HashMap<>()); if (updatesEnabled) { try { platform = System.getProperty("platform", "unknown"); @@ -141,6 +154,17 @@ public class DefaultUpdateService implements UpdateService { .updateInfo(updateMessage) .build()); } + String prevEdgeInstallVersion = edgeInstallVersion; + edgeInstallVersion = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v1/edge/install", new HttpEntity<>(request.toString(), headers), String.class); + if (edgeInstallVersion != null && !edgeInstallVersion.equals(prevEdgeInstallVersion)) { + edgeInstallInstructionsService.updateApplicationVersion(edgeInstallVersion); + edgeUpgradeInstructionsService.updateApplicationVersion(edgeInstallVersion); + } + EdgeUpgradeMessage prevEdgeUpgradeMessage = edgeUpgradeMessage; + edgeUpgradeMessage = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v1/edge/upgrades", new HttpEntity<>(request.toString(), headers), EdgeUpgradeMessage.class); + if (edgeUpgradeMessage != null && !edgeUpgradeMessage.equals(prevEdgeUpgradeMessage)) { + edgeUpgradeInstructionsService.updateInstructionMap(edgeUpgradeMessage.getEdgeVersions()); + } } catch (Exception e) { log.trace(e.getMessage()); } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesInProfileTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesInProfileTest.java index 1d5a2c127f..faa29ad35d 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesInProfileTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesInProfileTest.java @@ -52,4 +52,4 @@ public class MqttV5ClientSparkplugBAttributesInProfileTest extends AbstractMqttV processClientDeviceWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes(); } -} \ No newline at end of file +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesTest.java index f5d42046b7..abe1810887 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesTest.java @@ -78,4 +78,4 @@ public class MqttV5ClientSparkplugBAttributesTest extends AbstractMqttV5ClientSp processClientDeviceWithCorrectAccessTokenPublishWithBirth_SharedAttributes_LongType_IfMetricFailedTypeCheck_SendValueOk(); } -} \ No newline at end of file +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUpgradeMessage.java b/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUpgradeMessage.java new file mode 100644 index 0000000000..67cbce56cb --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUpgradeMessage.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.util.Map; + +@Data +@ApiModel +public class EdgeUpgradeMessage implements Serializable { + + private static final long serialVersionUID = 2872965507642822989L; + + @ApiModelProperty(position = 1, value = "Mapping for upgrade versions and upgrade strategy (next ver).") + private final Map edgeVersions; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/UpgradeInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/UpgradeInfo.java new file mode 100644 index 0000000000..d9101bd971 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/UpgradeInfo.java @@ -0,0 +1,11 @@ +package org.thingsboard.server.common.data; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class UpgradeInfo { + private boolean requiresUpdateDb; + private String nextEdgeVersion; +} From 713915cfa18ef5a8bd130aba6e26a26ab81c585a Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Fri, 1 Dec 2023 15:02:29 +0200 Subject: [PATCH 13/38] Fix license-format: Add header to new file --- .../server/common/data/UpgradeInfo.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/UpgradeInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/UpgradeInfo.java index d9101bd971..8dd8f4d9b8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/UpgradeInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/UpgradeInfo.java @@ -1,3 +1,18 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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; import lombok.AllArgsConstructor; From 1a23673df86162825cbdbcdbde5a50ff711dc03d Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Fri, 1 Dec 2023 15:21:06 +0200 Subject: [PATCH 14/38] Refactor upgradeVersionHashMap to use only Update service for update, do not hardcode --- .../DefaultEdgeUpgradeInstructionsService.java | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java index 66f12a7ab1..138fae70f2 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java @@ -39,14 +39,7 @@ import java.util.Map; @TbCoreComponent public class DefaultEdgeUpgradeInstructionsService implements EdgeUpgradeInstructionsService { - private static final Map upgradeVersionHashMap; - - static { - upgradeVersionHashMap = new HashMap<>(); - upgradeVersionHashMap.put("3.6.0", new UpgradeInfo(true, "3.6.1")); - upgradeVersionHashMap.put("3.6.1", new UpgradeInfo(true, "3.6.2")); - upgradeVersionHashMap.put("3.6.2", new UpgradeInfo(true, null)); - } + private static final Map upgradeVersionHashMap = new HashMap<>(); private static final String EDGE_DIR = "edge"; private static final String INSTRUCTIONS_DIR = "instructions"; From fef62e2693a551f245c6e7b136a310334519afc6 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 4 Dec 2023 13:24:57 +0200 Subject: [PATCH 15/38] Update psql to use 15 version --- .../install/centos/instructions.md | 22 +++++++++---------- .../install/ubuntu/instructions.md | 2 +- .../instructions/upgrade/docker/upgrade_db.md | 2 +- .../service/update/DefaultUpdateService.java | 4 ++-- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/application/src/main/data/json/edge/instructions/install/centos/instructions.md b/application/src/main/data/json/edge/instructions/install/centos/instructions.md index 9ad882c387..22bb3f63a0 100644 --- a/application/src/main/data/json/edge/instructions/install/centos/instructions.md +++ b/application/src/main/data/json/edge/instructions/install/centos/instructions.md @@ -56,13 +56,13 @@ sudo yum update sudo yum -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm # Install packages sudo yum -y install epel-release yum-utils -sudo yum-config-manager --enable pgdg12 -sudo yum install postgresql12-server postgresql12 +sudo yum-config-manager --enable pgdg15 +sudo yum install postgresql15-server postgresql15 # Initialize your PostgreSQL DB -sudo /usr/pgsql-12/bin/postgresql-12-setup initdb -sudo systemctl start postgresql-12 +sudo /usr/pgsql-15/bin/postgresql-15-setup initdb +sudo systemctl start postgresql-15 # Optional: Configure PostgreSQL to start on boot -sudo systemctl enable --now postgresql-12 +sudo systemctl enable --now postgresql-15 {:copy-code} ``` @@ -74,12 +74,12 @@ sudo systemctl enable --now postgresql-12 sudo yum -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm # Install packages sudo dnf -qy module disable postgresql -sudo dnf -y install postgresql12 postgresql12-server +sudo dnf -y install postgresql15 postgresql15-server # Initialize your PostgreSQL DB -sudo /usr/pgsql-12/bin/postgresql-12-setup initdb -sudo systemctl start postgresql-12 +sudo /usr/pgsql-15/bin/postgresql-15-setup initdb +sudo systemctl start postgresql-15 # Optional: Configure PostgreSQL to start on boot -sudo systemctl enable --now postgresql-12 +sudo systemctl enable --now postgresql-15 {:copy-code} ``` @@ -101,7 +101,7 @@ After configuring the password, edit the pg_hba.conf to use MD5 authentication w Edit pg_hba.conf file: ```bash -sudo nano /var/lib/pgsql/12/data/pg_hba.conf +sudo nano /var/lib/pgsql/15/data/pg_hba.conf {:copy-code} ``` @@ -121,7 +121,7 @@ host all all 127.0.0.1/32 md5 Finally, you should restart the PostgreSQL service to initialize the new configuration: ```bash -sudo systemctl restart postgresql-12.service +sudo systemctl restart postgresql-15.service {:copy-code} ``` diff --git a/application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md b/application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md index 2a6d9c0bc3..6fcdcd50f2 100644 --- a/application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md +++ b/application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md @@ -49,7 +49,7 @@ echo "deb http://apt.postgresql.org/pub/repos/apt/ ${RELEASE}"-pgdg main | sudo # install and launch the postgresql service: sudo apt update -sudo apt -y install postgresql-12 +sudo apt -y install postgresql-15 sudo service postgresql start {:copy-code} ``` diff --git a/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md index ac38031517..8c218ab41d 100644 --- a/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md +++ b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md @@ -23,7 +23,7 @@ services: entrypoint: upgrade-tb-edge.sh postgres: restart: always - image: "postgres:12" + image: "postgres:15" ports: - "5432" environment: diff --git a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java index 7ddc4b0e9d..cf31464ba9 100644 --- a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java @@ -69,10 +69,10 @@ public class DefaultUpdateService implements UpdateService { @Autowired private NotificationRuleProcessor notificationRuleProcessor; - @Autowired + @Autowired(required = false) private EdgeInstallInstructionsService edgeInstallInstructionsService; - @Autowired + @Autowired(required = false) private EdgeUpgradeInstructionsService edgeUpgradeInstructionsService; private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1, ThingsBoardThreadFactory.forName("tb-update-service")); From 36629fddb38070a854b3376eae305db55629a3a2 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 4 Dec 2023 14:51:31 +0200 Subject: [PATCH 16/38] Fix test and change upgrade instructions title --- .../server/controller/EdgeControllerTest.java | 12 ++++++++++++ .../pages/edge/edge-instructions-dialog.component.ts | 3 +++ 2 files changed, 15 insertions(+) diff --git a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java index cacc5b86c8..fe5cceb206 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java @@ -48,6 +48,7 @@ import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.UpgradeInfo; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; @@ -83,9 +84,11 @@ import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; +import org.thingsboard.server.service.edge.instructions.EdgeUpgradeInstructionsService; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -116,6 +119,9 @@ public class EdgeControllerTest extends AbstractControllerTest { @Autowired private EdgeDao edgeDao; + @Autowired + private EdgeUpgradeInstructionsService edgeUpgradeInstructionsService; + static class Config { @Bean @Primary @@ -1180,6 +1186,12 @@ public class EdgeControllerTest extends AbstractControllerTest { @Test public void testGetEdgeUpgradeInstructions() throws Exception { + // UpdateInfo config is updating from Thingsboard Update server + HashMap upgradeInfoHashMap = new HashMap<>(); + upgradeInfoHashMap.put("3.6.0", new UpgradeInfo(true, "3.6.1")); + upgradeInfoHashMap.put("3.6.1", new UpgradeInfo(true, "3.6.2")); + upgradeInfoHashMap.put("3.6.2", new UpgradeInfo(true, null)); + edgeUpgradeInstructionsService.updateInstructionMap(upgradeInfoHashMap); Edge edge = constructEdge("Edge for Test Docker Upgrade Instructions", "default"); Edge savedEdge = doPost("/api/edge", edge, Edge.class); String body = "{\"edgeVersion\": \"V_3_6_0\"}"; 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 index c618df96bb..6c9511d709 100644 --- 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 @@ -65,6 +65,9 @@ export class EdgeInstructionsDialogComponent extends DialogComponent Date: Tue, 5 Dec 2023 10:04:14 +0200 Subject: [PATCH 17/38] Minor refactoring. Provide better naming and refactor due to changes in thingsboard-updates --- ...DefaultEdgeUpgradeInstructionsService.java | 30 +++++++++---------- .../EdgeUpgradeInstructionsService.java | 4 +-- .../service/update/DefaultUpdateService.java | 5 ++-- .../server/controller/EdgeControllerTest.java | 10 +++---- ...{UpgradeInfo.java => EdgeUpgradeInfo.java} | 2 +- .../common/data/EdgeUpgradeMessage.java | 2 +- 6 files changed, 27 insertions(+), 26 deletions(-) rename common/data/src/main/java/org/thingsboard/server/common/data/{UpgradeInfo.java => EdgeUpgradeInfo.java} (96%) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java index 138fae70f2..0401521a1c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java @@ -20,7 +20,7 @@ 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.UpgradeInfo; +import org.thingsboard.server.common.data.EdgeUpgradeInfo; import org.thingsboard.server.common.data.edge.EdgeInstructions; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.install.InstallScripts; @@ -39,7 +39,7 @@ import java.util.Map; @TbCoreComponent public class DefaultEdgeUpgradeInstructionsService implements EdgeUpgradeInstructionsService { - private static final Map upgradeVersionHashMap = new HashMap<>(); + private static final Map upgradeVersionHashMap = new HashMap<>(); private static final String EDGE_DIR = "edge"; private static final String INSTRUCTIONS_DIR = "instructions"; @@ -71,23 +71,23 @@ public class DefaultEdgeUpgradeInstructionsService implements EdgeUpgradeInstruc } @Override - public void updateInstructionMap(Map map) { + public void updateInstructionMap(Map map) { for (String key : map.keySet()) { upgradeVersionHashMap.put(key, map.get(key)); } } private EdgeInstructions getDockerUpgradeInstructions(String tbVersion, String currentEdgeVersion) { - UpgradeInfo upgradeInfo = upgradeVersionHashMap.get(currentEdgeVersion); - if (upgradeInfo == null || upgradeInfo.getNextEdgeVersion() == null || tbVersion.equals(currentEdgeVersion)) { + EdgeUpgradeInfo edgeUpgradeInfo = upgradeVersionHashMap.get(currentEdgeVersion); + if (edgeUpgradeInfo == null || edgeUpgradeInfo.getNextEdgeVersion() == null || tbVersion.equals(currentEdgeVersion)) { return new EdgeInstructions("Edge upgrade instruction for " + currentEdgeVersion + "EDGE is not available."); } boolean rmUpgradeCompose = false; StringBuilder result = new StringBuilder(readFile(resolveFile("docker", "upgrade_preparing.md"))); - while (upgradeInfo.getNextEdgeVersion() != null || !tbVersion.equals(currentEdgeVersion)) { - String edgeVersion = upgradeInfo.getNextEdgeVersion(); + while (edgeUpgradeInfo.getNextEdgeVersion() != null || !tbVersion.equals(currentEdgeVersion)) { + String edgeVersion = edgeUpgradeInfo.getNextEdgeVersion(); String ubuntuUpgradeInstructions = readFile(resolveFile("docker", "instructions.md")); - if (upgradeInfo.isRequiresUpdateDb()) { + if (edgeUpgradeInfo.isRequiresUpdateDb()) { String upgradeDb = readFile(resolveFile("docker", "upgrade_db.md")); ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${UPGRADE_DB}", upgradeDb); } else { @@ -103,7 +103,7 @@ public class DefaultEdgeUpgradeInstructionsService implements EdgeUpgradeInstruc ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${TB_EDGE_VERSION}", edgeVersion + "EDGE"); ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${FROM_TB_EDGE_VERSION}", currentEdgeVersion + "EDGE"); currentEdgeVersion = edgeVersion; - upgradeInfo = upgradeVersionHashMap.get(upgradeInfo.getNextEdgeVersion()); + edgeUpgradeInfo = upgradeVersionHashMap.get(edgeUpgradeInfo.getNextEdgeVersion()); result.append(ubuntuUpgradeInstructions); } String startService = readFile(resolveFile("docker", "start_service.md")); @@ -113,17 +113,17 @@ public class DefaultEdgeUpgradeInstructionsService implements EdgeUpgradeInstruc } private EdgeInstructions getLinuxUpgradeInstructions(String tbVersion, String currentEdgeVersion, String os) { - UpgradeInfo upgradeInfo = upgradeVersionHashMap.get(currentEdgeVersion); - if (upgradeInfo == null || upgradeInfo.getNextEdgeVersion() == null || tbVersion.equals(currentEdgeVersion)) { + EdgeUpgradeInfo edgeUpgradeInfo = upgradeVersionHashMap.get(currentEdgeVersion); + if (edgeUpgradeInfo == null || edgeUpgradeInfo.getNextEdgeVersion() == null || tbVersion.equals(currentEdgeVersion)) { return new EdgeInstructions("Edge upgrade instruction for " + currentEdgeVersion + "EDGE is not available."); } String upgrade_preparing = readFile(resolveFile("upgrade_preparing.md")); upgrade_preparing = upgrade_preparing.replace("${OS}", os.equals("centos") ? "RHEL/CentOS 7/8" : "Ubuntu"); StringBuilder result = new StringBuilder(upgrade_preparing); - while (upgradeInfo.getNextEdgeVersion() != null || !tbVersion.equals(currentEdgeVersion)) { - String edgeVersion = upgradeInfo.getNextEdgeVersion(); + while (edgeUpgradeInfo.getNextEdgeVersion() != null || !tbVersion.equals(currentEdgeVersion)) { + String edgeVersion = edgeUpgradeInfo.getNextEdgeVersion(); String ubuntuUpgradeInstructions = readFile(resolveFile(os, "instructions.md")); - if (upgradeInfo.isRequiresUpdateDb()) { + if (edgeUpgradeInfo.isRequiresUpdateDb()) { String upgradeDb = readFile(resolveFile("upgrade_db.md")); ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${UPGRADE_DB}", upgradeDb); } else { @@ -134,7 +134,7 @@ public class DefaultEdgeUpgradeInstructionsService implements EdgeUpgradeInstruc ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${TB_EDGE_VERSION}", edgeVersion); ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${FROM_TB_EDGE_VERSION}", currentEdgeVersion); currentEdgeVersion = edgeVersion; - upgradeInfo = upgradeVersionHashMap.get(upgradeInfo.getNextEdgeVersion()); + edgeUpgradeInfo = upgradeVersionHashMap.get(edgeUpgradeInfo.getNextEdgeVersion()); result.append(ubuntuUpgradeInstructions); } String startService = readFile(resolveFile("start_service.md")); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java index 4c67c1ab9c..e6f5cb0753 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.service.edge.instructions; -import org.thingsboard.server.common.data.UpgradeInfo; +import org.thingsboard.server.common.data.EdgeUpgradeInfo; import org.thingsboard.server.common.data.edge.EdgeInstructions; import java.util.Map; @@ -24,7 +24,7 @@ public interface EdgeUpgradeInstructionsService { EdgeInstructions getUpgradeInstructions(String edgeVersion, String upgradeMethod); - void updateInstructionMap(Map upgradeVersions); + void updateInstructionMap(Map upgradeVersions); void updateApplicationVersion(String version); } diff --git a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java index cf31464ba9..6a86d022df 100644 --- a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java @@ -154,14 +154,15 @@ public class DefaultUpdateService implements UpdateService { .updateInfo(updateMessage) .build()); } + ObjectNode edgeRequest = JacksonUtil.newObjectNode().put(VERSION_PARAM, version); String prevEdgeInstallVersion = edgeInstallVersion; - edgeInstallVersion = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v1/edge/install", new HttpEntity<>(request.toString(), headers), String.class); + edgeInstallVersion = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v1/edge/installMapping", new HttpEntity<>(edgeRequest.toString(), headers), String.class); if (edgeInstallVersion != null && !edgeInstallVersion.equals(prevEdgeInstallVersion)) { edgeInstallInstructionsService.updateApplicationVersion(edgeInstallVersion); edgeUpgradeInstructionsService.updateApplicationVersion(edgeInstallVersion); } EdgeUpgradeMessage prevEdgeUpgradeMessage = edgeUpgradeMessage; - edgeUpgradeMessage = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v1/edge/upgrades", new HttpEntity<>(request.toString(), headers), EdgeUpgradeMessage.class); + edgeUpgradeMessage = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v1/edge/upgradeMapping", new HttpEntity<>(edgeRequest.toString(), headers), EdgeUpgradeMessage.class); if (edgeUpgradeMessage != null && !edgeUpgradeMessage.equals(prevEdgeUpgradeMessage)) { edgeUpgradeInstructionsService.updateInstructionMap(edgeUpgradeMessage.getEdgeVersions()); } diff --git a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java index fe5cceb206..9ecabf06f3 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java @@ -48,7 +48,7 @@ import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; -import org.thingsboard.server.common.data.UpgradeInfo; +import org.thingsboard.server.common.data.EdgeUpgradeInfo; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; @@ -1187,10 +1187,10 @@ public class EdgeControllerTest extends AbstractControllerTest { @Test public void testGetEdgeUpgradeInstructions() throws Exception { // UpdateInfo config is updating from Thingsboard Update server - HashMap upgradeInfoHashMap = new HashMap<>(); - upgradeInfoHashMap.put("3.6.0", new UpgradeInfo(true, "3.6.1")); - upgradeInfoHashMap.put("3.6.1", new UpgradeInfo(true, "3.6.2")); - upgradeInfoHashMap.put("3.6.2", new UpgradeInfo(true, null)); + HashMap upgradeInfoHashMap = new HashMap<>(); + upgradeInfoHashMap.put("3.6.0", new EdgeUpgradeInfo(true, "3.6.1")); + upgradeInfoHashMap.put("3.6.1", new EdgeUpgradeInfo(true, "3.6.2")); + upgradeInfoHashMap.put("3.6.2", new EdgeUpgradeInfo(true, null)); edgeUpgradeInstructionsService.updateInstructionMap(upgradeInfoHashMap); Edge edge = constructEdge("Edge for Test Docker Upgrade Instructions", "default"); Edge savedEdge = doPost("/api/edge", edge, Edge.class); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/UpgradeInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUpgradeInfo.java similarity index 96% rename from common/data/src/main/java/org/thingsboard/server/common/data/UpgradeInfo.java rename to common/data/src/main/java/org/thingsboard/server/common/data/EdgeUpgradeInfo.java index 8dd8f4d9b8..cadd589b49 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/UpgradeInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUpgradeInfo.java @@ -20,7 +20,7 @@ import lombok.Getter; @AllArgsConstructor @Getter -public class UpgradeInfo { +public class EdgeUpgradeInfo { private boolean requiresUpdateDb; private String nextEdgeVersion; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUpgradeMessage.java b/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUpgradeMessage.java index 67cbce56cb..bb17479607 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUpgradeMessage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUpgradeMessage.java @@ -29,5 +29,5 @@ public class EdgeUpgradeMessage implements Serializable { private static final long serialVersionUID = 2872965507642822989L; @ApiModelProperty(position = 1, value = "Mapping for upgrade versions and upgrade strategy (next ver).") - private final Map edgeVersions; + private final Map edgeVersions; } From 375ba464513bef22131905e76096e0e0602607ce Mon Sep 17 00:00:00 2001 From: ShvaykaD Date: Tue, 5 Dec 2023 18:52:52 +0200 Subject: [PATCH 18/38] added new get entity profile names APIs for profile entities(AssetProfile & DeviceProfile) --- .../server/controller/AssetController.java | 4 +- .../controller/AssetProfileController.java | 20 +++++++ .../server/controller/DeviceController.java | 19 +++--- .../controller/DeviceProfileController.java | 18 ++++++ .../AssetProfileControllerTest.java | 59 +++++++++++++++++++ .../DeviceProfileControllerTest.java | 59 +++++++++++++++++++ .../server/dao/asset/AssetProfileService.java | 5 ++ .../server/dao/asset/AssetService.java | 1 + .../dao/device/DeviceProfileService.java | 5 ++ .../server/dao/device/DeviceService.java | 1 + .../org/thingsboard/server/dao/DaoUtil.java | 14 +++++ .../server/dao/asset/AssetDao.java | 3 +- .../server/dao/asset/AssetProfileDao.java | 4 ++ .../dao/asset/AssetProfileServiceImpl.java | 14 ++++- .../server/dao/device/DeviceDao.java | 1 + .../server/dao/device/DeviceProfileDao.java | 4 ++ .../dao/device/DeviceProfileServiceImpl.java | 17 +++++- .../dao/sql/asset/AssetProfileRepository.java | 9 +++ .../server/dao/sql/asset/AssetRepository.java | 3 - .../server/dao/sql/asset/JpaAssetDao.java | 8 ++- .../dao/sql/asset/JpaAssetProfileDao.java | 8 +++ .../sql/device/DeviceProfileRepository.java | 10 +++- .../dao/sql/device/DeviceRepository.java | 3 - .../server/dao/sql/device/JpaDeviceDao.java | 8 ++- .../dao/sql/device/JpaDeviceProfileDao.java | 8 +++ .../thingsboard/rest/client/RestClient.java | 22 ++++++- 26 files changed, 298 insertions(+), 29 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java index 7e64ec428a..58b9376f1f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java @@ -397,10 +397,12 @@ public class AssetController extends BaseController { } @ApiOperation(value = "Get Asset Types (getAssetTypes)", - notes = "Returns a set of unique asset types based on assets that are either owned by the tenant or assigned to the customer which user is performing the request.", produces = MediaType.APPLICATION_JSON_VALUE) + notes = "Deprecated. See 'getAssetProfileNames' API from Asset Profile Controller instead." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/asset/types", method = RequestMethod.GET) @ResponseBody + @Deprecated(since = "3.6.2") public List getAssetTypes() throws ThingsboardException, ExecutionException, InterruptedException { SecurityUser user = getCurrentUser(); TenantId tenantId = user.getTenantId(); diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java index d9a2923d52..59c9670a0c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java @@ -29,18 +29,23 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetProfileInfo; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AssetProfileId; +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.resource.ImageService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.asset.profile.TbAssetProfileService; +import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.List; + import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_ID; import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_INFO_DESCRIPTION; @@ -212,4 +217,19 @@ public class AssetProfileController extends BaseController { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return checkNotNull(assetProfileService.findAssetProfileInfos(getTenantId(), pageLink)); } + + @ApiOperation(value = "Get Asset Profile names (getAssetProfileNames)", + notes = "Returns a set of unique asset profile names owned by the tenant." + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/assetProfile/names", method = RequestMethod.GET) + @ResponseBody + public List getAssetProfileNames( + @ApiParam(value = "Flag indicating whether to retrieve exclusively the names of asset profiles that are referenced by tenant's assets.") + @RequestParam(value = "activeOnly", required = false, defaultValue = "false") boolean activeOnly) throws ThingsboardException { + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + return checkNotNull(assetProfileService.findAssetProfileNamesByTenantId(tenantId, activeOnly)); + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index c05cf5cd6c..1761056051 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -194,14 +194,14 @@ public class DeviceController extends BaseController { "Requires to provide the Device Credentials object as well as an existing device profile ID or use \"default\".\n" + "You may find the example of device with different type of credentials below: \n\n" + "- Credentials type: \"Access token\" with device profile ID below: \n\n" + - DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN + "\n\n" + + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN + "\n\n" + "- Credentials type: \"Access token\" with device profile default below: \n\n" + - DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DEFAULT_DESCRIPTION_MARKDOWN + "\n\n" + + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DEFAULT_DESCRIPTION_MARKDOWN + "\n\n" + "- Credentials type: \"X509\" with device profile ID below: \n\n" + "Note: credentialsId - format Sha3Hash, certificateValue - format PEM (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" + - DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN + "\n\n" + + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN + "\n\n" + "- Credentials type: \"MQTT_BASIC\" with device profile ID below: \n\n" + - DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN + "\n\n" + + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN + "\n\n" + "- You may find the example of LwM2M device and RPK credentials below: \n\n" + "Note: LwM2M device - only existing device profile ID (Transport configuration -> Transport type: \"LWM2M\".\n\n" + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN + "\n\n" + @@ -304,12 +304,12 @@ public class DeviceController extends BaseController { "The structure of device credentials id and value is simple for the 'ACCESS_TOKEN' but is much more complex for the 'MQTT_BASIC' or 'LWM2M_CREDENTIALS'.\n" + "You may find the example of device with different type of credentials below: \n\n" + "- Credentials type: \"Access token\" with device ID and with device ID below: \n\n" + - DEVICE_UPDATE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN + "\n\n" + + DEVICE_UPDATE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN + "\n\n" + "- Credentials type: \"X509\" with device profile ID below: \n\n" + "Note: credentialsId - format Sha3Hash, certificateValue - format PEM (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" + - DEVICE_UPDATE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN + "\n\n" + + DEVICE_UPDATE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN + "\n\n" + "- Credentials type: \"MQTT_BASIC\" with device profile ID below: \n\n" + - DEVICE_UPDATE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN + "\n\n" + + DEVICE_UPDATE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN + "\n\n" + "- You may find the example of LwM2M device and RPK credentials below: \n\n" + "Note: LwM2M device - only existing device profile ID (Transport configuration -> Transport type: \"LWM2M\".\n\n" + DEVICE_UPDATE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN + "\n\n" + @@ -534,11 +534,12 @@ public class DeviceController extends BaseController { } @ApiOperation(value = "Get Device Types (getDeviceTypes)", - notes = "Returns a set of unique device profile names based on devices that are either owned by the tenant or assigned to the customer which user is performing the request." - + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + notes = "Deprecated. See 'getDeviceProfileNames' API from Device Profile Controller instead." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/device/types", method = RequestMethod.GET) @ResponseBody + @Deprecated(since = "3.6.2") public List getDeviceTypes() throws ThingsboardException, ExecutionException, InterruptedException { SecurityUser user = getCurrentUser(); TenantId tenantId = user.getTenantId(); diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java index 3c07946309..c16e201bad 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java @@ -32,15 +32,18 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileInfo; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.device.profile.TbDeviceProfileService; +import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; @@ -273,4 +276,19 @@ public class DeviceProfileController extends BaseController { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return checkNotNull(deviceProfileService.findDeviceProfileInfos(getTenantId(), pageLink, transportType)); } + + @ApiOperation(value = "Get Device Profile names (getDeviceProfileNames)", + notes = "Returns a set of unique device profile names owned by the tenant." + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/deviceProfile/names", method = RequestMethod.GET) + @ResponseBody + public List getDeviceProfileNames( + @ApiParam(value = "Flag indicating whether to retrieve exclusively the names of device profiles that are referenced by tenant's devices.") + @RequestParam(value = "activeOnly", required = false, defaultValue = "false") boolean activeOnly) throws ThingsboardException { + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + return checkNotNull(deviceProfileService.findDeviceProfileNamesByTenantId(tenantId, activeOnly)); + } + } diff --git a/application/src/test/java/org/thingsboard/server/controller/AssetProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AssetProfileControllerTest.java index 38420c3e32..34b1006ffb 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AssetProfileControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AssetProfileControllerTest.java @@ -28,6 +28,7 @@ import org.springframework.context.annotation.Primary; 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.EntityInfo; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; @@ -46,11 +47,13 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.common.data.DataConstants.DEFAULT_DEVICE_TYPE; @ContextConfiguration(classes = {AssetProfileControllerTest.Config.class}) @DaoSqlTest @@ -459,6 +462,62 @@ public class AssetProfileControllerTest extends AbstractControllerTest { testEntityDaoWithRelationsTransactionalException(assetProfileDao, savedTenant.getId(), assetProfileId, "/api/assetProfile/" + assetProfileId); } + @Test + public void testGetAssetProfileNames() throws Exception { + var pageLink = new PageLink(Integer.MAX_VALUE); + var assetProfileInfos = doGetTypedWithPageLink("/api/assetProfileInfos?", + new TypeReference>() { + }, pageLink); + Assert.assertNotNull("Asset Profile Infos page data is null!", assetProfileInfos); + Assert.assertEquals("Asset Profile Infos Page data is empty! Expected to have default profile created!", 1, assetProfileInfos.getTotalElements()); + List expectedAssetProfileNames = assetProfileInfos.getData().stream() + .map(info -> new EntityInfo(info.getId(), info.getName())) + .sorted(Comparator.comparing(EntityInfo::getName)) + .collect(Collectors.toList()); + var assetProfileNames = doGetTyped("/api/assetProfile/names", new TypeReference>() { + }); + Assert.assertNotNull("Asset Profile Names list is null!", assetProfileNames); + Assert.assertFalse("Asset Profile Names list is empty!", assetProfileNames.isEmpty()); + Assert.assertEquals(expectedAssetProfileNames, assetProfileNames); + Assert.assertEquals(1, assetProfileNames.size()); + Assert.assertEquals(DEFAULT_DEVICE_TYPE, assetProfileNames.get(0).getName()); + + int count = 3; + for (int i = 0; i < count; i++) { + Asset asset = new Asset(); + asset.setName("AssetName" + i); + asset.setType("AssetProfileName" + i); + Asset savedAsset = doPost("/api/asset", asset, Asset.class); + Assert.assertNotNull(savedAsset); + } + assetProfileInfos = doGetTypedWithPageLink("/api/assetProfileInfos?", + new TypeReference<>() { + }, pageLink); + Assert.assertNotNull("Asset Profile Infos page data is null!", assetProfileInfos); + Assert.assertEquals("Asset Profile Infos Page data is empty! Expected to have default profile created + count value!", 1 + count, assetProfileInfos.getTotalElements()); + expectedAssetProfileNames = assetProfileInfos.getData().stream() + .map(info -> new EntityInfo(info.getId(), info.getName())) + .sorted(Comparator.comparing(EntityInfo::getName)) + .collect(Collectors.toList()); + + assetProfileNames = doGetTyped("/api/assetProfile/names", new TypeReference<>() { + }); + Assert.assertNotNull("Asset Profile Names list is null!", assetProfileNames); + Assert.assertFalse("Asset Profile Names list is empty!", assetProfileNames.isEmpty()); + Assert.assertEquals(expectedAssetProfileNames, assetProfileNames); + Assert.assertEquals(1 + count, assetProfileNames.size()); + + assetProfileNames = doGetTyped("/api/assetProfile/names?activeOnly=true", new TypeReference<>() { + }); + Assert.assertNotNull("Asset Profile Names list is null!", assetProfileNames); + Assert.assertFalse("Asset Profile Names list is empty!", assetProfileNames.isEmpty()); + var expectedAssetProfileNamesWithoutDefault = expectedAssetProfileNames.stream() + .filter(entityInfo -> !entityInfo.getName().equals(DEFAULT_DEVICE_TYPE)) + .collect(Collectors.toList()); + Assert.assertEquals(expectedAssetProfileNamesWithoutDefault, assetProfileNames); + Assert.assertEquals(count, assetProfileNames.size()); + } + private AssetProfile savedAssetProfile(String name) { AssetProfile assetProfile = createAssetProfile(name); return doPost("/api/assetProfile", assetProfile, AssetProfile.class); diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java index 14b052d93c..7cdfa10ea0 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java @@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.DeviceProfileInfo; import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.SaveOtaPackageInfoRequest; import org.thingsboard.server.common.data.StringUtils; @@ -55,11 +56,13 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.common.data.DataConstants.DEFAULT_DEVICE_TYPE; import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE; import static org.thingsboard.server.common.data.ota.OtaPackageType.SOFTWARE; @@ -1045,6 +1048,62 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { testEntityDaoWithRelationsTransactionalException(deviceProfileDao, savedTenant.getId(), deviceProfileId, "/api/deviceProfile/" + deviceProfileId); } + @Test + public void testGetDeviceProfileNames() throws Exception { + var pageLink = new PageLink(Integer.MAX_VALUE); + var deviceProfileInfos = doGetTypedWithPageLink("/api/deviceProfileInfos?", + new TypeReference>() { + }, pageLink); + Assert.assertNotNull("Device Profile Infos page data is null!", deviceProfileInfos); + Assert.assertEquals("Device Profile Infos Page data is empty! Expected to have default profile created!", 1, deviceProfileInfos.getTotalElements()); + List expectedDeviceProfileNames = deviceProfileInfos.getData().stream() + .map(info -> new EntityInfo(info.getId(), info.getName())) + .sorted(Comparator.comparing(EntityInfo::getName)) + .collect(Collectors.toList()); + var deviceProfileNames = doGetTyped("/api/deviceProfile/names", new TypeReference>() { + }); + Assert.assertNotNull("Device Profile Names list is null!", deviceProfileNames); + Assert.assertFalse("Device Profile Names list is empty!", deviceProfileNames.isEmpty()); + Assert.assertEquals(expectedDeviceProfileNames, deviceProfileNames); + Assert.assertEquals(1, deviceProfileNames.size()); + Assert.assertEquals(DEFAULT_DEVICE_TYPE, deviceProfileNames.get(0).getName()); + + int count = 3; + for (int i = 0; i < count; i++) { + Device device = new Device(); + device.setName("DeviceName" + i); + device.setType("DeviceProfileName" + i); + Device savedDevice = doPost("/api/device", device, Device.class); + Assert.assertNotNull(savedDevice); + } + deviceProfileInfos = doGetTypedWithPageLink("/api/deviceProfileInfos?", + new TypeReference<>() { + }, pageLink); + Assert.assertNotNull("Device Profile Infos page data is null!", deviceProfileInfos); + Assert.assertEquals("Device Profile Infos Page data is empty! Expected to have default profile created + count value!", 1 + count, deviceProfileInfos.getTotalElements()); + expectedDeviceProfileNames = deviceProfileInfos.getData().stream() + .map(info -> new EntityInfo(info.getId(), info.getName())) + .sorted(Comparator.comparing(EntityInfo::getName)) + .collect(Collectors.toList()); + + deviceProfileNames = doGetTyped("/api/deviceProfile/names", new TypeReference<>() { + }); + Assert.assertNotNull("Device Profile Names list is null!", deviceProfileNames); + Assert.assertFalse("Device Profile Names list is empty!", deviceProfileNames.isEmpty()); + Assert.assertEquals(expectedDeviceProfileNames, deviceProfileNames); + Assert.assertEquals(1 + count, deviceProfileNames.size()); + + deviceProfileNames = doGetTyped("/api/deviceProfile/names?activeOnly=true", new TypeReference<>() { + }); + Assert.assertNotNull("Device Profile Names list is null!", deviceProfileNames); + Assert.assertFalse("Device Profile Names list is empty!", deviceProfileNames.isEmpty()); + var expectedDeviceProfileNamesWithoutDefault = expectedDeviceProfileNames.stream() + .filter(entityInfo -> !entityInfo.getName().equals(DEFAULT_DEVICE_TYPE)) + .collect(Collectors.toList()); + Assert.assertEquals(expectedDeviceProfileNamesWithoutDefault, deviceProfileNames); + Assert.assertEquals(count, deviceProfileNames.size()); + } + private DeviceProfile savedDeviceProfile(String name) { DeviceProfile deviceProfile = createDeviceProfile(name); return doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java index 477c352b54..7d7769411e 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.asset; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetProfileInfo; import org.thingsboard.server.common.data.id.AssetProfileId; @@ -23,6 +24,8 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.EntityDaoService; +import java.util.List; + public interface AssetProfileService extends EntityDaoService { AssetProfile findAssetProfileById(TenantId tenantId, AssetProfileId assetProfileId); @@ -57,4 +60,6 @@ public interface AssetProfileService extends EntityDaoService { void deleteAssetProfilesByTenantId(TenantId tenantId); + List findAssetProfileNamesByTenantId(TenantId tenantId, boolean activeOnly); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java index 722eeff365..cec8dd9007 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java @@ -81,6 +81,7 @@ public interface AssetService extends EntityDaoService { ListenableFuture> findAssetsByQuery(TenantId tenantId, AssetSearchQuery query); + @Deprecated(since = "3.6.2", forRemoval = true) ListenableFuture> findAssetTypesByTenantId(TenantId tenantId); Asset assignAssetToEdge(TenantId tenantId, AssetId assetId, EdgeId edgeId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java index 5d325aa1a6..066254d28d 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java @@ -17,12 +17,15 @@ package org.thingsboard.server.dao.device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileInfo; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.EntityDaoService; +import java.util.List; + public interface DeviceProfileService extends EntityDaoService { DeviceProfile findDeviceProfileById(TenantId tenantId, DeviceProfileId deviceProfileId); @@ -59,4 +62,6 @@ public interface DeviceProfileService extends EntityDaoService { void deleteDeviceProfilesByTenantId(TenantId tenantId); + List findDeviceProfileNamesByTenantId(TenantId tenantId, boolean activeOnly); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java index a90ea9a572..0bb0dbf50e 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java @@ -95,6 +95,7 @@ public interface DeviceService extends EntityDaoService { ListenableFuture> findDevicesByQuery(TenantId tenantId, DeviceSearchQuery query); + @Deprecated(since = "3.6.2", forRemoval = true) ListenableFuture> findDeviceTypesByTenantId(TenantId tenantId); Device assignDeviceToTenant(TenantId tenantId, Device device); diff --git a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java index 76f5a1948b..e7c2c9d42d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java @@ -19,6 +19,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.util.CollectionUtils; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.TenantId; @@ -163,4 +164,17 @@ public abstract class DaoUtil { } return list; } + + public static List convertTenantEntityInfosToDto(UUID tenantId, EntityType entityType, List entityInfos) { + if (CollectionUtils.isEmpty(entityInfos)) { + return Collections.emptyList(); + } + + List list = new ArrayList<>(entityInfos.size()); + for (var info : entityInfos) { + list.add(new EntitySubtype(TenantId.fromUUID(tenantId), entityType, info.getName())); + } + return list; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java index 37d5dcea7c..4cdb64348b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java @@ -24,10 +24,8 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.util.TbPair; -import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ExportableEntityDao; -import org.thingsboard.server.dao.ImageContainerDao; import org.thingsboard.server.dao.TenantEntityDao; import java.util.List; @@ -191,6 +189,7 @@ public interface AssetDao extends Dao, TenantEntityDao, ExportableEntityD * * @return the list of tenant asset type objects */ + @Deprecated(since = "3.6.2", forRemoval = true) ListenableFuture> findTenantAssetTypesAsync(UUID tenantId); Long countAssetsByAssetProfileId(TenantId tenantId, UUID assetProfileId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileDao.java index 87e9177d66..7dc188000c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileDao.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.asset; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetProfileInfo; import org.thingsboard.server.common.data.id.AssetProfileId; @@ -25,6 +26,7 @@ import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ExportableEntityDao; import org.thingsboard.server.dao.ImageContainerDao; +import java.util.List; import java.util.UUID; public interface AssetProfileDao extends Dao, ExportableEntityDao, ImageContainerDao { @@ -47,4 +49,6 @@ public interface AssetProfileDao extends Dao, ExportableEntityDao< PageData findAllWithImages(PageLink pageLink); + List findTenantAssetProfileNames(UUID tenantId, boolean activeOnly); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java index 86cafcd10b..0b9e949c7c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java @@ -21,6 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.asset.Asset; @@ -42,9 +43,11 @@ import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -318,7 +321,16 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService tenantAssetProfilesRemover = + @Override + public List findAssetProfileNamesByTenantId(TenantId tenantId, boolean activeOnly) { + log.trace("Executing findAssetProfileNamesByTenantId, tenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + return assetProfileDao.findTenantAssetProfileNames(tenantId.getId(), activeOnly) + .stream().sorted(Comparator.comparing(EntityInfo::getName)) + .collect(Collectors.toList()); + } + + private final PaginatedRemover tenantAssetProfilesRemover = new PaginatedRemover<>() { @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java index e6b19b271d..1638c7d872 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java @@ -162,6 +162,7 @@ public interface DeviceDao extends Dao, TenantEntityDao, ExportableEntit * * @return the list of tenant device type objects */ + @Deprecated(since = "3.6.2", forRemoval = true) ListenableFuture> findTenantDeviceTypesAsync(UUID tenantId); /** diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java index 7bca5ebf8f..95336e8f4a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileInfo; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -25,6 +26,7 @@ import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ExportableEntityDao; import org.thingsboard.server.dao.ImageContainerDao; +import java.util.List; import java.util.UUID; public interface DeviceProfileDao extends Dao, ExportableEntityDao, ImageContainerDao { @@ -49,4 +51,6 @@ public interface DeviceProfileDao extends Dao, ExportableEntityDa PageData findAllWithImages(PageLink pageLink); + List findTenantDeviceProfileNames(UUID tenantId, boolean activeOnly); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index 847811cd4f..7e3567c888 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.DeviceProfileInfo; import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; @@ -57,11 +58,13 @@ import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateString; @@ -373,7 +376,16 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService tenantDeviceProfilesRemover = + @Override + public List findDeviceProfileNamesByTenantId(TenantId tenantId, boolean activeOnly) { + log.trace("Executing findDeviceProfileNamesByTenantId, tenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + return deviceProfileDao.findTenantDeviceProfileNames(tenantId.getId(), activeOnly) + .stream().sorted(Comparator.comparing(EntityInfo::getName)) + .collect(Collectors.toList()); + } + + private final PaginatedRemover tenantDeviceProfilesRemover = new PaginatedRemover<>() { @Override @@ -422,7 +434,8 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService 1) { return EncryptionUtil.certTrimNewLinesForChainInDeviceProfile(certificateValue); } - } catch (CertificateException ignored) {} + } catch (CertificateException ignored) { + } return EncryptionUtil.certTrimNewLines(certificateValue); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java index a9acef8608..08ea3c0872 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java @@ -20,6 +20,7 @@ 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.common.data.EntityInfo; import org.thingsboard.server.common.data.asset.AssetProfileInfo; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.AssetProfileEntity; @@ -71,4 +72,12 @@ public interface AssetProfileRepository extends JpaRepository findAllByImageNotNull(Pageable pageable); + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(ap.id, 'ASSET_PROFILE', ap.name) " + + "FROM AssetProfileEntity ap WHERE ap.tenantId = :tenantId AND EXISTS (SELECT 1 FROM AssetEntity a WHERE a.tenantId = :tenantId AND a.assetProfileId = ap.id)") + List findActiveTenantAssetProfileNames(@Param("tenantId") UUID tenantId); + + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(a.id, 'ASSET_PROFILE', a.name) " + + "FROM AssetProfileEntity a WHERE a.tenantId = :tenantId") + List findAllTenantAssetProfileNames(@Param("tenantId") UUID tenantId); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java index c16afc9983..0c2db8ec66 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java @@ -167,9 +167,6 @@ public interface AssetRepository extends JpaRepository, Expor @Param("textSearch") String textSearch, Pageable pageable); - @Query("SELECT DISTINCT a.type FROM AssetEntity a WHERE a.tenantId = :tenantId") - List findTenantAssetTypes(@Param("tenantId") UUID tenantId); - Long countByAssetProfileId(UUID assetProfileId); @Query("SELECT a FROM AssetEntity a, RelationEntity re WHERE a.tenantId = :tenantId " + diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java index e2fe60945d..48dd0996de 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java @@ -39,11 +39,10 @@ import org.thingsboard.server.dao.util.SqlDao; import java.util.Arrays; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.UUID; -import static org.thingsboard.server.dao.DaoUtil.convertTenantEntityTypesToDto; +import static org.thingsboard.server.dao.DaoUtil.convertTenantEntityInfosToDto; import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE; /** @@ -57,6 +56,9 @@ public class JpaAssetDao extends JpaAbstractDao implements A @Autowired private AssetRepository assetRepository; + @Autowired + private AssetProfileRepository assetProfileRepository; + @Override protected Class getEntityClass() { return AssetEntity.class; @@ -193,7 +195,7 @@ public class JpaAssetDao extends JpaAbstractDao implements A @Override public ListenableFuture> findTenantAssetTypesAsync(UUID tenantId) { - return service.submit(() -> convertTenantEntityTypesToDto(tenantId, EntityType.ASSET, assetRepository.findTenantAssetTypes(tenantId))); + return service.submit(() -> convertTenantEntityInfosToDto(tenantId, EntityType.ASSET, assetProfileRepository.findActiveTenantAssetProfileNames(tenantId))); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java index b57e5c0c40..d409b2c4d2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java @@ -20,6 +20,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetProfileInfo; @@ -103,6 +104,13 @@ public class JpaAssetProfileDao extends JpaAbstractDao findTenantAssetProfileNames(UUID tenantId, boolean activeOnly) { + return activeOnly ? + assetProfileRepository.findActiveTenantAssetProfileNames(tenantId) : + assetProfileRepository.findAllTenantAssetProfileNames(tenantId); + } + @Override public AssetProfile findByTenantIdAndExternalId(UUID tenantId, UUID externalId) { return DaoUtil.getData(assetProfileRepository.findByTenantIdAndExternalId(tenantId, externalId)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java index a9baf60e60..2093404bb0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java @@ -22,7 +22,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.thingsboard.server.common.data.DeviceProfileInfo; import org.thingsboard.server.common.data.DeviceTransportType; -import org.thingsboard.server.common.data.asset.AssetProfileInfo; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.DeviceProfileEntity; @@ -83,4 +83,12 @@ public interface DeviceProfileRepository extends JpaRepository findAllByImageNotNull(Pageable pageable); + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(dp.id, 'DEVICE_PROFILE', dp.name) " + + "FROM DeviceProfileEntity dp WHERE dp.tenantId = :tenantId AND EXISTS (SELECT 1 FROM DeviceEntity dv WHERE dv.tenantId = :tenantId AND dv.deviceProfileId = dp.id)") + List findActiveTenantDeviceProfileNames(@Param("tenantId") UUID tenantId); + + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(d.id, 'DEVICE_PROFILE', d.name) " + + "FROM DeviceProfileEntity d WHERE d.tenantId = :tenantId") + List findAllTenantDeviceProfileNames(@Param("tenantId") UUID tenantId); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java index 3ca96e7087..97ddb94b88 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java @@ -145,9 +145,6 @@ public interface DeviceRepository extends JpaRepository, Exp @Param("textSearch") String textSearch, Pageable pageable); - @Query("SELECT DISTINCT d.type FROM DeviceEntity d WHERE d.tenantId = :tenantId") - List findTenantDeviceTypes(@Param("tenantId") UUID tenantId); - DeviceEntity findByTenantIdAndName(UUID tenantId, String name); List findDevicesByTenantIdAndCustomerIdAndIdIn(UUID tenantId, UUID customerId, List deviceIds); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java index bf7d8670b1..d022c42af6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java @@ -44,11 +44,10 @@ import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.UUID; -import static org.thingsboard.server.dao.DaoUtil.convertTenantEntityTypesToDto; +import static org.thingsboard.server.dao.DaoUtil.convertTenantEntityInfosToDto; /** * Created by Valerii Sosliuk on 5/6/2017. @@ -64,6 +63,9 @@ public class JpaDeviceDao extends JpaAbstractDao implement @Autowired private NativeDeviceRepository nativeDeviceRepository; + @Autowired + private DeviceProfileRepository deviceProfileRepository; + @Override protected Class getEntityClass() { return DeviceEntity.class; @@ -217,7 +219,7 @@ public class JpaDeviceDao extends JpaAbstractDao implement @Override public ListenableFuture> findTenantDeviceTypesAsync(UUID tenantId) { - return service.submit(() -> convertTenantEntityTypesToDto(tenantId, EntityType.DEVICE, deviceRepository.findTenantDeviceTypes(tenantId))); + return service.submit(() -> convertTenantEntityInfosToDto(tenantId, EntityType.DEVICE, deviceProfileRepository.findActiveTenantDeviceProfileNames(tenantId))); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java index 12a91e3cf7..9109f223a9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java @@ -23,6 +23,7 @@ import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileInfo; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -121,6 +122,13 @@ public class JpaDeviceProfileDao extends JpaAbstractDao findTenantDeviceProfileNames(UUID tenantId, boolean activeOnly) { + return activeOnly ? + deviceProfileRepository.findActiveTenantDeviceProfileNames(tenantId) : + deviceProfileRepository.findAllTenantDeviceProfileNames(tenantId); + } + @Override public DeviceProfile findByTenantIdAndExternalId(UUID tenantId, UUID externalId) { return DaoUtil.getData(deviceProfileRepository.findByTenantIdAndExternalId(tenantId, externalId)); 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 fb878a33b6..8b17cef6c1 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 @@ -505,7 +505,7 @@ public class RestClient implements Closeable { HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { - }).getBody(); + }).getBody(); } public AlarmComment saveAlarmComment(AlarmId alarmId, AlarmComment alarmComment) { @@ -703,6 +703,7 @@ public class RestClient implements Closeable { }).getBody(); } + @Deprecated(since = "3.6.2") public List getAssetTypes() { return restTemplate.exchange(URI.create( baseURL + "/api/asset/types"), @@ -712,6 +713,15 @@ public class RestClient implements Closeable { }).getBody(); } + public List getAssetProfileNames(boolean activeOnly) { + return restTemplate.exchange( + baseURL + "/api/assetProfile/names?activeOnly={activeOnly}", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, activeOnly).getBody(); + } + public BulkImportResult processAssetsBulkImport(BulkImportRequest request) { return restTemplate.exchange( baseURL + "/api/asset/bulk_import", @@ -1394,6 +1404,7 @@ public class RestClient implements Closeable { }).getBody(); } + @Deprecated(since = "3.6.2") public List getDeviceTypes() { return restTemplate.exchange( baseURL + "/api/device/types", @@ -1403,6 +1414,15 @@ public class RestClient implements Closeable { }).getBody(); } + public List getDeviceProfileNames(boolean activeOnly) { + return restTemplate.exchange( + baseURL + "/api/deviceProfile/names?activeOnly={activeOnly}", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, activeOnly).getBody(); + } + public JsonNode claimDevice(String deviceName, ClaimRequest claimRequest) { return restTemplate.exchange( baseURL + "/api/customer/device/{deviceName}/claim", From a8723f07a8f32f4c402a305b9f170c964a054dc2 Mon Sep 17 00:00:00 2001 From: dudnikmaksim Date: Wed, 6 Dec 2023 19:03:44 +0200 Subject: [PATCH 19/38] Invalid log message format format --- .../widget/lib/gateway/gateway-logs.component.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.ts index 6e0f20bdc3..721cea4ae6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.ts @@ -114,10 +114,16 @@ export class GatewayLogsComponent implements AfterViewInit { const result = { ts: data[0], key: this.activeLink.key, - message: /\[(.*)/.exec(data[1])[0], + message: data[1], status: 'INVALID LOG FORMAT' as GatewayStatus }; + try { + result.message = /\[(.*)/.exec(data[1])[0]; + } catch (e) { + result.message = data[1]; + } + try { result.status = data[1].match(/\|(\w+)\|/)[1]; } catch (e) { From 220b2ee8e3a66a40d4253c07d87bf4c40a3d829b Mon Sep 17 00:00:00 2001 From: rusikv Date: Thu, 7 Dec 2023 15:08:49 +0200 Subject: [PATCH 20/38] UI: increased page size for rule-chain-select --- .../shared/components/rule-chain/rule-chain-select.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.ts b/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.ts index 003d85b751..1610db23eb 100644 --- a/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.ts +++ b/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.ts @@ -65,7 +65,7 @@ export class RuleChainSelectComponent implements ControlValueAccessor, OnInit { } ngOnInit() { - const pageLink = new PageLink(100, 0, null, { + const pageLink = new PageLink(1000, 0, null, { property: 'name', direction: Direction.ASC }); From d08f0467df9090aa2a336224dd012d1db226a88a Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 7 Dec 2023 16:42:47 +0200 Subject: [PATCH 21/38] UI: Fix selection in image gallery for system images. --- .../app/shared/components/image/image-gallery.component.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/components/image/image-gallery.component.ts b/ui-ngx/src/app/shared/components/image/image-gallery.component.ts index af223e8563..7d76c6bde8 100644 --- a/ui-ngx/src/app/shared/components/image/image-gallery.component.ts +++ b/ui-ngx/src/app/shared/components/image/image-gallery.component.ts @@ -612,7 +612,9 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe if (this.selectionMode) { this.selectImage($event, image); } else { - this.dataSource.selection.toggle(image); + if (this.deleteEnabled(image)) { + this.dataSource.selection.toggle(image); + } } } From c598b4fa9d19a899410188975defb5053900bb20 Mon Sep 17 00:00:00 2001 From: dudnikmaksim Date: Thu, 7 Dec 2023 16:58:19 +0200 Subject: [PATCH 22/38] default config --- .../lib/gateway/gateway-connectors.component.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts index 9fa1f7b5f2..cd54221e32 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts @@ -139,6 +139,21 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie this.cd.detectChanges(); }); + this.connectorForm.get('type').valueChanges.subscribe(type=> { + if(type && this.initialConnector === null) { + this.attributeService.getEntityAttributes(this.device, AttributeScope.CLIENT_SCOPE, + [`${type.toUpperCase()}_DEFAULT_CONFIG`], {ignoreErrors: true}).subscribe(defaultConfig=>{ + if (defaultConfig && defaultConfig.length) { + this.connectorForm.get('configurationJson').setValue( + isString(defaultConfig[0].value) ? + JSON.parse(defaultConfig[0].value) : + defaultConfig[0].value); + this.cd.detectChanges(); + } + }) + } + }); + this.dataSource.sort = this.sort; this.dataSource.sortingDataAccessor = (data: AttributeData, sortHeaderId: string) => { if (sortHeaderId === 'syncStatus') { From 0a9b21ce157ae019f224cde8bbf84fb7f6c660f9 Mon Sep 17 00:00:00 2001 From: Dmitriymush Date: Thu, 7 Dec 2023 17:02:23 +0200 Subject: [PATCH 23/38] UI: added additional check for string and empty string in value card widget --- .../widget/lib/cards/value-card-widget.component.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts index bce2f0b506..77228677aa 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts @@ -27,7 +27,7 @@ import { ViewChild } from '@angular/core'; import { WidgetContext } from '@home/models/widget-component.models'; -import { formatValue, isDefinedAndNotNull } from '@core/utils'; +import { formatValue, isDefinedAndNotNull, isNotEmptyStr, isString } from '@core/utils'; import { backgroundStyle, ColorProcessor, @@ -183,7 +183,10 @@ export class ValueCardWidgetComponent implements OnInit, AfterViewInit, OnDestro const tsValue = getSingleTsValue(this.ctx.data); let ts; let value; - if (tsValue && isDefinedAndNotNull(tsValue[1])) { + if ( + tsValue && isDefinedAndNotNull(tsValue[1]) && + (!isString(tsValue[1]) || isNotEmptyStr(tsValue[1])) + ) { ts = tsValue[0]; value = tsValue[1]; this.valueText = formatValue(value, this.decimals, this.units, false); From e9c5d815d98eb65b2c8104674c7c41746c737220 Mon Sep 17 00:00:00 2001 From: dudnikmaksim Date: Thu, 7 Dec 2023 17:23:48 +0200 Subject: [PATCH 24/38] default config --- .../widget/lib/gateway/gateway-connectors.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts index cd54221e32..6f1fc90f5e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts @@ -140,7 +140,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie }); this.connectorForm.get('type').valueChanges.subscribe(type=> { - if(type && this.initialConnector === null) { + if(type && !this.initialConnector) { this.attributeService.getEntityAttributes(this.device, AttributeScope.CLIENT_SCOPE, [`${type.toUpperCase()}_DEFAULT_CONFIG`], {ignoreErrors: true}).subscribe(defaultConfig=>{ if (defaultConfig && defaultConfig.length) { @@ -322,6 +322,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie } private clearOutConnectorForm(): void { + this.initialConnector = null; this.connectorForm.setValue({ name: '', type: 'mqtt', @@ -331,7 +332,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie configuration: '', configurationJson: {} }); - this.initialConnector = null; this.connectorForm.markAsPristine(); } From c1262bd3a818f2554c991ceb9b6310ce720b7944 Mon Sep 17 00:00:00 2001 From: Dmitriymush Date: Thu, 7 Dec 2023 17:28:29 +0200 Subject: [PATCH 25/38] UI: refactored check for value card widget --- .../widget/lib/cards/value-card-widget.component.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts index 77228677aa..27899f6e83 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts @@ -27,7 +27,7 @@ import { ViewChild } from '@angular/core'; import { WidgetContext } from '@home/models/widget-component.models'; -import { formatValue, isDefinedAndNotNull, isNotEmptyStr, isString } from '@core/utils'; +import { formatValue, isDefinedAndNotNull } from '@core/utils'; import { backgroundStyle, ColorProcessor, @@ -183,10 +183,7 @@ export class ValueCardWidgetComponent implements OnInit, AfterViewInit, OnDestro const tsValue = getSingleTsValue(this.ctx.data); let ts; let value; - if ( - tsValue && isDefinedAndNotNull(tsValue[1]) && - (!isString(tsValue[1]) || isNotEmptyStr(tsValue[1])) - ) { + if (tsValue && isDefinedAndNotNull(tsValue[1]) && tsValue[0] !== 0) { ts = tsValue[0]; value = tsValue[1]; this.valueText = formatValue(value, this.decimals, this.units, false); From 54914f9851d54ed678106e59a0febef3bb984505 Mon Sep 17 00:00:00 2001 From: rusikv Date: Thu, 7 Dec 2023 17:31:49 +0200 Subject: [PATCH 26/38] UI: change page size for rule-chain-select to 1024 --- .../shared/components/rule-chain/rule-chain-select.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.ts b/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.ts index 1610db23eb..d9f2f90cc8 100644 --- a/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.ts +++ b/ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.ts @@ -65,7 +65,7 @@ export class RuleChainSelectComponent implements ControlValueAccessor, OnInit { } ngOnInit() { - const pageLink = new PageLink(1000, 0, null, { + const pageLink = new PageLink(1024, 0, null, { property: 'name', direction: Direction.ASC }); From f7af8e298ccf8eb82093172ed581bc5f5136914f Mon Sep 17 00:00:00 2001 From: ShvaykaD Date: Thu, 7 Dec 2023 17:41:29 +0200 Subject: [PATCH 27/38] added fixes after review --- .../install/SqlDatabaseUpgradeService.java | 4 + .../org/thingsboard/server/dao/DaoUtil.java | 24 ++--- .../dao/asset/AssetProfileServiceImpl.java | 6 +- .../server/dao/asset/BaseAssetService.java | 15 ++- .../dao/device/DeviceProfileServiceImpl.java | 6 +- .../server/dao/device/DeviceServiceImpl.java | 7 +- .../dao/sql/asset/AssetProfileRepository.java | 5 +- .../sql/device/DeviceProfileRepository.java | 5 +- .../resources/sql/schema-entities-idx.sql | 2 + .../dao/service/AssetProfileServiceTest.java | 97 +++++++++++++++++++ .../dao/service/DeviceProfileServiceTest.java | 94 ++++++++++++++++++ 11 files changed, 223 insertions(+), 42 deletions(-) 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 f404edb8b1..c2444390f6 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 @@ -766,6 +766,10 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService } catch (Exception e) { log.warn("Failed to execute update script for save attributes rule nodes due to: ", e); } + try { + connection.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_asset_profile_id ON asset(tenant_id, asset_profile_id);"); + } catch (Exception e) { + } }); break; default: diff --git a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java index e7c2c9d42d..1d9b276f65 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java @@ -153,28 +153,24 @@ public abstract class DaoUtil { } } - public static List convertTenantEntityTypesToDto(UUID tenantId, EntityType entityType, List types) { + public static List convertTenantEntityTypesToDto(UUID tenantUUID, EntityType entityType, List types) { if (CollectionUtils.isEmpty(types)) { return Collections.emptyList(); } - - List list = new ArrayList<>(types.size()); - for (String type : types) { - list.add(new EntitySubtype(TenantId.fromUUID(tenantId), entityType, type)); - } - return list; + TenantId tenantId = TenantId.fromUUID(tenantUUID); + return types.stream() + .map(type -> new EntitySubtype(tenantId, entityType, type)) + .collect(Collectors.toList()); } - public static List convertTenantEntityInfosToDto(UUID tenantId, EntityType entityType, List entityInfos) { + public static List convertTenantEntityInfosToDto(UUID tenantUUID, EntityType entityType, List entityInfos) { if (CollectionUtils.isEmpty(entityInfos)) { return Collections.emptyList(); } - - List list = new ArrayList<>(entityInfos.size()); - for (var info : entityInfos) { - list.add(new EntitySubtype(TenantId.fromUUID(tenantId), entityType, info.getName())); - } - return list; + var tenantId = TenantId.fromUUID(tenantUUID); + return entityInfos.stream() + .map(info -> new EntitySubtype(tenantId, entityType, info.getName())) + .collect(Collectors.toList()); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java index 0b9e949c7c..a46978bec1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java @@ -43,11 +43,9 @@ import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import java.util.ArrayList; -import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.stream.Collectors; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -325,9 +323,7 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService findAssetProfileNamesByTenantId(TenantId tenantId, boolean activeOnly) { log.trace("Executing findAssetProfileNamesByTenantId, tenantId [{}]", tenantId); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - return assetProfileDao.findTenantAssetProfileNames(tenantId.getId(), activeOnly) - .stream().sorted(Comparator.comparing(EntityInfo::getName)) - .collect(Collectors.toList()); + return assetProfileDao.findTenantAssetProfileNames(tenantId.getId(), activeOnly); } private final PaginatedRemover tenantAssetProfilesRemover = diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java index 0431b48cea..6de1158cf6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java @@ -56,7 +56,6 @@ import org.thingsboard.server.dao.service.PaginatedRemover; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -363,7 +362,12 @@ public class BaseAssetService extends AbstractCachedEntityService - assetList == null ? Collections.emptyList() : assetList.stream().filter(asset -> query.getAssetTypes().contains(asset.getType())).collect(Collectors.toList()), MoreExecutors.directExecutor() + assetList == null ? + Collections.emptyList() : + assetList.stream() + .filter(asset -> query.getAssetTypes().contains(asset.getType())) + .collect(Collectors.toList()), + MoreExecutors.directExecutor() ); return assets; } @@ -372,12 +376,7 @@ public class BaseAssetService extends AbstractCachedEntityService> findAssetTypesByTenantId(TenantId tenantId) { log.trace("Executing findAssetTypesByTenantId, tenantId [{}]", tenantId); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - ListenableFuture> tenantAssetTypes = assetDao.findTenantAssetTypesAsync(tenantId.getId()); - return Futures.transform(tenantAssetTypes, - assetTypes -> { - assetTypes.sort(Comparator.comparing(EntitySubtype::getType)); - return assetTypes; - }, MoreExecutors.directExecutor()); + return assetDao.findTenantAssetTypesAsync(tenantId.getId()); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index 7e3567c888..c67babdab0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -58,13 +58,11 @@ import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.util.ArrayList; -import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateString; @@ -380,9 +378,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService findDeviceProfileNamesByTenantId(TenantId tenantId, boolean activeOnly) { log.trace("Executing findDeviceProfileNamesByTenantId, tenantId [{}]", tenantId); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - return deviceProfileDao.findTenantDeviceProfileNames(tenantId.getId(), activeOnly) - .stream().sorted(Comparator.comparing(EntityInfo::getName)) - .collect(Collectors.toList()); + return deviceProfileDao.findTenantDeviceProfileNames(tenantId.getId(), activeOnly); } private final PaginatedRemover tenantDeviceProfilesRemover = diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index cfc30ccd8f..fcedc94c9c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -486,12 +486,7 @@ public class DeviceServiceImpl extends AbstractCachedEntityService> findDeviceTypesByTenantId(TenantId tenantId) { log.trace("Executing findDeviceTypesByTenantId, tenantId [{}]", tenantId); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - ListenableFuture> tenantDeviceTypes = deviceDao.findTenantDeviceTypesAsync(tenantId.getId()); - return Futures.transform(tenantDeviceTypes, - deviceTypes -> { - deviceTypes.sort(Comparator.comparing(EntitySubtype::getType)); - return deviceTypes; - }, MoreExecutors.directExecutor()); + return deviceDao.findTenantDeviceTypesAsync(tenantId.getId()); } @Transactional diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java index 08ea3c0872..faf4510879 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java @@ -73,11 +73,12 @@ public interface AssetProfileRepository extends JpaRepository findAllByImageNotNull(Pageable pageable); @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(ap.id, 'ASSET_PROFILE', ap.name) " + - "FROM AssetProfileEntity ap WHERE ap.tenantId = :tenantId AND EXISTS (SELECT 1 FROM AssetEntity a WHERE a.tenantId = :tenantId AND a.assetProfileId = ap.id)") + "FROM AssetProfileEntity ap WHERE ap.tenantId = :tenantId AND EXISTS " + + "(SELECT 1 FROM AssetEntity a WHERE a.tenantId = :tenantId AND a.assetProfileId = ap.id) ORDER BY ap.name ASC") List findActiveTenantAssetProfileNames(@Param("tenantId") UUID tenantId); @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(a.id, 'ASSET_PROFILE', a.name) " + - "FROM AssetProfileEntity a WHERE a.tenantId = :tenantId") + "FROM AssetProfileEntity a WHERE a.tenantId = :tenantId ORDER BY a.name ASC") List findAllTenantAssetProfileNames(@Param("tenantId") UUID tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java index 2093404bb0..cc50b06e6e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java @@ -84,11 +84,12 @@ public interface DeviceProfileRepository extends JpaRepository findAllByImageNotNull(Pageable pageable); @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(dp.id, 'DEVICE_PROFILE', dp.name) " + - "FROM DeviceProfileEntity dp WHERE dp.tenantId = :tenantId AND EXISTS (SELECT 1 FROM DeviceEntity dv WHERE dv.tenantId = :tenantId AND dv.deviceProfileId = dp.id)") + "FROM DeviceProfileEntity dp WHERE dp.tenantId = :tenantId AND EXISTS " + + "(SELECT 1 FROM DeviceEntity dv WHERE dv.tenantId = :tenantId AND dv.deviceProfileId = dp.id) ORDER BY dp.name ASC") List findActiveTenantDeviceProfileNames(@Param("tenantId") UUID tenantId); @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(d.id, 'DEVICE_PROFILE', d.name) " + - "FROM DeviceProfileEntity d WHERE d.tenantId = :tenantId") + "FROM DeviceProfileEntity d WHERE d.tenantId = :tenantId ORDER BY d.name ASC") List findAllTenantDeviceProfileNames(@Param("tenantId") UUID tenantId); } diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index 472c0f601e..91f3eff713 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -57,6 +57,8 @@ CREATE INDEX IF NOT EXISTS idx_asset_customer_id_and_type ON asset(tenant_id, cu CREATE INDEX IF NOT EXISTS idx_asset_type ON asset(tenant_id, type); +CREATE INDEX IF NOT EXISTS idx_asset_profile_id ON asset(tenant_id, asset_profile_id); + CREATE INDEX IF NOT EXISTS idx_attribute_kv_by_key_and_last_update_ts ON attribute_kv(entity_id, attribute_key, last_update_ts desc); CREATE INDEX IF NOT EXISTS idx_audit_log_tenant_id_and_created_time ON audit_log(tenant_id, created_time DESC); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AssetProfileServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AssetProfileServiceTest.java index 391c00fa49..0d6f0d8075 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AssetProfileServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AssetProfileServiceTest.java @@ -24,6 +24,7 @@ import org.junit.Test; import org.junit.jupiter.api.Assertions; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetProfileInfo; @@ -35,11 +36,14 @@ import org.thingsboard.server.dao.exception.DataValidationException; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.stream.Collectors; +import static org.assertj.core.api.Assertions.assertThat; + @DaoSqlTest public class AssetProfileServiceTest extends AbstractServiceTest { @@ -272,4 +276,97 @@ public class AssetProfileServiceTest extends AbstractServiceTest { Assert.assertEquals(1, pageData.getTotalElements()); } + @Test + public void testFindAllassetProfilesByTenantId() { + int assetProfilesCount = 4; // 3 created + default + var assetProfiles = new ArrayList(4); + + var profileC = assetProfileService.saveAssetProfile( + createAssetProfile(tenantId, "profile C")); + assetProfiles.add(assetProfileService.saveAssetProfile(profileC)); + + + var profileA = assetProfileService.saveAssetProfile( + createAssetProfile(tenantId, "profile A")); + assetProfiles.add(assetProfileService.saveAssetProfile(profileA)); + + + var profileB = assetProfileService.saveAssetProfile( + createAssetProfile(tenantId, "profile B")); + assetProfiles.add(assetProfileService.saveAssetProfile(profileB)); + + + assetProfiles.add(assetProfileService.findDefaultAssetProfile(tenantId)); + + List sortedProfileInfos = assetProfiles.stream() + .map(profile -> new EntityInfo(profile.getId(), profile.getName())) + .sorted(Comparator.comparing(EntityInfo::getName)) + .collect(Collectors.toList()); + + var assetProfileInfos = assetProfileService + .findAssetProfileNamesByTenantId(tenantId, false); + + assertThat(assetProfileInfos).isNotNull(); + assertThat(assetProfileInfos).hasSize(assetProfilesCount); + assertThat(assetProfileInfos).isEqualTo(sortedProfileInfos); + } + + @Test + public void testFindActiveOnlyassetProfilesByTenantId() { + + String profileCName = "profile C"; + assetProfileService.saveAssetProfile( + createAssetProfile(tenantId, profileCName)); + + String profileAName = "profile A"; + assetProfileService.saveAssetProfile( + createAssetProfile(tenantId, profileAName)); + + String profileBName = "profile B"; + assetProfileService.saveAssetProfile( + createAssetProfile(tenantId, profileBName)); + + + var assetProfileInfos = assetProfileService + .findAssetProfileNamesByTenantId(tenantId, true); + + assertThat(assetProfileInfos).isNotNull(); + assertThat(assetProfileInfos).isEmpty(); + + var assetC = new Asset(); + assetC.setName("Test asset C"); + assetC.setType(profileCName); + assetC.setTenantId(tenantId); + + assetC = assetService.saveAsset(assetC); + + var assetA = new Asset(); + assetA.setName("Test asset A"); + assetA.setType(profileAName); + assetA.setTenantId(tenantId); + + assetA = assetService.saveAsset(assetA); + + var assetB = new Asset(); + assetB.setName("Test asset B"); + assetB.setType(profileBName); + assetB.setTenantId(tenantId); + + assetB = assetService.saveAsset(assetB); + + assetProfileInfos = assetProfileService + .findAssetProfileNamesByTenantId(tenantId, true); + + var expected = List.of( + new EntityInfo(assetA.getAssetProfileId(), profileAName), + new EntityInfo(assetB.getAssetProfileId(), profileBName), + new EntityInfo(assetC.getAssetProfileId(), profileCName) + ); + + assertThat(assetProfileInfos).isNotEmpty(); + assertThat(assetProfileInfos).hasSize(3); + assertThat(assetProfileInfos).isEqualTo(expected); + } + + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceProfileServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceProfileServiceTest.java index 16a80e438d..ac3b603f13 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceProfileServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceProfileServiceTest.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileInfo; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; import org.thingsboard.server.common.data.page.PageData; @@ -41,6 +42,7 @@ import org.thingsboard.server.dao.ota.OtaPackageService; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; @@ -372,4 +374,96 @@ public class DeviceProfileServiceTest extends AbstractServiceTest { Assert.assertEquals(1, pageData.getTotalElements()); } + @Test + public void testFindAllDeviceProfilesByTenantId() { + int deviceProfilesCount = 4; // 3 created + default + var deviceProfiles = new ArrayList(4); + + var profileC = deviceProfileService.saveDeviceProfile( + createDeviceProfile(tenantId, "profile C")); + deviceProfiles.add(deviceProfileService.saveDeviceProfile(profileC)); + + + var profileA = deviceProfileService.saveDeviceProfile( + createDeviceProfile(tenantId, "profile A")); + deviceProfiles.add(deviceProfileService.saveDeviceProfile(profileA)); + + + var profileB = deviceProfileService.saveDeviceProfile( + createDeviceProfile(tenantId, "profile B")); + deviceProfiles.add(deviceProfileService.saveDeviceProfile(profileB)); + + + deviceProfiles.add(deviceProfileService.findDefaultDeviceProfile(tenantId)); + + List sortedProfileInfos = deviceProfiles.stream() + .map(profile -> new EntityInfo(profile.getId(), profile.getName())) + .sorted(Comparator.comparing(EntityInfo::getName)) + .collect(Collectors.toList()); + + var deviceProfileInfos = deviceProfileService + .findDeviceProfileNamesByTenantId(tenantId, false); + + assertThat(deviceProfileInfos).isNotNull(); + assertThat(deviceProfileInfos).hasSize(deviceProfilesCount); + assertThat(deviceProfileInfos).isEqualTo(sortedProfileInfos); + } + + @Test + public void testFindActiveOnlyDeviceProfilesByTenantId() { + + String profileCName = "profile C"; + deviceProfileService.saveDeviceProfile( + createDeviceProfile(tenantId, profileCName)); + + String profileAName = "profile A"; + deviceProfileService.saveDeviceProfile( + createDeviceProfile(tenantId, profileAName)); + + String profileBName = "profile B"; + deviceProfileService.saveDeviceProfile( + createDeviceProfile(tenantId, profileBName)); + + + var deviceProfileInfos = deviceProfileService + .findDeviceProfileNamesByTenantId(tenantId, true); + + assertThat(deviceProfileInfos).isNotNull(); + assertThat(deviceProfileInfos).isEmpty(); + + var deviceC = new Device(); + deviceC.setName("Test Device C"); + deviceC.setType(profileCName); + deviceC.setTenantId(tenantId); + + deviceC = deviceService.saveDevice(deviceC); + + var deviceA = new Device(); + deviceA.setName("Test Device A"); + deviceA.setType(profileAName); + deviceA.setTenantId(tenantId); + + deviceA = deviceService.saveDevice(deviceA); + + var deviceB = new Device(); + deviceB.setName("Test Device B"); + deviceB.setType(profileBName); + deviceB.setTenantId(tenantId); + + deviceB = deviceService.saveDevice(deviceB); + + deviceProfileInfos = deviceProfileService + .findDeviceProfileNamesByTenantId(tenantId, true); + + var expected = List.of( + new EntityInfo(deviceA.getDeviceProfileId(), profileAName), + new EntityInfo(deviceB.getDeviceProfileId(), profileBName), + new EntityInfo(deviceC.getDeviceProfileId(), profileCName) + ); + + assertThat(deviceProfileInfos).isNotEmpty(); + assertThat(deviceProfileInfos).hasSize(3); + assertThat(deviceProfileInfos).isEqualTo(expected); + } + } From 4c8cf696326bd44482fa6cab0499b4471d773893 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 7 Dec 2023 17:27:34 +0100 Subject: [PATCH 28/38] TbSqlBlockingQueue failover improvement --- .../server/dao/sql/TbSqlBlockingQueue.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java index fd3822bb5f..940231aaaa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java @@ -54,7 +54,7 @@ public class TbSqlBlockingQueue implements TbSqlQueue { String logName = params.getLogName(); int batchSize = params.getBatchSize(); long maxDelay = params.getMaxDelay(); - List> entities = new ArrayList<>(batchSize); + final List> entities = new ArrayList<>(batchSize); while (!Thread.interrupted()) { try { long currentTs = System.currentTimeMillis(); @@ -83,19 +83,23 @@ public class TbSqlBlockingQueue implements TbSqlQueue { Thread.sleep(remainingDelay); } } - } catch (Exception e) { - stats.incrementFailed(entities.size()); - entities.forEach(entityFutureWrapper -> entityFutureWrapper.getFuture().setException(e)); - if (e instanceof InterruptedException) { + } catch (Throwable t) { + log.error("[{}] Failed to save {} entities", logName, entities.size(), t); + try { + stats.incrementFailed(entities.size()); + entities.forEach(entityFutureWrapper -> entityFutureWrapper.getFuture().setException(t)); + } catch (Throwable th) { + log.error("[{}] Failed to set future exception", logName, th); + } + if (t instanceof InterruptedException) { log.info("[{}] Queue polling was interrupted", logName); break; - } else { - log.error("[{}] Failed to save {} entities", logName, entities.size(), e); } } finally { entities.clear(); } } + log.info("[{}] Queue polling completed", logName); }); logExecutor.scheduleAtFixedRate(() -> { From b2f2d1d9093bdfc4f8c208681b6c4c6fe9ba8eee Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 8 Dec 2023 13:10:02 +0200 Subject: [PATCH 29/38] updated config parameter description --- application/src/main/resources/thingsboard.yml | 2 +- msa/vc-executor/src/main/resources/tb-vc-executor.yml | 2 +- transport/coap/src/main/resources/tb-coap-transport.yml | 2 +- transport/http/src/main/resources/tb-http-transport.yml | 2 +- transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml | 2 +- transport/mqtt/src/main/resources/tb-mqtt-transport.yml | 2 +- transport/snmp/src/main/resources/tb-snmp-transport.yml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 93348d02ee..9421dfa742 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1280,7 +1280,7 @@ swagger: # Queue configuration parameters queue: type: "${TB_QUEUE_TYPE:in-memory}" # in-memory or kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) - prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka) except of js executor topics (please use REMOTE_JS_EVAL_REQUEST_TOPIC and REMOTE_JS_EVAL_RESPONSE_TOPIC to specify custom topic names) + prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). in_memory: stats: # For debug level diff --git a/msa/vc-executor/src/main/resources/tb-vc-executor.yml b/msa/vc-executor/src/main/resources/tb-vc-executor.yml index 8c363b019c..645acfb164 100644 --- a/msa/vc-executor/src/main/resources/tb-vc-executor.yml +++ b/msa/vc-executor/src/main/resources/tb-vc-executor.yml @@ -48,7 +48,7 @@ zk: # Queue configuration parameters queue: type: "${TB_QUEUE_TYPE:kafka}" # in-memory or kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) - prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka) except of js executor topics (please use REMOTE_JS_EVAL_REQUEST_TOPIC and REMOTE_JS_EVAL_RESPONSE_TOPIC to specify custom topic names) + prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). in_memory: stats: # For debug lvl diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index a45b41803e..ab5deff3f7 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -198,7 +198,7 @@ transport: # Queue configuration parameters queue: type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) - prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka) except of js executor topics (please use REMOTE_JS_EVAL_REQUEST_TOPIC and REMOTE_JS_EVAL_RESPONSE_TOPIC to specify custom topic names) + prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). kafka: # Kafka Bootstrap Servers bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}" diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index 81fdf414bb..05231a6fb1 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -181,7 +181,7 @@ transport: # Queue configuration parameters queue: type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) - prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka) except of js executor topics (please use REMOTE_JS_EVAL_REQUEST_TOPIC and REMOTE_JS_EVAL_RESPONSE_TOPIC to specify custom topic names) + prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka) . kafka: # Kafka Bootstrap Servers bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}" diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index f68f158dc1..10dbac101c 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -277,7 +277,7 @@ transport: # Queue configuration properties queue: type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) - prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka) except of js executor topics (please use REMOTE_JS_EVAL_REQUEST_TOPIC and REMOTE_JS_EVAL_RESPONSE_TOPIC to specify custom topic names) + prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). kafka: # Kafka Bootstrap Servers bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}" diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index 16c2c019df..0eb8b4315b 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -214,7 +214,7 @@ transport: # Queue configuration parameters queue: type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) - prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka) except of js executor topics (please use REMOTE_JS_EVAL_REQUEST_TOPIC and REMOTE_JS_EVAL_RESPONSE_TOPIC to specify custom topic names) + prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). kafka: # Kafka Bootstrap Servers bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}" diff --git a/transport/snmp/src/main/resources/tb-snmp-transport.yml b/transport/snmp/src/main/resources/tb-snmp-transport.yml index 2b6cc17d9b..d139b641a6 100644 --- a/transport/snmp/src/main/resources/tb-snmp-transport.yml +++ b/transport/snmp/src/main/resources/tb-snmp-transport.yml @@ -160,7 +160,7 @@ transport: # Queue configuration parameters queue: type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) - prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka) except of js executor topics (please use REMOTE_JS_EVAL_REQUEST_TOPIC and REMOTE_JS_EVAL_RESPONSE_TOPIC to specify custom topic names) + prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). kafka: # Kafka Bootstrap Servers bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}" From 88d9128b965cee0475a078f114bc8be938594f95 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Fri, 8 Dec 2023 13:34:36 +0200 Subject: [PATCH 30/38] Use docker volume instead of bind mount --- .../install/docker/instructions.md | 31 ++++----- .../install/docker/localhost_warning.md | 3 - .../upgrade/docker/start_service.md | 10 +-- .../instructions/upgrade/docker/upgrade_db.md | 20 +++--- .../upgrade/docker/upgrade_preparing.md | 63 ++++++++++++++++++- .../instructions/upgrade/docker/upgrade_rm.md | 5 -- ...DefaultEdgeInstallInstructionsService.java | 11 ++-- ...DefaultEdgeUpgradeInstructionsService.java | 8 --- .../dao/util/DeviceConnectivityUtil.java | 2 +- 9 files changed, 99 insertions(+), 54 deletions(-) delete mode 100644 application/src/main/data/json/edge/instructions/install/docker/localhost_warning.md delete mode 100644 application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_rm.md diff --git a/application/src/main/data/json/edge/instructions/install/docker/instructions.md b/application/src/main/data/json/edge/instructions/install/docker/instructions.md index b07b128062..9910124b3e 100644 --- a/application/src/main/data/json/edge/instructions/install/docker/instructions.md +++ b/application/src/main/data/json/edge/instructions/install/docker/instructions.md @@ -4,22 +4,8 @@ Here is the list of commands, that can be used to quickly install ThingsBoard Ed 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 @@ -30,7 +16,7 @@ nano docker-compose.yml Add the following lines to the yml file: ```bash -version: '3.0' +version: '3.8' services: mytbedge: restart: always @@ -47,8 +33,9 @@ services: 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 + - tb-edge-data:/data + - tb-edge-logs:/var/log/tb-edge + ${EXTRA_HOSTS} postgres: restart: always image: "postgres:15" @@ -58,7 +45,15 @@ services: POSTGRES_DB: tb-edge POSTGRES_PASSWORD: postgres volumes: - - ~/.mytb-edge-data/db:/var/lib/postgresql/data + - tb-edge-postgres-data:/var/lib/postgresql/data + +volumes: + tb-edge-data: + name: tb-edge-data + tb-edge-logs: + name: tb-edge-logs + tb-edge-postgres-data: + name: tb-edge-postgres-data {:copy-code} ``` diff --git a/application/src/main/data/json/edge/instructions/install/docker/localhost_warning.md b/application/src/main/data/json/edge/instructions/install/docker/localhost_warning.md deleted file mode 100644 index d2a5bad26b..0000000000 --- a/application/src/main/data/json/edge/instructions/install/docker/localhost_warning.md +++ /dev/null @@ -1,3 +0,0 @@ -###### 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/json/edge/instructions/upgrade/docker/start_service.md b/application/src/main/data/json/edge/instructions/upgrade/docker/start_service.md index b9e5786211..a6c9c6731c 100644 --- a/application/src/main/data/json/edge/instructions/upgrade/docker/start_service.md +++ b/application/src/main/data/json/edge/instructions/upgrade/docker/start_service.md @@ -5,15 +5,15 @@ nano docker-compose.yml ``` ```text -version: '3.0' +version: '3.8' services: -mytbedge: -restart: always -image: "thingsboard/tb-edge:${TB_EDGE_VERSION}" + mytbedge: + restart: always + image: "thingsboard/tb-edge:${TB_EDGE_VERSION}" ... ``` -Make sure your image is the set to tb-edge-${TB_EDGE_VERSION}. +Make sure your image is the set to **tb-edge-${TB_EDGE_VERSION}**. Execute the following commands to up this docker compose directly: ```bash diff --git a/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md index 8c218ab41d..d922fa9155 100644 --- a/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md +++ b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md @@ -1,16 +1,14 @@ -${CLEAR_DOCKER_UPGRADE} - Create docker compose file for ThingsBoard Edge upgrade process: ```bash -nano docker-compose-upgrade.yml +> docker-compose-upgrade.yml && nano docker-compose-upgrade.yml {:copy-code} ``` Add the following lines to the yml file: ```bash -version: '3.0' +version: '3.8' services: mytbedge: restart: on-failure @@ -18,8 +16,8 @@ services: environment: SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/tb-edge volumes: - - ~/.mytb-edge-data:/data - - ~/.mytb-edge-logs:/var/log/tb-edge + - tb-edge-data:/data + - tb-edge-logs:/var/log/tb-edge entrypoint: upgrade-tb-edge.sh postgres: restart: always @@ -30,7 +28,15 @@ services: POSTGRES_DB: tb-edge POSTGRES_PASSWORD: postgres volumes: - - ~/.mytb-edge-data/db:/var/lib/postgresql/data + - tb-edge-postgres-data:/var/lib/postgresql/data + +volumes: + tb-edge-data: + name: tb-edge-data + tb-edge-logs: + name: tb-edge-logs + tb-edge-postgres-data: + name: tb-edge-postgres-data {:copy-code} ``` diff --git a/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md index 04034dea5e..bb3ce268b8 100644 --- a/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md +++ b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md @@ -15,10 +15,69 @@ docker compose rm mytbedge docker-compose stop docker-compose rm mytbedge ``` + +##### Migrating Data from Docker Bind Mount Folders to Docker Volumes +Starting with the **3.6.2** release, the ThingsBoard team has transitioned from using Docker bind mount folders to Docker volumes. +This change aims to enhance security and efficiency in storing data for Docker containers and to mitigate permission issues across various environments. + +To migrate from Docker bind mounts to Docker volumes, please execute the following commands: + +```bash +docker run --rm -v tb-edge-data:/volume -v ~/.mytb-edge-data:/backup busybox sh -c "cp -a /backup/. /volume" +docker run --rm -v tb-edge-logs:/volume -v ~/.mytb-edge-logs:/backup busybox sh -c "cp -a /backup/. /volume" +docker run --rm -v tb-edge-postgres-data:/volume -v ~/.mytb-edge-data/db:/backup busybox sh -c "cp -a /backup/. /volume" +{:copy-code} +``` + +After completing the data migration to the newly created Docker volumes, you'll need to update the volume mounts in your Docker Compose configuration. +Modify the `docker-compose.yml` file for ThingsBoard Edge to update the volume settings. + +First, please update docker compose file version. Find next snippet: +```text +version: '3.0' +... +``` + +And replace it with: +```text +version: '3.8' +... +``` + +Then update volume mounts. Locate the following snippet: +```text + volumes: + - ~/.mytb-edge-data:/data + - ~/.mytb-edge-logs:/var/log/tb-edge +... +``` + +And replace it with: +```text + volumes: + - tb-edge-data:/data + - tb-edge-logs:/var/log/tb-edge +... +``` + +Apply a similar update for the PostgreSQL service. Find the section: +```text + volumes: + - ~/.mytb-edge-data/db:/var/lib/postgresql/data +... +``` + +And replace it with: +```text + volumes: + - tb-edge-postgres-data/:/var/lib/postgresql/data +... +``` + ##### Backup Database -Make a copy of the database folder before upgrading: +Make a copy of the database volume before upgrading: ```bash -sudo cp -r ~/.mytb-edge-data/db ~/.mytb-edge-db-BACKUP +docker run --rm -v tb-edge-postgres-data:/source -v tb-edge-postgres-data-backup:/backup busybox sh -c "cp -a /source/. /backup" {:copy-code} ``` diff --git a/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_rm.md b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_rm.md deleted file mode 100644 index 4957f9333f..0000000000 --- a/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_rm.md +++ /dev/null @@ -1,5 +0,0 @@ -Delete docker compose file, if already exists: -```bash -rm docker-compose-upgrade.yml -{:copy-code} -``` diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java index 49f24ac454..fef3c2644a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java @@ -22,6 +22,7 @@ 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.EdgeInstructions; +import org.thingsboard.server.dao.util.DeviceConnectivityUtil; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.install.InstallScripts; @@ -75,12 +76,12 @@ public class DefaultEdgeInstallInstructionsService implements EdgeInstallInstruc private EdgeInstructions getDockerInstallInstructions(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!!!"); + + if (DeviceConnectivityUtil.isLocalhost(baseUrl)) { + dockerInstallInstructions = dockerInstallInstructions.replace("${EXTRA_HOSTS}", "extra_hosts:\n - \"host.docker.internal:host-gateway\"\n"); + dockerInstallInstructions = dockerInstallInstructions.replace("${BASE_URL}", "host.docker.internal"); } else { - dockerInstallInstructions = dockerInstallInstructions.replace("${LOCALHOST_WARNING}", ""); + dockerInstallInstructions = dockerInstallInstructions.replace("${EXTRA_HOSTS}", ""); dockerInstallInstructions = dockerInstallInstructions.replace("${BASE_URL}", baseUrl); } String edgeVersion = appVersion + "EDGE"; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java index 0401521a1c..4f233babf1 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java @@ -82,7 +82,6 @@ public class DefaultEdgeUpgradeInstructionsService implements EdgeUpgradeInstruc if (edgeUpgradeInfo == null || edgeUpgradeInfo.getNextEdgeVersion() == null || tbVersion.equals(currentEdgeVersion)) { return new EdgeInstructions("Edge upgrade instruction for " + currentEdgeVersion + "EDGE is not available."); } - boolean rmUpgradeCompose = false; StringBuilder result = new StringBuilder(readFile(resolveFile("docker", "upgrade_preparing.md"))); while (edgeUpgradeInfo.getNextEdgeVersion() != null || !tbVersion.equals(currentEdgeVersion)) { String edgeVersion = edgeUpgradeInfo.getNextEdgeVersion(); @@ -93,13 +92,6 @@ public class DefaultEdgeUpgradeInstructionsService implements EdgeUpgradeInstruc } else { ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${UPGRADE_DB}", ""); } - if (!rmUpgradeCompose) { - rmUpgradeCompose = true; - ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${CLEAR_DOCKER_UPGRADE}", ""); - } else { - String rmUpgrade = readFile(resolveFile("docker", "upgrade_rm.md")); - ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${CLEAR_DOCKER_UPGRADE}", rmUpgrade); - } ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${TB_EDGE_VERSION}", edgeVersion + "EDGE"); ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${FROM_TB_EDGE_VERSION}", currentEdgeVersion + "EDGE"); currentEdgeVersion = edgeVersion; diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java b/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java index c5708ce34e..63ddba1c14 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java @@ -217,7 +217,7 @@ public class DeviceConnectivityUtil { return host; } - private static boolean isLocalhost(String host) { + public static boolean isLocalhost(String host) { try { InetAddress inetAddress = InetAddress.getByName(host); return inetAddress.isLoopbackAddress(); From 5a6ca7e5b63e03b438fb9bc4c47483f21cc78851 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Fri, 8 Dec 2023 13:39:49 +0200 Subject: [PATCH 31/38] Rename local env to make it fix method name --- ...DefaultEdgeUpgradeInstructionsService.java | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java index 4f233babf1..212fe781de 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java @@ -85,18 +85,18 @@ public class DefaultEdgeUpgradeInstructionsService implements EdgeUpgradeInstruc StringBuilder result = new StringBuilder(readFile(resolveFile("docker", "upgrade_preparing.md"))); while (edgeUpgradeInfo.getNextEdgeVersion() != null || !tbVersion.equals(currentEdgeVersion)) { String edgeVersion = edgeUpgradeInfo.getNextEdgeVersion(); - String ubuntuUpgradeInstructions = readFile(resolveFile("docker", "instructions.md")); + String dockerUpgradeInstructions = readFile(resolveFile("docker", "instructions.md")); if (edgeUpgradeInfo.isRequiresUpdateDb()) { String upgradeDb = readFile(resolveFile("docker", "upgrade_db.md")); - ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${UPGRADE_DB}", upgradeDb); + dockerUpgradeInstructions = dockerUpgradeInstructions.replace("${UPGRADE_DB}", upgradeDb); } else { - ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${UPGRADE_DB}", ""); + dockerUpgradeInstructions = dockerUpgradeInstructions.replace("${UPGRADE_DB}", ""); } - ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${TB_EDGE_VERSION}", edgeVersion + "EDGE"); - ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${FROM_TB_EDGE_VERSION}", currentEdgeVersion + "EDGE"); + dockerUpgradeInstructions = dockerUpgradeInstructions.replace("${TB_EDGE_VERSION}", edgeVersion + "EDGE"); + dockerUpgradeInstructions = dockerUpgradeInstructions.replace("${FROM_TB_EDGE_VERSION}", currentEdgeVersion + "EDGE"); currentEdgeVersion = edgeVersion; edgeUpgradeInfo = upgradeVersionHashMap.get(edgeUpgradeInfo.getNextEdgeVersion()); - result.append(ubuntuUpgradeInstructions); + result.append(dockerUpgradeInstructions); } String startService = readFile(resolveFile("docker", "start_service.md")); startService = startService.replace("${TB_EDGE_VERSION}", currentEdgeVersion + "EDGE"); @@ -114,20 +114,20 @@ public class DefaultEdgeUpgradeInstructionsService implements EdgeUpgradeInstruc StringBuilder result = new StringBuilder(upgrade_preparing); while (edgeUpgradeInfo.getNextEdgeVersion() != null || !tbVersion.equals(currentEdgeVersion)) { String edgeVersion = edgeUpgradeInfo.getNextEdgeVersion(); - String ubuntuUpgradeInstructions = readFile(resolveFile(os, "instructions.md")); + String linuxUpgradeInstructions = readFile(resolveFile(os, "instructions.md")); if (edgeUpgradeInfo.isRequiresUpdateDb()) { String upgradeDb = readFile(resolveFile("upgrade_db.md")); - ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${UPGRADE_DB}", upgradeDb); + linuxUpgradeInstructions = linuxUpgradeInstructions.replace("${UPGRADE_DB}", upgradeDb); } else { - ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${UPGRADE_DB}", ""); + linuxUpgradeInstructions = linuxUpgradeInstructions.replace("${UPGRADE_DB}", ""); } - ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${TB_EDGE_TAG}", getTagVersion(edgeVersion)); - ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${FROM_TB_EDGE_TAG}", getTagVersion(currentEdgeVersion)); - ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${TB_EDGE_VERSION}", edgeVersion); - ubuntuUpgradeInstructions = ubuntuUpgradeInstructions.replace("${FROM_TB_EDGE_VERSION}", currentEdgeVersion); + linuxUpgradeInstructions = linuxUpgradeInstructions.replace("${TB_EDGE_TAG}", getTagVersion(edgeVersion)); + linuxUpgradeInstructions = linuxUpgradeInstructions.replace("${FROM_TB_EDGE_TAG}", getTagVersion(currentEdgeVersion)); + linuxUpgradeInstructions = linuxUpgradeInstructions.replace("${TB_EDGE_VERSION}", edgeVersion); + linuxUpgradeInstructions = linuxUpgradeInstructions.replace("${FROM_TB_EDGE_VERSION}", currentEdgeVersion); currentEdgeVersion = edgeVersion; edgeUpgradeInfo = upgradeVersionHashMap.get(edgeUpgradeInfo.getNextEdgeVersion()); - result.append(ubuntuUpgradeInstructions); + result.append(linuxUpgradeInstructions); } String startService = readFile(resolveFile("start_service.md")); result.append(startService); From 186728451937a73c7b1638729ad19fd95976bd4d Mon Sep 17 00:00:00 2001 From: Dmitriymush Date: Fri, 8 Dec 2023 12:51:33 +0200 Subject: [PATCH 32/38] UI: added milligram per cubic meter unit --- ui-ngx/src/assets/locale/locale.constant-en_US.json | 1 + ui-ngx/src/assets/metadata/units.json | 5 +++++ 2 files changed, 6 insertions(+) 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 5e585783d5..6d10cefbfe 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -4533,6 +4533,7 @@ "gram-per-cubic-centimeter": "Gram per cubic centimeter", "kilogram-per-square-meter": "Kilogram per square metre", "milligram-per-milliliter": "Milligram per milliliter", + "milligram-per-cubic-meter": "Milligram per cubic meter", "pound-per-cubic-foot": "Pound per cubic foot", "ounces-per-cubic-inch": "Ounces per cubic inch", "tons-per-cubic-yard": "Tons per cubic yard", diff --git a/ui-ngx/src/assets/metadata/units.json b/ui-ngx/src/assets/metadata/units.json index 3237d6620b..53744cecb7 100644 --- a/ui-ngx/src/assets/metadata/units.json +++ b/ui-ngx/src/assets/metadata/units.json @@ -1316,6 +1316,11 @@ "symbol": "mg/mL", "tags": ["concentration","mass per volume","mg/mL"] }, +{ + "name": "unit.milligram-per-cubic-meter", + "symbol": "mg/m³", + "tags": ["concentration","mass per volume","mg/m³"] +}, { "name": "unit.pound-per-cubic-foot", "symbol": "lb/ft³", From 4cb541fc7e511ef327ba534baa458a72eafe8be1 Mon Sep 17 00:00:00 2001 From: artem Date: Fri, 8 Dec 2023 14:02:46 +0200 Subject: [PATCH 33/38] russian language removed --- .../assets/locale/locale.constant-en_US.json | 1 - .../assets/locale/locale.constant-ru_RU.json | 1875 ----------------- 2 files changed, 1876 deletions(-) delete mode 100644 ui-ngx/src/assets/locale/locale.constant-ru_RU.json 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 5e585783d5..260494061a 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6692,7 +6692,6 @@ "nl_BE": "Koninkrijk België", "pt_BR": "Português do Brasil", "ro_RO": "Română", - "ru_RU": "Русский", "sl_SI": "Slovenščina", "tr_TR": "Türkçe", "uk_UA": "Українська", diff --git a/ui-ngx/src/assets/locale/locale.constant-ru_RU.json b/ui-ngx/src/assets/locale/locale.constant-ru_RU.json deleted file mode 100644 index 5e15a3496e..0000000000 --- a/ui-ngx/src/assets/locale/locale.constant-ru_RU.json +++ /dev/null @@ -1,1875 +0,0 @@ -{ - "access": { - "unauthorized": "Неавторизированный", - "unauthorized-access": "Несанкционированный доступ", - "unauthorized-access-text": "Вы должны войти в систему для получения доступа к этому ресурсу!", - "access-forbidden": "Доступ запрещен", - "access-forbidden-text": "У вас нет прав доступа к этому ресурсу!
Для получения доступа попробуйте войти под другим пользователем.", - "refresh-token-expired": "Сессия истекла", - "refresh-token-failed": "Не удалось обновить сессию" - }, - "action": { - "activate": "Активировать", - "suspend": "Приостановить", - "save": "Сохранить", - "saveAs": "Сохранить как", - "cancel": "Отмена", - "ok": "ОК", - "delete": "Удалить", - "add": "Добавить", - "yes": "Да", - "no": "Нет", - "update": "Обновить", - "remove": "Удалить", - "search": "Поиск", - "clear-search": "Очистить", - "assign": "Присвоить", - "unassign": "Отозвать", - "share": "Поделиться", - "make-private": "Закрыть для общего доступа", - "apply": "Применить", - "apply-changes": "Применить изменения", - "edit-mode": "Режим редактирования", - "enter-edit-mode": "Режим редактирования", - "decline-changes": "Отменить изменения", - "close": "Закрыть", - "back": "Назад", - "run": "Запуск", - "sign-in": "Войти", - "edit": "Редактировать", - "view": "Просмотреть", - "create": "Создать", - "drag": "Переместить", - "refresh": "Обновить", - "undo": "Откатить", - "copy": "Копировать", - "paste": "Вставить", - "copy-reference": "Копировать ссылку", - "paste-reference": "Вставить ссылку", - "import": "Импортировать", - "export": "Экспортировать", - "share-via": "Поделиться в {{provider}}", - "continue": "Продолжить", - "discard-changes": "Отменить изменения", - "done": "Завершено" - }, - "aggregation": { - "aggregation": "Агрегация", - "function": "Тип агрегации данных", - "limit": "Максимальное значение", - "group-interval": "Интервал группировки", - "min": "Мин", - "max": "Maкс", - "avg": "Среднее", - "sum": "Сумма", - "count": "Количество", - "none": "Без агрегации" - }, - "admin": { - "general": "Общие", - "general-settings": "Общие настройки", - "outgoing-mail": "Исходящая почта", - "outgoing-mail-settings": "Настройки исходящей почты", - "system-settings": "Системные настройки", - "test-mail-sent": "Пробное письмо успешно отправлено!", - "base-url": "Базовая URL", - "base-url-required": "Базовая URL обязательна.", - "mail-from": "Отправитель", - "mail-from-required": "Отправитель обязателен.", - "smtp-protocol": "SMTP протокол", - "smtp-host": "SMTP хост", - "smtp-host-required": "SMTP хост обязателен.", - "smtp-port": "SMTP порт", - "smtp-port-required": "SMTP порт обязателен.", - "smtp-port-invalid": "Недействительный SMTP порт.", - "timeout-msec": "Таймаут (мс)", - "timeout-required": "Таймаут обязателен.", - "timeout-invalid": "Недействительный таймаут.", - "enable-tls": "Включить TLS", - "tls-version" : "Версия TLS", - "send-test-mail": "Отправить пробное письмо", - "security-settings": "Настройки безопасности", - "password-policy": "Политика паролей", - "minimum-password-length": "Минимальная длина пароля", - "minimum-password-length-required": "Требуется минимальная длина пароля", - "minimum-password-length-range": "Минимальная длина пароля должна быть в диапазоне от 5 до 50", - "minimum-uppercase-letters": "Минимальное количество прописных букв", - "minimum-uppercase-letters-range": "Минимальное количество прописных букв не может быть отрицательным", - "minimum-lowercase-letters": "Минимальное количество строчных букв", - "minimum-lowercase-letters-range": "Минимальное количество строчных букв не может быть отрицательным", - "minimum-digits": "Минимальное количество цифр", - "minimum-digits-range": "Минимальное количество цифр не может быть отрицательным", - "minimum-special-characters": "Минимальное количество специальных символов", - "minimum-special-characters-range": "Минимальное количество специальных символов не может быть отрицательным", - "password-expiration-period-days": "Срок действия пароля в днях", - "password-expiration-period-days-range": "Срок действия пароля в днях не может быть отрицательным", - "password-reuse-frequency-days": "Частота повторного использования пароля в днях", - "password-reuse-frequency-days-range": "Частота повторного использования пароля в днях не может быть отрицательной", - "general-policy": "Общая политика", - "max-failed-login-attempts": "Максимальное количество неудачных попыток входа в систему, прежде чем учетная запись заблокирована", - "minimum-max-failed-login-attempts-range": "Максимальное количество неудачных попыток входа в систему не может быть отрицательным", - "user-lockout-notification-email": "В случае блокировки учетной записи пользователя отправьте уведомление на электронную почту", - "smpp-provider": { - "smpp-version": "SMPP версия", - "smpp-host": "SMPP хост", - "smpp-host-required": "SMPP хост обязателен.", - "smpp-port": "SMPP порт", - "smpp-port-required": "SMPP порт обязателен.", - "system-id": "ИД системи", - "system-id-required": "ИД системи обязателен.", - "password": "Пароль", - "password-required": "Пароль обязателен.", - "type-settings": "Настройки типов", - "source-settings": "Настройки источника", - "destination-settings": "Настройки назначения", - "additional-settings": "Дополнительные настройки", - "system-type": "Тип системы", - "bind-type": "Тип привязки", - "service-type": "Тип обслуживания", - "source-address": "Адрес источника", - "source-ton": "Тип номера источника", - "source-npi": "Идентификация плана нумерации источника", - "destination-ton": "Тип номера назничения", - "destination-npi": "Идентификация плана нумерации назначения", - "address-range": "Диапазон адресов", - "coding-scheme": "Схема кодирования" - } - }, - "alarm": { - "alarm": "Оповещение", - "alarms": "Оповещения", - "select-alarm": "Выбрать оповещение", - "no-alarms-matching": "Оповещения '{{entity}}' не найдены.", - "alarm-required": "Оповещение обязательно", - "alarm-status": "Статус оповещения", - "search-status": { - "ANY": "Все", - "ACTIVE": "Активные", - "CLEARED": "Сброшенные", - "ACK": "Подтвержденные", - "UNACK": "Неподтвержденные" - }, - "display-status": { - "ACTIVE_UNACK": "Активные неподтвержденные", - "ACTIVE_ACK": "Активные подтвержденные", - "CLEARED_UNACK": "Сброшенные неподтвержденные", - "CLEARED_ACK": "Сброшенные подтвержденные" - }, - "no-alarms-prompt": "Оповещения отсутствуют", - "created-time": "Время создания", - "type": "Тип", - "severity": "Уровень", - "originator": "Инициатор", - "originator-type": "Тип инициатора", - "details": "Подробности", - "status": "Статус", - "alarm-details": "Подробности об оповещении", - "start-time": "Время начала", - "end-time": "Время окончания", - "ack-time": "Время подтверждения", - "clear-time": "Время сброса", - "severity-critical": "Критический", - "severity-major": "Основной", - "severity-minor": "Второстепенный", - "severity-warning": "Предупреждение", - "severity-indeterminate": "Неопределенный", - "acknowledge": "Подтвердить", - "clear": "Сбросить", - "search": "Поиск оповещений", - "selected-alarms": "Выбрано { count, plural, =1 {1 оповещение} few {# оповещения} other {# оповещений} }", - "no-data": "Нет данных для отображения", - "polling-interval": "Интервал опроса оповещений (сек)", - "polling-interval-required": "Интервал опроса оповещений обязателен.", - "min-polling-interval-message": "Минимальный интервал опроса оповещений 1 секунда.", - "aknowledge-alarms-title": "Подтвердить { count, plural, =1 {1 оповещение} other {# оповещений} }", - "aknowledge-alarms-text": "Вы точно хотите подтвердить { count, plural, =1 {1 оповещение} other {# оповещений} }?", - "aknowledge-alarm-title": "Подтвердить оповещение", - "aknowledge-alarm-text": "Вы точно хотите подтвердить оповещение?", - "clear-alarms-title": "Сбросить { count, plural, =1 {1 оповещение} other {# оповещений} }", - "clear-alarms-text": "Вы точно хотите сбросить { count, plural, =1 {1 оповещение} other {# оповещений} }?", - "clear-alarm-title": "Сбросить оповещение", - "clear-alarm-text": "Вы точно хотите сбросить оповещение?", - "alarm-status-filter": "Фильтр оповещений", - "max-count-load": "Максимальное количество оповещений для загрузки (0 - неограниченно)", - "max-count-load-required": "Максимальное количество оповещений для загрузки обязателен.", - "max-count-load-error-min": "Минимальное значение 0.", - "fetch-size": "Размер пакета для загрузки", - "fetch-size-required": "Размер пакета для загрузки обязателен.", - "fetch-size-error-min": "Минимальное значение 10." - }, - "alias": { - "add": "Добавить псевдоним", - "edit": "Редактировать псевдоним", - "name": "Псевдоним", - "name-required": "Псевдоним обязателен", - "duplicate-alias": "Такой псевдоним уже существует.", - "filter-type-single-entity": "Отдельный объект", - "filter-type-entity-list": "Список объектов", - "filter-type-entity-name": "Название объекта", - "filter-type-state-entity": "Объект из состояния дашборда", - "filter-type-state-entity-description": "Объект, полученный из параметров состояния дашборда", - "filter-type-asset-type": "Тип актива", - "filter-type-asset-type-description": "Активы типа '{{assetTypes}}'", - "filter-type-asset-type-and-name-description": "Активы типа '{{assetTypes}}' и названием, начинающимся с '{{prefix}}'", - "filter-type-device-type": "Тип устройства", - "filter-type-device-type-description": "Устройства типа '{{deviceTypes}}'", - "filter-type-device-type-and-name-description": "Устройства типа '{{deviceTypes}}' и названием, начинающимся с '{{prefix}}'", - "filter-type-entity-view-type": "Тип Представления Объекта", - "filter-type-entity-view-type-description": "Представления Объекта типа '{{entityViewTypes}}'", - "filter-type-entity-view-type-and-name-description": "Представления Объекта типа '{{entityViewTypes}}' и названием, начинающимся с '{{prefix}}'", - "filter-type-relations-query": "Запрос по типу отношений", - "filter-type-relations-query-description": "{{entities}}, имеющие отношение типа {{relationType}} {{direction}} {{rootEntity}}", - "filter-type-asset-search-query": "Поисковый запрос по активам", - "filter-type-asset-search-query-description": "Активы типа {{assetTypes}}, имеющие отношение типа {{relationType}} {{direction}} {{rootEntity}}", - "filter-type-device-search-query": "Поисковый запрос по устройствам", - "filter-type-device-search-query-description": "Устройства типа {{deviceTypes}}, имеющие отношение типа {{relationType}} {{direction}} {{rootEntity}}", - "filter-type-entity-view-search-query": "Поисковый запрос по представлениям объектов", - "filter-type-entity-view-search-query-description": "Представления объектов типа {{entityViewTypes}}, имеющие отношение типа {{relationType}} {{direction}} {{rootEntity}}", - "entity-filter": "Фильтр объектов", - "resolve-multiple": "Принять как несколько объектов", - "filter-type": "Тип фильтра", - "filter-type-required": "Тип фильтра обязателен.", - "entity-filter-no-entity-matched": "Объекты, соответствующие фильтру, не найдены.", - "no-entity-filter-specified": "Не указан фильтр объектов", - "root-state-entity": "Использовать объект, полученный из дашборда, как корневой", - "last-level-relation": "Использовать только отношения последнего уровня", - "root-entity": "Корневой объект", - "state-entity-parameter-name": "Название объекта состояния", - "default-state-entity": "Объект состояния по умолчанию", - "default-entity-parameter-name": "По умолчанию", - "max-relation-level": "Максимальная глубина отношений", - "unlimited-level": "Неограниченная глубина", - "state-entity": "Объект состояния дашборда", - "all-entities": "Все объекты", - "any-relation": "не указано" - }, - "asset": { - "asset": "Актив", - "assets": "Активы", - "management": "Управление активами", - "view-assets": "Просмотреть активы", - "add": "Добавить актив", - "assign-to-customer": "Присвоить клиенту", - "assign-asset-to-customer": "Присвоить актив(ы) клиенту", - "assign-asset-to-customer-text": "Пожалуйста, выберите активы, которые нужно присвоить объекту", - "no-assets-text": "Активы не найдены", - "assign-to-customer-text": "Пожалуйста, выберите клиента, которому нужно присвоить актив(ы)", - "public": "Общедоступные", - "assignedToCustomer": "Присвоить клиенту", - "make-public": "Открыть общий доступ к активу", - "make-private": "Закрыть общий доступ к активу", - "unassign-from-customer": "Отозвать у клиента", - "delete": "Удалить актив", - "asset-public": "Актив общедоступный", - "asset-type": "Тип актива", - "asset-type-required": "Тип актива обязателен.", - "select-asset-type": "Выберите тип актива", - "enter-asset-type": "Введите тип актива", - "any-asset": "Любой актив", - "no-asset-types-matching": "Активы типа '{{entitySubtype}}' не найдены.", - "asset-type-list-empty": "Типы активов не выбраны.", - "asset-types": "Типы активов", - "name": "Название", - "name-required": "Название обязательно.", - "description": "Описание", - "type": "Тип", - "type-required": "Тип обязателен.", - "details": "Подробности", - "events": "События", - "add-asset-text": "Добавить новый актив", - "asset-details": "Подробности об активе", - "assign-assets": "Присвоить активы", - "assign-assets-text": "Присвоить { count, plural, =1 {1 актив} few {# актива} other {# активов} } клиенту", - "delete-assets": "Удалить активы", - "unassign-assets": "Отозвать активы", - "unassign-assets-action-title": "Отозвать { count, plural, =1 {1 актив} few {# актива} other {# активов} } у клиента", - "assign-new-asset": "Присвоить новый актив", - "delete-asset-title": "Вы точно хотите удалить '{{assetName}}'?", - "delete-asset-text": "Внимание, после подтверждения актив и все связанные с ним данные будут безвозвратно удалены.", - "delete-assets-title": "Вы точно хотите удалить { count, plural, =1 {1 актив} few {# актива} other {# активов} }", - "delete-assets-action-title": "Удалить { count, plural, =1 {1 актив} few {# актива} other {# активов} }", - "delete-assets-text": "Внимание, после подтверждения выбранные активы и все связанные с ними данные будут безвозвратно удалены.", - "make-public-asset-title": "Вы точно хотите открыть общий доступ к активу '{{assetName}}'?", - "make-public-asset-text": "Внимание, после подтверждения актив и все связанные с ним данные станут общедоступными.", - "make-private-asset-title": "Вы точно хотите закрыть общий доступ к активу '{{assetName}}'?", - "make-private-asset-text": "После подтверждения актив и все связанные с ним данные будут закрыты для общего доступа", - "unassign-asset-title": "Вы точно хотите отозвать актив '{{assetName}}'?", - "unassign-asset-text": "После подтверждения актив будут отозван, и клиент потеряет к нему доступ.", - "unassign-asset": "Отозвать актив", - "unassign-assets-title": "Вы точно хотите отозвать { count, plural, =1 {1 актив} few {# актива} other {# активов} }?", - "unassign-assets-text": "После подтверждения активы будут отозваны, и клиент потеряет к ним доступ.", - "copyId": "Копировать ИД актива", - "idCopiedMessage": "ИД актива скопировано в буфер обмена", - "select-asset": "Выбрать активы", - "no-assets-matching": "Активы, соответствующие '{{entity}}', не найдены.", - "asset-required": "Актив обязателен", - "name-starts-with": "Название актива, начинающееся с", - "import": "Импортировать активы", - "asset-file": "Файл с активами", - "label": "Метка" - }, - "attribute": { - "attributes": "Атрибуты", - "latest-telemetry": "Последняя телеметрия", - "attributes-scope": "Контекст атрибутов объекта", - "scope-latest-telemetry": "Последняя телеметрия", - "scope-client": "Клиентские атрибуты", - "scope-server": "Серверные атрибуты", - "scope-shared": "Общие атрибуты", - "add": "Добавить атрибут", - "key": "Ключ", - "last-update-time": "Последнее обновление", - "key-required": "Ключ атрибута обязателен.", - "value": "Значение", - "value-required": "Значение атрибута обязательно.", - "delete-attributes-title": "Вы уверенны, что хотите удалить { count, plural, =1 {1 атрибут} few {# атрибута} other {# атрибутов} }? ", - "delete-attributes-text": "Внимание, после подтверждения выбранные атрибуты будут удалены.", - "delete-attributes": "Удалить атрибуты", - "enter-attribute-value": "Введите значение атрибута", - "show-on-widget": "Показать на виджете", - "widget-mode": "Виджет-режим", - "next-widget": "Следующий виджет", - "prev-widget": "Предыдущий виджет", - "add-to-dashboard": "Добавить на дашборд", - "add-widget-to-dashboard": "Добавить виджет на дашборд", - "selected-attributes": "{ count, plural, =1 {Выбран} other {Выбраны} } { count, plural, =1 {1 атрибут} few {# атрибута} other {# атрибутов} }", - "selected-telemetry": "{ count, plural, =1 {Выбран} other {Выбраны} } { count, plural, =1 {1 параметр} few {# параметра} other {# параметров} } телеметрии" - }, - "audit-log": { - "audit": "Аудит", - "audit-logs": "Логи аудита", - "timestamp": "Время", - "entity-type": "Тип объекта", - "entity-name": "Название объекта", - "user": "Пользователь", - "type": "Тип", - "status": "Статус", - "details": "Подробности", - "type-added": "Добавленный", - "type-deleted": "Удаленный", - "type-updated": "Обновленный", - "type-attributes-updated": "Обновлены атрибуты", - "type-attributes-deleted": "Удалены атрибуты", - "type-rpc-call": "RPC вызов", - "type-credentials-updated": "Обновлены учетные данные", - "type-assigned-to-customer": "Присвоен клиенту", - "type-unassigned-from-customer": "Отозван у клиента", - "type-activated": "Активирован", - "type-suspended": "Приостановлен", - "type-credentials-read": "Чтение учетные данных", - "type-attributes-read": "Чтение атрибутов", - "type-relation-add-or-update": "Обновлены отношения", - "type-relation-delete": "Удалены отношения", - "type-relations-delete": "Удалены все отношения", - "type-alarm-ack": "Подтвержден", - "type-alarm-clear": "Сброшен", - "type-login": "Вход", - "type-logout": "Выход", - "type-lockout": "Заблокирован", - "status-success": "Успех", - "status-failure": "Сбой", - "audit-log-details": "Подробности аудит лога", - "no-audit-logs-prompt": "Логи не найдены", - "action-data": "Данные действия", - "failure-details": "Подробности сбоя", - "search": "Поиск аудит логов", - "clear-search": "Очистить поиск" - }, - "confirm-on-exit": { - "message": "У вас есть несохраненные изменения. Вы точно хотите покинуть эту страницу?", - "html-message": "У вас есть несохраненные изменения.
Вы точно хотите покинуть эту страницу?", - "title": "Несохраненные изменения" - }, - "contact": { - "country": "Страна", - "city": "Город", - "state": "Штат", - "postal-code": "Почтовый код", - "postal-code-invalid": "Допустимы только цифры", - "address": "Адрес", - "address2": "Адрес 2", - "phone": "Телефон", - "email": "Эл. адрес", - "no-address": "Адрес не указан" - }, - "common": { - "username": "Имя пользователя", - "password": "Пароль", - "enter-username": "Введите имя пользователя", - "enter-password": "Введите пароль", - "enter-search": "Введите условие поиска", - "created-time": "Время создания" - }, - "content-type": { - "json": "Json", - "text": "Текстовый", - "binary": "Бинарный (Base64)" - }, - "customer": { - "customer": "Клиент", - "customers": "Клиенты", - "management": "Управление клиентами", - "dashboard": "Дашборд клиента", - "dashboards": "Дашборды клиента", - "devices": "Устройства клиента", - "entity-views": "Представления объектов клиента", - "assets": "Активы клиента", - "public-dashboards": "Общедоступные дашборды", - "public-devices": "Общедоступные устройства", - "public-assets": "Общедоступные активы", - "public-entity-views": "Общедоступные представления объектов", - "add": "Добавить клиента", - "delete": "Удалить клиента", - "manage-customer-users": "Управление пользователями клиента", - "manage-customer-devices": "Управление устройствами клиента", - "manage-customer-dashboards": "Управление дашбордами клиента", - "manage-public-devices": "Управление общедоступными устройствами", - "manage-public-dashboards": "Управление общедоступными дашбордами", - "manage-customer-assets": "Управление активами клиента", - "manage-public-assets": "Управление общедоступными активами", - "add-customer-text": "Добавить нового клиента", - "no-customers-text": "Клиенты не найдены", - "customer-details": "Подробности о клиенте", - "delete-customer-title": "Вы точно хотите удалить клиента '{{customerTitle}}'?", - "delete-customer-text": "Внимание, после подтверждения клиент и все связанные с ним данные будут безвозвратно удалены.", - "delete-customers-title": "Вы точно хотите удалить { count, plural, =1 {1 клиент} few {# клиента} other {# клиентов} }?", - "delete-customers-action-title": "Удалить { count, plural, =1 {1 клиент} few {# клиента} other {# клиентов} }", - "delete-customers-text": "Внимание, после подтверждения выбранные клиенты и все связанные с ними данные будут безвозвратно удалены.", - "manage-users": "Управление пользователями", - "manage-assets": "Управление активами", - "manage-devices": "Управление устройствами", - "manage-dashboards": "Управление дашбордами", - "title": "Имя", - "title-required": "Название обязательно.", - "description": "Описание", - "details": "Подробности", - "events": "События", - "copyId": "Копировать ИД клиента", - "idCopiedMessage": "ИД клиента скопирован в буфер обмена", - "select-customer": "Выбрать клиента", - "no-customers-matching": "Клиенты, соответствующие '{{entity}}', не найдены.", - "customer-required": "Клиент обязателен", - "select-default-customer": "Выбрать клиента по умолчанию", - "default-customer": "Клиент по умолчанию", - "default-customer-required": "Клиент по умолчанию обязателен для отладки дашборда на уровне на уровне Владельца" - }, - "datetime": { - "date-from": "Дата с", - "time-from": "Время с", - "date-to": "Дата до", - "time-to": "Время до" - }, - "dashboard": { - "dashboard": "Дашборд", - "dashboards": "Дашборды", - "management": "Управление дашбордами", - "view-dashboards": "Просмотреть дашборды", - "add": "Добавить дашборд", - "assign-dashboard-to-customer": "Прикрепить дашборд(ы) к клиенту", - "assign-dashboard-to-customer-text": "Пожалуйста, выберите дашборды, которые нужно прикрепить к клиенту", - "assign-to-customer-text": "Пожалуйста, выберите клиента, к которому нужно прикрепить дашборд(ы)", - "assign-to-customer": "Прикрепить к клиенту", - "unassign-from-customer": "Отозвать у клиента", - "make-public": "Открыть дашборд для общего доступа", - "make-private": "Закрыть дашборд для общего доступа", - "manage-assigned-customers": "Управление назначенными клиентами", - "assigned-customers": "Назначенные клиенты", - "assign-to-customers": "Присвоить дашборд(ы) клиенту", - "assign-to-customers-text": "Пожалуйста, выбери клиентов, которым нужно присвоить дашборд(ы)", - "unassign-from-customers": "Отозвать дашборд(ы) у клиентов", - "unassign-from-customers-text": "Пожалуйста, выберите клиентов, у которых нужно отозвать дашборд(ы)", - "no-dashboards-text": "Дашборды не найдены", - "no-widgets": "Виджеты не сконфигурированы", - "add-widget": "Добавить новый виджет", - "title": "Название", - "select-widget-title": "Выберите виджет", - "copyId": "Копировать идентификатор дашборда", - "idCopiedMessage": "Идентификатор дашборда скопирован в буфер обмена", - "select-widget-subtitle": "Список доступных виджетов", - "delete": "Удалить дашборд", - "title-required": "Название обязательно.", - "description": "Описание", - "details": "Подробности", - "dashboard-details": "Подробности о дашборде", - "add-dashboard-text": "Добавить новый дашборд", - "assign-dashboards": "Прикрепить дашборды", - "assign-new-dashboard": "Прикрепить новый дашборд", - "assign-dashboards-text": "Прикрепить { count, plural, =1 {1 дашборд} few {# дашборда} other {# дашбордов} } к клиенту", - "unassign-dashboards-action-text": "Отозвать { count, plural, =1 {1 дашборд} few {# дашборда} other {# дашбордов} } у клиента", - "delete-dashboards": "Удалить дашборды", - "unassign-dashboards": "Отозвать дашборды", - "unassign-dashboards-action-title": "Отозвать { count, plural, =1 {1 дашборд} few {# дашборда} other {# дашбордов} } у клиента", - "delete-dashboard-title": "Вы точно хотите удалить дашборд '{{dashboardTitle}}'?", - "delete-dashboard-text": "Внимание, после подтверждения дашборд и все связанные с ним данные будут безвозвратно утеряны.", - "delete-dashboards-title": "Вы точно хотите удалить { count, plural, =1 {1 дашборд} few {# дашборда} other {# дашбордов} }?", - "delete-dashboards-action-title": "Удалить { count, plural, =1 {1 дашборд} few {# дашборда} other {# дашбордов} }", - "delete-dashboards-text": "Внимание, после подтверждения дашборды и все связанные с ними данные будут безвозвратно утеряны.", - "unassign-dashboard-title": "Вы точно хотите отозвать дашборд '{{dashboardTitle}}'?", - "unassign-dashboard-text": "После подтверждения дашборд не будет доступен клиенту.", - "unassign-dashboard": "Отозвать дашборд", - "unassign-dashboards-title": "Вы точно хотите отозвать { count, plural, =1 {1 дашборд} few {# дашборда} other {# дашбордов} }?", - "unassign-dashboards-text": "После подтверждения выбранные дашборды не будут доступны клиенту.", - "public-dashboard-title": "Теперь дашборд общедоступный", - "public-dashboard-text": "Теперь ваш дашборд {{dashboardTitle}} доступен всем по ссылке:", - "public-dashboard-notice": "Примечание: Для получения доступа к данным устройства нужно открыть общий доступ к этому устройству.", - "make-private-dashboard-title": "Вы точно хотите закрыть общий доступ к дашборду '{{dashboardTitle}}'?", - "make-private-dashboard-text": "После подтверждения дашборд будет закрыт для общего доступа.", - "make-private-dashboard": "Закрыть дашборд для общего доступа", - "socialshare-text": "'{{dashboardTitle}}' сделано ThingsBoard", - "socialshare-title": "'{{dashboardTitle}}' сделано ThingsBoard", - "select-dashboard": "Выберите дашборд", - "no-dashboards-matching": "Дашборд '{{entity}}' не найден.", - "dashboard-required": "Дашборд обязателен.", - "select-existing": "Выберите существующий дашборд", - "create-new": "Создать новый дашборд", - "new-dashboard-title": "Новое название дашборда", - "open-dashboard": "Открыть дашборд", - "set-background": "Установить фон", - "background-color": "Фоновый цвет", - "background-image": "Фоновая картинка", - "background-size-mode": "Размер фона", - "no-image": "Картинка не выбрана", - "drop-image": "Перетащите картинку или кликните для выбора файла.", - "settings": "Настройки", - "columns-count": "Количество колонок", - "columns-count-required": "Количество колонок обязательно.", - "min-columns-count-message": "Минимальное число колонок - 10.", - "max-columns-count-message": "Максимальное число колонок - 1000.", - "widgets-margins": "Величина отступа между виджетами", - "horizontal-margin": "Величина горизонтального отступа", - "horizontal-margin-required": "Величина горизонтального отступа обязательна.", - "min-horizontal-margin-message": "Минимальная величина горизонтального отступа - 0.", - "max-horizontal-margin-message": "Максимальная величина горизонтального отступа - 50.", - "vertical-margin": "Величина вертикального отступа", - "vertical-margin-required": "Величина вертикального отступа обязательна.", - "min-vertical-margin-message": "Минимальная величина вертикального отступа - 0.", - "max-vertical-margin-message": "Максимальная величина вертикального отступа - 50.", - "autofill-height": "Автозаполнение по высоте", - "mobile-layout": "Настройки мобильного режима", - "mobile-row-height": "Высота строки в мобильном режиме, px", - "mobile-row-height-required": "Высота строки в мобильном режиме обязательна.", - "min-mobile-row-height-message": "Минимальная высота строки в мобильном режиме составляет 5 px.", - "max-mobile-row-height-message": "Максимальная высота строки в мобильном режиме составляет 200 px.", - "display-title": "Показать название дашборда", - "toolbar-always-open": "Отображать панель инструментов", - "title-color": "Цвет названия", - "display-dashboards-selection": "Отображать выборку дашбордов", - "display-entities-selection": "Отображать выбору объектов", - "display-dashboard-timewindow": "Показать временное окно", - "display-dashboard-export": "Показать экспорт", - "import": "Импортировать дашборд", - "export": "Экспортировать дашборд", - "export-failed-error": "Не удалось экспортировать дашборд: {{error}}", - "create-new-dashboard": "Создать новый дашборд", - "dashboard-file": "Файл дашборда", - "invalid-dashboard-file-error": "Не удалось импортировать дашборд: неизвестная схема данных дашборда.", - "dashboard-import-missing-aliases-title": "Настроить псевдонимы импортированного дашборда", - "create-new-widget": "Создать новый виджет", - "import-widget": "Импортировать виджет", - "widget-file": "Виджет-файл", - "invalid-widget-file-error": "Не удалось импортировать виджет: неправильный формат данных.", - "widget-import-missing-aliases-title": "Настроить псевдонимы, которые использует импортированный виджет", - "open-toolbar": "Открыть панель инструментов дашборда", - "close-toolbar": "Закрыть панель инструментов", - "configuration-error": "Ошибка в настройках", - "alias-resolution-error-title": "Ошибка в настройках псевдонимов дашборда", - "invalid-aliases-config": "Не удалось найти устройство, соответствующее фильтру псевдонимов.
Пожалуйста, обратитесь к администратору для устранения неполадки.", - "select-devices": "Выберите устройства", - "assignedToCustomer": "Присвоенные клиенту", - "assignedToCustomers": "Присвоенные клиентам", - "public": "Публичный", - "public-link": "Публичная ссылка", - "copy-public-link": "Копировать публичную ссылку", - "public-link-copied-message": "Публичная ссылка на дашборд скопирована в буфер обмена.", - "manage-states": "Управление состоянием дашборда", - "states": "Состояния дашборда", - "search-states": "Поиск состояния дашборда", - "selected-states": "Выбрано { count, plural, =1 {1 состояние} few {# состояния} other {# состояний} } дашборда", - "edit-state": "Изменить состояние дашборда", - "delete-state": "Удалить состояние дашборда", - "add-state": "Добавить состояние дашборда", - "state": "Состояние дашборда", - "state-name": "Название", - "state-name-required": "Название состояния дашборда обязательно.", - "state-id": "ИД состояния", - "state-id-required": "ИД состояния дашборда обязателен.", - "state-id-exists": "Состояния дашборда с таким именем уже существует.", - "is-root-state": "Корневое состояние", - "delete-state-title": "Удалить состояние дашборда", - "delete-state-text": "Вы точно хотите удалить состояние дашборда '{{stateName}}'?", - "show-details": "Показать подробности", - "hide-details": "Скрыть подробности", - "select-state": "Выбрать состояние", - "state-controller": "Контроллер состояния" - }, - "datakey": { - "settings": "Настройки", - "advanced": "Дополнительно", - "label": "Метка", - "color": "Цвет", - "units": "Укажите символы, которые нужно указывать после значения", - "decimals": "Число знаков после запятой", - "data-generation-func": "Функция генерации данных", - "use-data-post-processing-func": "Использовать функцию пост-обработки данных", - "configuration": "Конфигурация ключа данных", - "timeseries": "Телеметрия", - "attributes": "Атрибуты", - "entity-field": "Поле объекта", - "alarm": "Параметры оповещения", - "timeseries-required": "Телеметрия объекта обязательна.", - "timeseries-or-attributes-required": "Телеметрия/атрибуты обязательны.", - "maximum-timeseries-or-attributes": "Максимальное количество параметров телеметрии или атрибутов равно {{count}}", - "alarm-fields-required": "Параметры оповещения обязательны.", - "function-types": "Тип функции", - "function-types-required": "Тип функции обязателен.", - "maximum-function-types": "Максимальное количество типов функции равно {{count}}", - "time-description": "время текущего значения;", - "value-description": "текущее значение;", - "prev-value-description": "результат предыдущего вызова функции;", - "time-prev-description": "время предыдущего значения;", - "prev-orig-value-description": "исходное предыдущее значение;" - }, - "datasource": { - "type": "Тип источника данных", - "name": "Название", - "add-datasource-prompt": "Пожалуйста, добавьте источник данных" - }, - "details": { - "edit-mode": "Режим редактирования", - "edit-json": "Редактировать JSON", - "toggle-edit-mode": "Режим редактирования" - }, - "device": { - "device": "Устройство", - "device-required": "Устройство обязательно.", - "devices": "Устройства", - "management": "Управление устройствами", - "view-devices": "Просмотреть устройства", - "device-alias": "Псевдоним устройства", - "aliases": "Псевдонимы устройства", - "no-alias-matching": "'{{alias}}' не найден.", - "no-aliases-found": "Псевдонимы не найдены.", - "no-key-matching": "'{{key}}' не найден.", - "no-keys-found": "Ключи не найдены.", - "create-new-alias": "Создать новый!", - "create-new-key": "Создать новый!", - "duplicate-alias-error": "Найден дублирующийся псевдоним '{{alias}}'.
В рамках дашборда псевдонимы устройств должны быть уникальными.", - "configure-alias": "Настроить '{{alias}}' псевдоним", - "no-devices-matching": "Устройство '{{entity}}' не найдено.", - "alias": "Псевдоним", - "alias-required": "Псевдоним устройства обязателен.", - "remove-alias": "Удалить псевдоним устройства", - "add-alias": "Добавить псевдоним устройства", - "name-starts-with": "Название начинается с", - "device-list": "Список устройств", - "use-device-name-filter": "Использовать фильтр", - "device-list-empty": "Устройства не выбраны.", - "device-name-filter-required": "Фильтр названия устройства обязателен.", - "device-name-filter-no-device-matched": "Устройства, названия которых начинаются с '{{device}}', не найдены.", - "add": "Добавить устройство", - "assign-to-customer": "Присвоить клиенту", - "assign-device-to-customer": "Присвоить устройство(а) клиенту", - "assign-device-to-customer-text": "Пожалуйста, выберите устройства, которые нужно присвоить клиенту", - "make-public": "Открыть общий доступ к устройству", - "make-private": "Закрыть общий доступ к устройству", - "no-devices-text": "Устройства не найдены", - "assign-to-customer-text": "Пожалуйста, выберите клиента, которому нужно присвоить устройство(а)", - "device-details": "Подробности об устройстве", - "add-device-text": "Добавить новое устройство", - "credentials": "Учетные данные", - "manage-credentials": "Управление учетными данными", - "delete": "Удалить устройство", - "assign-devices": "Присвоить устройство", - "assign-devices-text": "Присвоить { count, plural, =1 {1 устройство} few {# устройства} other {# устройств} } клиенту", - "delete-devices": "Удалить устройства", - "unassign-from-customer": "Отозвать у клиенту", - "unassign-devices": "Отозвать устройства", - "unassign-devices-action-title": "Отозвать у клиента { count, plural, =1 {1 устройство} few {# устройства} other {# устройств} }", - "assign-new-device": "Присвоить новое устройство", - "make-public-device-title": "Вы точно хотите открыть общий доступ к устройству '{{deviceName}}'?", - "make-public-device-text": "После подтверждения устройство и все связанные с ним данные будут общедоступными.", - "make-private-device-title": "Вы точно хотите закрыть общий доступ к устройству '{{deviceName}}'", - "make-private-device-text": "После подтверждения устройство и все связанные с ним данные будут закрыты для общего доступа.", - "view-credentials": "Просмотреть учетные данные", - "delete-device-title": "Вы точно хотите удалить устройство '{{deviceName}}'?", - "delete-device-text": "Внимание, после подтверждения устройство и все связанные с ним данные будут безвозвратно утеряны.", - "delete-devices-title": "Вы точно хотите удалить { count, plural, =1 {1 устройство} few {# устройства} other {# устройств} }?", - "delete-devices-action-title": "Удалить { count, plural, =1 {1 устройство} few {# устройства} other {# устройств} }", - "delete-devices-text": "Внимание, после подтверждения выбранные устройства и все связанные с ними данные будут безвозвратно утеряны..", - "unassign-device-title": "Вы точно хотите отозвать устройство '{{deviceName}}'?", - "unassign-device-text": "После подтверждения устройство будет недоступно клиенту.", - "unassign-device": "Отозвать устройство", - "unassign-devices-title": "Вы точно хотите отозвать { count, plural, =1 {1 устройство} few {# устройства} other {# устройств} }?", - "unassign-devices-text": "После подтверждения выбранные устройства будут недоступны клиенту.", - "device-credentials": "Учетные данные устройства", - "credentials-type": "Тип учетных данных", - "access-token": "Токен", - "access-token-required": "Токен обязателен.", - "access-token-invalid": "Длина токена должна быть от 1 до 32 символов.", - "secret": "Секрет", - "secret-required": "Секрет обязателен.", - "device-type": "Тип устройства", - "device-type-required": "Тип устройства обязатеен.", - "select-device-type": "Выберите тип устройства", - "enter-device-type": "Введите тип устройства", - "any-device": "Любое устройство", - "no-device-types-matching": "Тип устройства, соответствующий '{{entitySubtype}}', не найден.", - "device-type-list-empty": "Не выбран тип устройства.", - "device-types": "Типы устройств", - "name": "Название", - "name-required": "Название обязательно.", - "description": "Описание", - "events": "События", - "details": "Подробности", - "copyId": "Копировать идентификатор устройства", - "copyAccessToken": "Копировать токен", - "idCopiedMessage": "Идентификатор устройства скопирован в буфер обмена", - "accessTokenCopiedMessage": "Токен устройства скопирован в буфер обмена", - "assignedToCustomer": "Присвоен клиенту", - "unable-delete-device-alias-title": "Не удалось удалить псевдоним устройства", - "unable-delete-device-alias-text": "Не удалось удалить псевдоним '{{deviceAlias}}' устройства, т.к. он используется следующими виджетами:
{{widgetsList}}", - "is-gateway": "Гейтвей", - "public": "Общедоступный", - "device-public": "Устройство общедоступно", - "select-device": "Выбрать устройство", - "import": "Импортировать устройства", - "device-file": "Файл с устройствами" - }, - "dialog": { - "close": "Закрыть диалог" - }, - "direction": { - "column": "Колонка", - "row": "Строка" - }, - "error": { - "unable-to-connect": "Не удалось подключиться к серверу! Пожалуйста, проверьте интернет-соединение.", - "unhandled-error-code": "Код необработанной ошибки: {{errorCode}}", - "unknown-error": "Неизвестная ошибка" - }, - "entity": { - "entity": "Объект", - "entities": "Объекты", - "aliases": "Псевдонимы объекта", - "entity-alias": "Псевдоним объекта", - "unable-delete-entity-alias-title": "Не удалось удалить псевдоним объекта", - "unable-delete-entity-alias-text": "Псевдоним объекта '{{entityAlias}}' не может быть удален, так как используется следующим(и) виджетом(ами):
{{widgetsList}}", - "duplicate-alias-error": "Найден дубликат псевдонима '{{alias}}'.
В рамках одного дашборда псевдонимы объектов должны быть уникальными.", - "missing-entity-filter-error": "Отсутствует фильтр для псевдонима '{{alias}}'.", - "configure-alias": "Настроить псевдоним '{{alias}}'", - "alias": "Псевдоним", - "alias-required": "Псевдоним объекта обязателен.", - "remove-alias": "Убрать псевдоним объекта", - "add-alias": "Добавить псевдоним объекта", - "entity-list": "Список объектов", - "entity-type": "Тип объекта", - "entity-types": "Типы объекта", - "entity-type-list": "Список типов объекта", - "any-entity": "Любой объект", - "enter-entity-type": "Введите тип объекта", - "no-entities-matching": "Объекты, соответствующие '{{entity}}', не найдены.", - "no-entity-types-matching": "Типы объектов, соответствующие '{{entityType}}', не найдены.", - "name-starts-with": "Название, начинающееся с", - "use-entity-name-filter": "Использовать фильтр", - "entity-list-empty": "Не выбраны объекты.", - "entity-name-filter-required": "Фильтр по названию объекта обязателен.", - "entity-name-filter-no-entity-matched": "Объекты, чье название начинается с '{{entity}}', не найдены.", - "all-subtypes": "Все", - "select-entities": "Выберите объекты", - "no-aliases-found": "Псевдонимы не найдены.", - "no-alias-matching": "Псевдоним '{{alias}}' не найден.", - "create-new-alias": "Создать новый!", - "key": "Ключ", - "key-name": "Название ключа", - "no-keys-found": "Ключ не найден.", - "no-key-matching": "Ключ '{{key}}' не найден.", - "create-new-key": "Создать новый!", - "type": "Тип", - "type-required": "Тип объекта обязателен.", - "type-device": "Устройство", - "type-devices": "Устройства", - "list-of-devices": "{ count, plural, =1 {Одно устройство} other {Список из # устройств} }", - "device-name-starts-with": "Устройства, чьи название начинается с '{{prefix}}'", - "type-asset": "Актив", - "type-assets": "Активы", - "list-of-assets": "{ count, plural, =1 {Один актив} other {Список из # активов} }", - "asset-name-starts-with": "Активы, чьи название начинается с '{{prefix}}'", - "type-entity-view": "Представление Объекта", - "type-entity-views": "Представления Объекта", - "list-of-entity-views": "{ count, plural, =1 {Одно представление объекта} other {Список из # представлений объекта} }", - "entity-view-name-starts-with": "Представления Объекта, чьи название начинается с '{{prefix}}'", - "type-rule": "Правило", - "type-rules": "Правила", - "list-of-rules": "{ count, plural, =1 {Одно правило} other {Список из # правил} }", - "rule-name-starts-with": "Правила, чьи названия начинаются с '{{prefix}}'", - "type-plugin": "Плагин", - "type-plugins": "Плагины", - "list-of-plugins": "{ count, plural, =1 {Один плагин} other {Список из # плагинов} }", - "plugin-name-starts-with": "Плагины, чьи имена начинаются с '{{prefix}}'", - "type-tenant": "Владелец", - "type-tenants": "Владельцы", - "list-of-tenants": "{ count, plural, =1 {Один владелец} other {Список из # владельцев} }", - "tenant-name-starts-with": "Владельцы, чьи имена начинаются с '{{prefix}}'", - "type-customer": "Клиент", - "type-customers": "Клиенты", - "list-of-customers": "{ count, plural, =1 {Один клиент} other {Список из # клиентов} }", - "customer-name-starts-with": "Клиенты, чьи имена начинаются с '{{prefix}}'", - "type-user": "Пользователь", - "type-users": "Пользователи", - "list-of-users": "{ count, plural, =1 {Один пользователь} other {Список из # пользователей} }", - "user-name-starts-with": "Пользователи, чьи имена начинаются с '{{prefix}}'", - "type-dashboard": "Дашборд", - "type-dashboards": "Дашборды", - "list-of-dashboards": "{ count, plural, =1 {Один дашборд} other {Список из # дашбордов} }", - "dashboard-name-starts-with": "Дашборды, чьи названия начинаются с '{{prefix}}'", - "type-alarm": "Оповещение", - "type-alarms": "Оповещения", - "list-of-alarms": "{ count, plural, =1 {Одно оповещение} other {Список из # оповещений} }", - "alarm-name-starts-with": "Оповещения, чьи названия начинаются с '{{prefix}}'", - "type-rulechain": "Цепочка правил", - "type-rulechains": "Цепочки правил", - "list-of-rulechains": "{ count, plural, =1 {Одна цепочка правил} other {Список из # цепочек правил} }", - "rulechain-name-starts-with": "Цепочки правил, чьи названия начинаются с '{{prefix}}'", - "type-rulenode": "Правило", - "type-rulenodes": "Правила", - "list-of-rulenodes": "{ count, plural, =1 {Одно правило} other {Список из # правил} }", - "rulenode-name-starts-with": "Правила, чьи названия начинаются с '{{prefix}}'", - "type-current-customer": "Текущий клиент", - "type-current-tenant": "Текущий владелец", - "search": "Поиск объектов", - "selected-entities": "Выбран(ы) { count, plural, =1 {1 объект} few {# объекта} other {# объектов} }", - "entity-name": "Название объекта", - "entity-label": "Метка объекта", - "details": "Подробности об объекте", - "no-entities-prompt": "Объекты не найдены", - "no-data": "Нет данных для отображения", - "columns-to-display": "Отобразить следующие колонки" - }, - "entity-field": { - "created-time": "Время создания", - "name": "Название", - "type": "Тип", - "first-name": "Имя", - "last-name": "Фамилия", - "email": "Электронная почта", - "title": "Название", - "country": "Страна", - "state": "Штат/Область", - "city": "Город", - "address": "Адрес", - "address2": "Адрес 2", - "zip": "Индекс", - "phone": "Телефон", - "label": "Метка" - }, - "entity-view": { - "entity-view": "Представление Объекта", - "entity-view-required": "Представление объекта обязательно.", - "entity-views": "Представления Объектов", - "management": "Управление представлениями объектов", - "view-entity-views": "Просмотр представлений объектов", - "entity-view-alias": "Псевдоним Представления Объекта", - "aliases": "Псевдонимы Представления Объекта", - "no-alias-matching": "Псевдоним '{{alias}}' не найден.", - "no-aliases-found": "Псевдонимы не найдены.", - "no-key-matching": "Ключ '{{key}}' не найден.", - "no-keys-found": "Ключи не найдены.", - "create-new-alias": "Создать новый!", - "create-new-key": "Создать новый!", - "duplicate-alias-error": "Найден дубликат псевдонима '{{alias}}'.
В рамках одного дашборда псевдонимы представлений объектов должны быть уникальными.", - "configure-alias": "Настроить псевдоним '{{alias}}'", - "no-entity-views-matching": "Объекты, соответствующие '{{entity}}', не найдены.", - "alias": "Псевдоним", - "alias-required": "Псевдоним представления объекта обязателен.", - "remove-alias": "Убрать псевдоним представления объекта", - "add-alias": "Добавить псевдоним представления объекта", - "name-starts-with": "Представления объектов, чьи название начинается с", - "entity-view-list": "Список представлений объектов", - "use-entity-view-name-filter": "Использовать фильтр", - "entity-view-list-empty": "Не выбраны представления объектов.", - "entity-view-name-filter-required": "Для представлений объектов фильтр по названиям обязателен.", - "entity-view-name-filter-no-entity-view-matched": "Представление объектов, чьи название начинаются с '{{entityView}}', не найдены.", - "add": "Представление объекта", - "assign-to-customer": "Назначить клиенту", - "assign-entity-view-to-customer": "Назначить представление(я) объекта(ов) клиенту", - "assign-entity-view-to-customer-text": "Пожалуйста, выберите представления объектов, которые нужно назначить клиенту", - "no-entity-views-text": "Представления объектов не найдены", - "assign-to-customer-text": "Пожалуйста, выберите клиента, которому нужно назначить представления объектов", - "entity-view-details": "Подробности о представлении объекта", - "add-entity-view-text": "Добавить новое представление объекта", - "delete": "Удалить представление объекта", - "assign-entity-views": "Назначить представления объектов", - "assign-entity-views-text": "Назначить клиенту { count, plural, =1 {1 представление объекта} few {# представления объектов} other {# представлений объектов} }", - "delete-entity-views": "Удалить представления объектов", - "unassign-from-customer": "Отозвать у клиента", - "unassign-entity-views": "Отозвать представления объектов", - "unassign-entity-views-action-title": "Отозвать у клиента { count, plural, =1 {1 представление объекта} few {# представления объектов} other {# представлений объектов} }", - "assign-new-entity-view": "Назначит новое представление объекта", - "delete-entity-view-title": "Вы точно хотите удалить представление объекта '{{entityViewName}}'?", - "delete-entity-view-text": "Внимание, после подтверждения представление объекта и все связанные с ним данные будут безвозвратно удалены.", - "delete-entity-views-title": "Вы точно хотите удалить { count, plural, =1 {1 представление объекта} few {# представления объектов} other {# представлений объектов} }?", - "delete-entity-views-action-title": "Удалить { count, plural, =1 {1 представление объекта} few {# представления объектов} other {# представлений объектов} }", - "delete-entity-views-text": "Внимание, после подтверждения выбранные представления объектов и все связанные с ними данные будут безвозвратно удалены.", - "unassign-entity-view-title": "Вы точно хотите отозвать представление объекта '{{entityViewName}}'?", - "unassign-entity-view-text": "После подтверждение представление объекта будет недоступно клиенту.", - "unassign-entity-view": "Отозвать представление объекта", - "unassign-entity-views-title": "Вы точно хотите отозвать { count, plural, =1 {1 представление объекта} few {# представления объектов} other {# представлений объектов} }?", - "unassign-entity-views-text": "После подтверждение выбранные представления объектов будет недоступно клиенту.", - "entity-view-type": "Тип представления объекта", - "entity-view-type-required": "Тип представления объекта обязателен.", - "select-entity-view-type": "Выберите тип представления объекта", - "enter-entity-view-type": "Введите тип представления объекта", - "any-entity-view": "Любое представление объекта", - "no-entity-view-types-matching": "Типы представления объекта, соответствующие '{{entitySubtype}}', не найдены.", - "entity-view-type-list-empty": "Не выбраны типы представления объекта.", - "entity-view-types": "Типы представления объекта", - "name": "Название", - "name-required": "Название обязательно.", - "description": "Описание", - "events": "События", - "details": "Подробности", - "copyId": "Копировать ИД представление объекта", - "assignedToCustomer": "Назначено клиенту", - "unable-entity-view-device-alias-title": "Не удалось удалить псевдоним представления объекта", - "unable-entity-view-device-alias-text": "Не удалось удалить псевдоним устройства '{{entityViewAlias}}', т.к. он используется следующими виджетами:
{{widgetsList}}", - "select-entity-view": "Выбрать представление объекта", - "make-public": "Открыть общий доступ к представлению объекта", - "make-private": "Закрыть общий доступ к представлению объекта", - "start-date": "Дата начала", - "start-ts": "Время начала", - "end-date": "Дата окончания", - "end-ts": "Время окончания", - "date-limits": "Временной лимит", - "client-attributes": "Клиентские атрибуты", - "shared-attributes": "Общие атрибуты", - "server-attributes": "Серверные атрибуты", - "timeseries": "Телеметрия", - "client-attributes-placeholder": "Клиентские атрибуты", - "shared-attributes-placeholder": "Общие атрибуты", - "server-attributes-placeholder": "Серверные атрибуты", - "timeseries-placeholder": "Телеметрия", - "target-entity": "Целевой объект", - "attributes-propagation": "Пробросить атрибуты", - "attributes-propagation-hint": "Представление объекта автоматически копирует указанные атрибуты с Целевого Объекта каждый раз, когда вы сохраняете или обновляете это представление. В целях производительности атрибуты целевого объекта не пробрасываются в представление объекта на каждом их изменении. Вы можете включить автоматический проброс, настроив в вашей цепочке правило \"copy to view\" и соединив его с сообщениями типа \"Post attributes\" и \"Attributes Updated\".", - "timeseries-data": "Данные телеметрии", - "timeseries-data-hint": "Настроить ключи данных телеметрии целевого объекта, которые будут доступны представлению объекта. Эти данные только для чтения.", - "make-public-entity-view-title": "Вы уверенны, что хотите открыть общий доступ к представленю объекта '{{entityViewName}}'?", - "make-public-entity-view-text": "После подтверждения представление объекта и все связанные с ним данные станут публичными и доступными для других пользователей.", - "make-private-entity-view-title": "Вы уверенны, что хотите закрыть общий доступ к представлению объекта '{{entityViewName}}'?", - "make-private-entity-view-text": "После подтверждения представление объекта и все звязанные с ним данные станут приватными и не будут доступны для других пользователей." - }, - "event": { - "event-type": "Тип события", - "type-error": "Ошибка", - "type-lc-event": "Событие жизненного цикла", - "type-stats": "Статистика", - "type-debug-rule-node": "Отладка", - "type-debug-rule-chain": "Отладка", - "no-events-prompt": "События не найдены", - "error": "Ошибка", - "alarm": "Аварийное оповещение", - "event-time": "Время возникновения события", - "server": "Сервер", - "body": "Тело", - "method": "Метод", - "type": "Тип", - "message-id": "ИД сообщения", - "message-type": "Тип сообщения", - "data-type": "Тип данных", - "relation-type": "Тип отношения", - "metadata": "Метаданные", - "data": "Данные", - "event": "Событие", - "status": "Статус", - "success": "Успех", - "failed": "Неудача", - "messages-processed": "Сообщения обработаны", - "errors-occurred": "Возникли ошибки", - "all-events": "Все", - "entity-type": "Тип объекта", - "clear-request-title": "Удалить все события", - "clear-request-text": "Вы точно хотите удалить все события?" - }, - "extension": { - "extensions": "Расширение", - "selected-extensions": "Выбрано { count, plural, =1 {1 расширение} few {# расширения} other {# расширений} }", - "type": "Тип", - "key": "Ключ", - "value": "Значение", - "id": "ИД", - "extension-id": "ИД расширения", - "extension-type": "Тип расширения", - "transformer-json": "JSON *", - "unique-id-required": "Такое ИД расширения уже существует.", - "delete": "Удалить расширение", - "add": "Добавить расширение", - "edit": "Редактировать расширение", - "delete-extension-title": "Вы точно хотите удалить расширение '{{extensionId}}'?", - "delete-extension-text": "Внимание, после подтверждения расширение и все связанные с ним данные будут безвозвратно удалены.", - "delete-extensions-title": "Вы точно хотите удалить { count, plural, =1 {1 расширение} few {# расширения} other {# расширений} }?", - "delete-extensions-text": "Внимание, после подтверждения выбранные расширения будут удалены.", - "converters": "Конвертеры", - "converter-id": "ИД конвертера", - "configuration": "Конфигурация", - "converter-configurations": "Конфигурация конвертера", - "token": "Токен безопасности", - "add-converter": "Добавить конвертер", - "add-config": "Добавить конфигурацию конвертера", - "device-name-expression": "Маска названия устройства", - "device-type-expression": "Маска типа устройства", - "custom": "Пользовательский", - "to-double": "To Double", - "transformer": "Преобразователь", - "json-required": "JSON преобразователя обязателен.", - "json-parse": "Не удалось распознать JSON преобразователя.", - "attributes": "Атрибуты", - "add-attribute": "Добавить атрибут", - "add-map": "Добавить элемент сопоставления", - "timeseries": "Телеметрия", - "add-timeseries": "Добавить параметр телеметрии", - "field-required": "Параметр обязателен", - "brokers": "Брокеры", - "add-broker": "Добавить брокер", - "host": "Хост", - "port": "Порт", - "port-range": "Значение порта лежит в диапазоне от 1 до 65535.", - "ssl": "SSL", - "credentials": "Учетные данные", - "username": "Имя пользователя", - "password": "Пароль", - "retry-interval": "Интервал повтора в миллисекундах", - "anonymous": "Анонимный", - "basic": "Общий", - "pem": "PEM", - "ca-cert": "Файл CA сертификата *", - "private-key": "Файл приватного ключа *", - "cert": "Файл сертификата *", - "no-file": "Не выбран файл.", - "drop-file": "Перетяните файл или нажмите для выбора файла.", - "mapping": "Сопоставление", - "topic-filter": "Фильтр тем", - "converter-type": "Тип конвертера", - "converter-json": "JSON", - "json-name-expression": "JSON выражение для названия устройства", - "topic-name-expression": "Выражение для названия устройства в названии темы", - "json-type-expression": "JSON выражение для типа устройства", - "topic-type-expression": "Выражение для типа устройства в названии темы", - "attribute-key-expression": "Выражение для атрибута", - "attr-json-key-expression": "JSON выражение для атрибута", - "attr-topic-key-expression": "Выражение для атрибута в названии темы", - "request-id-expression": "Выражение для ИД запроса", - "request-id-json-expression": "JSON выражение для ИД запроса", - "request-id-topic-expression": "Выражение для ИД запроса в названии темы", - "response-topic-expression": "Выражение для темы ответов", - "value-expression": "Выражение для значения", - "topic": "Тема", - "timeout": "Таймаут в миллисекундах", - "converter-json-required": "JSON конвертер обязателен.", - "converter-json-parse": "Не удалось распознать JSON конвертера.", - "filter-expression": "Выражение для фильтрации", - "connect-requests": "Запросы о подключении устройства", - "add-connect-request": "Добавить запросы о подключении устройства", - "disconnect-requests": "Запросы об отсоединении устройства", - "add-disconnect-request": "Добавить запрос об отсоединении устройства", - "attribute-requests": "Запросы для атрибутов", - "add-attribute-request": "Добавить запрос для атрибутов", - "attribute-updates": "Обновление атрибутов", - "add-attribute-update": "Добавить обновление атрибутов", - "server-side-rpc": "Серверный RPC", - "add-server-side-rpc-request": "Добавить серверный RPC", - "device-name-filter": "Фильтр для названия устройства", - "attribute-filter": "Фильтр для атрибутов", - "method-filter": "Фильтр для процедур", - "request-topic-expression": "Выражение для темы запросов", - "response-timeout": "Время ожидания ответа в миллисекундах", - "topic-expression": "Выражение для названия темы", - "client-scope": "Клиентский", - "add-device": "Добавить устройство", - "opc-server": "Серверы", - "opc-add-server": "Добавить сервер", - "opc-add-server-prompt": "Пожалуйста, добавьте сервер", - "opc-application-name": "Название приложения", - "opc-application-uri": "URI приложения", - "opc-scan-period-in-seconds": "Частота сканирования в секундах", - "opc-security": "Безопасность", - "opc-identity": "Идентификация", - "opc-keystore": "Хранилище ключей", - "opc-type": "Тип", - "opc-keystore-type": "Тип", - "opc-keystore-location": "Расположение *", - "opc-keystore-password": "Пароль", - "opc-keystore-alias": "Псевдоним", - "opc-keystore-key-password": "Пароль для ключ", - "opc-device-node-pattern": "Паттерн OPC узла устройства", - "opc-device-name-pattern": "Паттерн названия устройства", - "modbus-server": "Серверы/ведомые устройства", - "modbus-add-server": "Добавить сервер/ведомое устройство", - "modbus-add-server-prompt": "Пожалуйста, добавить сервер/ведомое устройство", - "modbus-transport": "Транспорт", - "modbus-tcp-reconnect": "Переподключатсься автоматически", - "modbus-port-name": "Название последовательного порта", - "modbus-encoding": "Кодирование символов", - "modbus-parity": "Паритет", - "modbus-baudrate": "Скорость передачи", - "modbus-databits": "Биты данных", - "modbus-stopbits": "Стоп-биты", - "modbus-databits-range": "Параметр \"Биты данных\" может принимать значения 7 или 8.", - "modbus-stopbits-range": "Параметр \"Стоп-биты\" может принимать значения 1 или 2.", - "modbus-unit-id": "ИД устройства", - "modbus-unit-id-range": "ИД устройства должен быть в диапазоне от 1 до 247.", - "modbus-device-name": "Название устройства", - "modbus-poll-period": "Частота опроса (в миллисекундах)", - "modbus-attributes-poll-period": "Частота опроса для атрибутов (в миллисекундах)", - "modbus-timeseries-poll-period": "Частота опроса для телеметрии (в миллисекундах)", - "modbus-poll-period-range": "Значение параметра \"Частота опроса\" должно быть больше ноля.", - "modbus-tag": "Тег", - "modbus-function": "Modbus функция", - "modbus-register-address": "Адрес регистра", - "modbus-register-address-range": "Адрес регистра должен быть в диапазоне от 0 до 65535.", - "modbus-register-bit-index": "Номер бита", - "modbus-register-bit-index-range": "Номер бита должен быть в диапазоне от 0 до 15.", - "modbus-register-count": "Количество регистров", - "modbus-register-count-range": "Количество регистров должно быть больше ноля.", - "modbus-byte-order": "Порядок байтов", - "sync": { - "status": "Статус", - "sync": "Синхронизирован", - "not-sync": "Не синхронизирован", - "last-sync-time": "Время последней синхронизации", - "not-available": "Не доступен" - }, - "export-extensions-configuration": "Экспортировать конфигурацию расширений", - "import-extensions-configuration": "Импортировать конфигурацию расширений", - "import-extensions": "Импортировать расширения", - "import-extension": "Импортировать расширение", - "export-extension": "Экспортировать расширение", - "file": "Файл расширений", - "invalid-file-error": "Не правильный формат файла" - }, - "fullscreen": { - "expand": "Во весь экран", - "exit": "Выйти из полноэкранного режима", - "toggle": "Во весь экран", - "fullscreen": "Полноэкранный режим" - }, - "function": { - "function": "Функция" - }, - "grid": { - "delete-item-title": "Вы точно хотите удалить этот объект?", - "delete-item-text": "Внимание, после подтверждения объект и все связанные с ним данные будут безвозвратно утеряны.", - "delete-items-title": "Вы точно хотите удалить { count, plural, =1 {1 объект} few {# объекта} other {# объектов} }?", - "delete-items-action-title": "Удалить { count, plural, =1 {1 объект} few {# объекта} other {# объектов} }", - "delete-items-text": "Внимание, после подтверждения выбранные объекты и все связанные с ними данные будут безвозвратно утеряны.", - "add-item-text": "Добавить новый объект", - "no-items-text": "Объекты не найдены", - "item-details": "Подробности об объекте", - "delete-item": "Удалить объект", - "delete-items": "Удалить объекты", - "scroll-to-top": "Прокрутка к началу" - }, - "help": { - "goto-help-page": "Перейти к справке" - }, - "home": { - "home": "Главная", - "profile": "Профиль", - "logout": "Выйти из системы", - "menu": "Меню", - "avatar": "Аватар", - "open-user-menu": "Открыть меню пользователя" - }, - "import": { - "no-file": "Файл не выбран", - "drop-file": "Перетащите JSON файл или кликните для выбора файла.", - "drop-file-csv": "Перетащите CSV файл или кликните для выбора файла.", - "column-value": "Значение", - "column-title": "Название", - "column-example": "Пример значений данных", - "column-key": "Ключ атрибута/телеметрии", - "csv-delimiter": "Разделитель в CSV файле", - "csv-first-line-header": "Первая строка содержит названия колонок", - "csv-update-data": "Обновить атрибут/телеметрию", - "import-csv-number-columns-error": "Файл должен содержать как минимум две колонки", - "import-csv-invalid-format-error": "Неверный формат данных. Строка: '{{line}}'", - "column-type": { - "name": "Название", - "type": "Тип", - "label": "Метка", - "column-type": "Тип колонки", - "client-attribute": "Клиентский атрибут", - "shared-attribute": "Общий атрибут", - "server-attribute": "Серверный атрибут", - "timeseries": "Телеметрия", - "entity-field": "Entity field", - "access-token": "Токен" - }, - "stepper-text": { - "select-file": "Выберите файл", - "configuration": "Конфигурация импорта", - "column-type": "Выберите тип колонок", - "creat-entities": "Создание новых объектов" - }, - "message": { - "create-entities": "{{count}} новый(х) объект(ов) было успешно создано.", - "update-entities": "{{count}} объект(ов) успешно обновлено.", - "error-entities": "Возникла ошибка при создании {{count}} объекта(ов)." - } - }, - "item": { - "selected": "Выбранные" - }, - "js-func": { - "no-return-error": "Функция должна возвращать значение!", - "return-type-mismatch": "Функция должна возвращать значение типа '{{type}}'!" - }, - "key-val": { - "key": "Ключ", - "value": "Значение", - "remove-entry": "Удалить элемент", - "add-entry": "Добавить элемент", - "no-data": "Элементы отсутствуют" - }, - "layout": { - "layout": "Макет", - "manage": "Управление макетами", - "settings": "Настройки макета", - "color": "Цвет", - "main": "Основной", - "right": "Правый", - "select": "Выбрать макет" - }, - "legend": { - "direction": "Расположение элементов легенды", - "position": "Расположение легенды", - "show-max": "Показать максимальное значение", - "show-min": "Показать минимальное значение", - "show-avg": "Показать среднее значение", - "show-total": "Показать сумму", - "settings": "Настройки легенды", - "min": "Мин", - "max": "Макс", - "avg": "Среднее", - "total": "Сумма", - "comparison-time-ago": { - "days": "(день назад)", - "weeks": "(неделю назад)", - "months": "(месяц назад)", - "years": "(год назад)" - } - }, - "login": { - "login": "Войти", - "request-password-reset": "Запрос на сброс пароля", - "reset-password": "Сбросить пароль", - "create-password": "Создать пароль", - "passwords-mismatch-error": "Введенные пароли должны быть одинаковыми!", - "password-again": "Введите пароль еще раз", - "sign-in": "Пожалуйста, войдите в систему", - "username": "Имя пользователя (эл. адрес)", - "remember-me": "Запомнить меня", - "forgot-password": "Забыли пароль?", - "password-reset": "Пароль сброшен", - "expired-password-reset-message": "Срок действия Вашего пароля закончился! Пожалуйста, создайте новый пароль.", - "new-password": "Новый пароль", - "new-password-again": "Повторите новый пароль", - "password-link-sent-message": "Ссылка для сброса пароля была успешно отправлена!", - "email": "Эл. адрес", - "login-with": "Войти через {{name}}", - "or": "или" - }, - "position": { - "top": "Верх", - "bottom": "Низ", - "left": "Левый край", - "right": "Правый край" - }, - "profile": { - "profile": "Профиль", - "last-login-time": "Время последнего входа в систему", - "change-password": "Изменить пароль", - "current-password": "Текущий пароль", - "copy-jwt-token": "Копировать JWT токен", - "tokenCopiedMessage": "JWT токен скопирован в буфер обмена", - "tokenCopiedWarnMessage": "JWT токен недействителен! Перезагрузите страницу." - }, - "relation": { - "relations": "Отношения", - "direction": "Направления", - "search-direction": { - "FROM": "От", - "TO": "К" - }, - "direction-type": { - "FROM": "от", - "TO": "к" - }, - "from-relations": "Исходящие отношения", - "to-relations": "Входящие отношения", - "selected-relations": "Выбрано { count, plural, =1 {1 отношение} few {# отношения} other {# отношений} }", - "type": "Тип", - "to-entity-type": "К типу объекта", - "to-entity-name": "К объекта", - "from-entity-type": "От типа объекта", - "from-entity-name": "От объекта", - "to-entity": "К объекту", - "from-entity": "От объекта", - "delete": "Удалить отношение", - "relation-type": "Тип отношения", - "relation-type-required": "Тип отношения обязателен.", - "any-relation-type": "Любой тип", - "add": "Добавить отношение", - "edit": "Редактировать отношение", - "delete-to-relation-title": "Вы точно хотите удалить отношение, идущее к объекту '{{entityName}}'?", - "delete-to-relation-text": "Внимание, после подтверждения объект '{{entityName}}' будет отвязан от текущего объекта.", - "delete-to-relations-title": "Вы точно хотите удалить { count, plural, =1 {1 отношение} few {# отношения} other {# отношений} }?", - "delete-to-relations-text": "Внимание, после подтверждения выбранные объекты будут отвязаны от текущего объекта.", - "delete-from-relation-title": "Вы точно хотите удалить отношение, идущее от объекта '{{entityName}}'?", - "delete-from-relation-text": "Внимание, после подтверждения текущий объект будет отвязан от объекта '{{entityName}}'.", - "delete-from-relations-title": "Вы точно хотите удалить { count, plural, =1 {1 отношение} few {# отношения} other {# отношений} }?", - "delete-from-relations-text": "Внимание, после подтверждения выбранные объекты будут отвязаны от соответствующих объектов.", - "remove-relation-filter": "Удалить фильтр отношений", - "add-relation-filter": "Добавить фильтр отношений", - "any-relation": "Любое отношение", - "relation-filters": "Фильтры отношений", - "additional-info": "Дополнительная информация (JSON)", - "invalid-additional-info": "Не удалось распознать JSON с дополнительной информацией." - }, - "rulechain": { - "rulechain": "Цепочка правил", - "rulechains": "Цепочки правил", - "root": "Корневая", - "delete": "Удалить цепочку правил", - "name": "Названия", - "name-required": "Название необходимо.", - "description": "Описание", - "add": "Добавить цепочку правил", - "set-root": "Сделать цепочку корневой", - "set-root-rulechain-title": "Вы точно хотите сделать цепочку правил '{{ruleChainName}}' корневой?", - "set-root-rulechain-text": "После подтверждения цепочка правил станет корневой и будет обрабатывать все входящие сообщения.", - "delete-rulechain-title": "Вы точно хотите удалить цепочку правил '{{ruleChainName}}'?", - "delete-rulechain-text": "Внимание, после подтверждения цепочка правил и все связанные с ней данные будут безвозвратно удалены.", - "delete-rulechains-title": "Вы точно хотите удалить { count, plural, =1 {1 цепочку правил} few {# цепочки правил} other {# цепочек правил} }?", - "delete-rulechains-action-title": "Удалить { count, plural, =1 {1 цепочку правил} few {# цепочки правил} other {# цепочек правил} }", - "delete-rulechains-text": "Внимание, после подтверждения выбранные цепочки правил и все связанные с ними данные будут безвозвратно удалены.", - "add-rulechain-text": "Добавить новую цепочку правил", - "no-rulechains-text": "Цепочки правил не найдены", - "rulechain-details": "Подробности о цепочке правил", - "details": "Подробности", - "events": "События", - "system": "Системная", - "import": "Импортировать цепочку правил", - "export": "Экспортировать цепочку правил", - "export-failed-error": "Не удалось экспортировать цепочку правил: {{error}}", - "create-new-rulechain": "Создать новую цепочку правил", - "rulechain-file": "Файл цепочки правил", - "invalid-rulechain-file-error": "Не удалось импортировать цепочку правил: неправильный формат.", - "copyId": "Копировать ИД цепочки правил", - "idCopiedMessage": "ИД цепочки правил скопирован в буфер обмена", - "select-rulechain": "Выбрать цепочку правил", - "no-rulechains-matching": "Цепочки правил, соответствующие '{{entity}}', не найдены.", - "rulechain-required": "Цепочка правил обязательна", - "management": "Управление цепочками правил", - "debug-mode": "Режим отладки" - }, - "rulenode": { - "details": "Подробности", - "events": "События", - "search": "Поиск правил", - "open-node-library": "Открыть библиотеку правил", - "add": "Добавить правило", - "name": "Название", - "name-required": "Название обязательно.", - "type": "Тип", - "delete": "Удалить правило", - "select-all-objects": "Выделить все правила и связи", - "deselect-all-objects": "Отменить выделение правил и связей", - "delete-selected-objects": "Удалить выделенные правила и связи", - "delete-selected": "Удалить выделенные", - "select-all": "Выделить всё", - "copy-selected": "Копировать выделенное", - "deselect-all": "Отменить выделение", - "rulenode-details": "Подробности о правиле", - "debug-mode": "Режим отладки", - "configuration": "Настройки", - "link": "Связь", - "link-details": "Подробности о связи правила", - "add-link": "Добавить связь", - "link-label": "Метка связи", - "link-label-required": "Метка связи обязателен.", - "custom-link-label": "Пользовательская метка связи", - "custom-link-label-required": "Пользовательская метка связи обязателен.", - "link-labels": "Метки связи", - "link-labels-required": "Метки связи обязательны.", - "no-link-labels-found": "Метки связи не найдены", - "no-link-label-matching": "Метка '{{label}}' не найдена.", - "create-new-link-label": "Создать новую!", - "type-filter": "Фильтр", - "type-filter-details": "Фильтр входящих сообщений с заданными условиями", - "type-enrichment": "Насыщение", - "type-enrichment-details": "Добавить данные в метадату сообщения", - "type-transformation": "Преобразование", - "type-transformation-details": "Изменить содержимое сообщение и его метадату", - "type-action": "Действие", - "type-action-details": "Выполнить заданное действие", - "type-external": "Сторонние", - "type-external-details": "Взаимодействовать со сторонними системами", - "type-rule-chain": "Цепочка правил", - "type-rule-chain-details": "Перенаправить входящее сообщение в другую цепочку правил", - "type-input": "Вход", - "type-input-details": "Логический вход цепочки правил перенаправляет входящие сообщения в следующее правило", - "type-unknown": "Неизвестный", - "type-unknown-details": "Неопределенное правило", - "directive-is-not-loaded": "Указанная директива конфигурации '{{directiveName}}' не доступна.", - "ui-resources-load-error": "Не удалось загрузить UI ресурсы.", - "invalid-target-rulechain": "Не удалось определить целевую цепочку правил!", - "test-script-function": "Протестировать скрипт", - "message": "Сообщение", - "message-type": "Тип сообщения", - "select-message-type": "Выбрать тип сообщения", - "message-type-required": "Тип сообщения обязателен", - "metadata": "Метаданные", - "metadata-required": "Метаданные объекта не могут быть пустыми.", - "output": "Выход", - "test": "Протестировать", - "help": "Помощь", - "reset-debug-mode": "Сбросить режим отладки во всех правилах" - }, - "queue": { - "select_name": "Выберите имя для Queue", - "name": "Имя для Queue", - "name_required": "Поле 'Имя для Queue' обязательно к заполнению!" - }, - "tenant": { - "tenant": "Владелец", - "tenants": "Владельцы", - "management": "Управление владельцами", - "add": "Добавить владельца", - "admins": "Администраторы", - "manage-tenant-admins": "Управление администраторами владельца", - "delete": "Удалить владельца", - "add-tenant-text": "Добавить нового владельца", - "no-tenants-text": "Владельцы не найдены", - "tenant-details": "Подробности об владельце", - "delete-tenant-title": "Вы точно хотите удалить владельца '{{tenantTitle}}'?", - "delete-tenant-text": "Внимание, после подтверждения владелец и все связанные с ним данные будут безвозвратно утеряны.", - "delete-tenants-title": "Вы точно хотите удалить { count, plural, =1 {1 владельца} other {# владельцев} }?", - "delete-tenants-action-title": "Удалить { count, plural, =1 {1 владельца} other {# владельцев} }", - "delete-tenants-text": "Внимание, после подтверждения выбранные Владельцы и все связанные с ними данные будут безвозвратно утеряны.", - "title": "Имя", - "title-required": "Имя обязательно.", - "description": "Описание", - "details": "Подробности", - "events": "События", - "copyId": "Копировать ИД владельца", - "idCopiedMessage": "ИД владельца скопирован в буфер обмена", - "select-tenant": "Выбрать владельца", - "no-tenants-matching": "Владельцы, соответствующие '{{entity}}', не найдены.", - "tenant-required": "Владелец обязателен" - }, - "timeinterval": { - "seconds-interval": "{ seconds, plural, =1 {1 секунда} few {# секунды} other {# секунд} }", - "minutes-interval": "{ minutes, plural, =1 {1 минута} few {# минуты} other {# минут} }", - "hours-interval": "{ hours, plural, =1 {1 час} few {# часа} other {# часов} }", - "days-interval": "{ days, plural, =1 {1 день} few {# дня} other {# дней} }", - "days": "Дни", - "hours": "Часы", - "minutes": "Минуты", - "seconds": "Секунды", - "advanced": "Дополнительно" - }, - "timewindow": { - "days": "{ days, plural, =1 {1 день} few {# дня} other {# дней} }", - "hours": "{ hours, plural, =1 {1 час} few {# часа} other {# часов} }", - "minutes": "{ minutes, plural, =1 {1 минута} few {# минуты} other {# минут} }", - "seconds": "{ seconds, plural, =1 {1 секунда} few {# секунды} other {# секунд} }", - "realtime": "Режим реального времени", - "history": "История", - "last-prefix": "Последние", - "period": "с {{ startTime }} до {{ endTime }}", - "edit": "Изменить временное окно", - "date-range": "Диапазон дат", - "last": "Последние", - "time-period": "Период времени", - "hide": "Скрыть" - }, - "user": { - "user": "Пользователь", - "users": "Пользователи", - "customer-users": "Пользователи клиента", - "tenant-admins": "Администраторы владельца", - "sys-admin": "Системный администратор", - "tenant-admin": "Администратор владельца", - "customer": "Клиент", - "anonymous": "Аноним", - "add": "Добавить пользователя", - "delete": "Удалить пользователя", - "add-user-text": "Добавить нового пользователя", - "no-users-text": "Пользователи не найдены", - "user-details": "Подробности о пользователе", - "delete-user-title": "Вы точно хотите удалить пользователя '{{userEmail}}'?", - "delete-user-text": "Внимание, после подтверждения пользователь и все связанные с ним данные будут безвозвратно утеряны.", - "delete-users-title": "Вы точно хотите удалить { count, plural, =1 {1 пользователя} other {# пользователей} }?", - "delete-users-action-title": "Удалить { count, plural, =1 {1 пользователя} other {# пользователей} }", - "delete-users-text": "Внимание, после подтверждения выбранные пользователи и все связанные с ними данные будут безвозвратно утеряны.", - "activation-email-sent-message": "Активационное письмо успешно отправлено!", - "resend-activation": "Повторить отправку активационного письма", - "email": "Эл. адрес", - "email-required": "Эл. адрес обязателен.", - "invalid-email-format": "Неправильный формат эл. адреса'.", - "first-name": "Имя", - "last-name": "Фамилия", - "description": "Описание", - "default-dashboard": "Дашборд по умолчанию", - "always-fullscreen": "Всегда в полноэкранном режиме", - "select-user": "Выбрать пользователя", - "no-users-matching": "Пользователи, соответствующие '{{entity}}', не найдены.", - "user-required": "Необходимо указать пользователя", - "activation-method": "Метод активации", - "display-activation-link": "Отобразить ссылку для активации", - "send-activation-mail": "Отправить активационное письмо", - "activation-link": "Активационная ссылка для пользователя", - "activation-link-text": "Для активации пользователя используйте ссылку :", - "copy-activation-link": "Копировать активационную ссылку", - "activation-link-copied-message": "Ссылка для активации пользователя скопирована в буфер обмена", - "details": "Подробности", - "login-as-tenant-admin": "Войти как администратор владельца", - "login-as-customer-user": "Войти как пользователь клиента", - "disable-account": "Отключить учетную запись пользователя", - "enable-account": "Включить учетную запись пользователя", - "enable-account-message": "Учетная запись пользователя была успешно включена!", - "disable-account-message": "Учетная запись пользователя была успешно отключена!", - "copyId": "Копировать ИД пользователя", - "idCopiedMessage": "ИД пользователя скопирован в буфер обмена" - }, - "value": { - "type": "Тип значения", - "string": "Строка", - "string-value": "Строковое значение", - "integer": "Целое число", - "integer-value": "Целочисленное значение", - "invalid-integer-value": "Неправильный формат целого числа", - "double": "Число двойной точности", - "double-value": "Значение двойной точности", - "boolean": "Логический тип", - "boolean-value": "Логическое значение", - "false": "Ложь", - "true": "Правда", - "long": "Целое число" - }, - "widget": { - "widget-library": "Галерея виджетов", - "widget-bundle": "Набор виджетов", - "select-widgets-bundle": "Выберите набор виджетов", - "management": "Управление виджетами", - "editor": "Редактор виджетов", - "widget-type-not-found": "Ошибка при загрузке конфигурации виджета.
Возможно, связанный с ней\n тип виджета уже удален.", - "widget-type-load-error": "Не удалось загрузить виджет по следующим причинам:", - "remove": "Удалить виджет", - "edit": "Редактировать виджет", - "remove-widget-title": "Вы точно хотите удалить виджет '{{widgetTitle}}'?", - "remove-widget-text": "Внимание, после подтверждения виджет и все связанные с ним данные будут безвозвратно утеряны.", - "timeseries": "Телеметрия", - "search-data": "Поиск данных", - "no-data-found": "Данные не найдено", - "latest": "Последние значения", - "rpc": "Управляющий виджет", - "alarm": "Виджет оповещений", - "static": "Статический виджет", - "select-widget-type": "Выберите тип виджета", - "missing-widget-title-error": "Укажите название виджета!", - "widget-saved": "Виджет сохранен", - "unable-to-save-widget-error": "Не удалось сохранить виджет! Виджет содержит ошибки!", - "save": "Сохранить виджет", - "saveAs": "Сохранить виджет как", - "save-widget-type-as": "Сохранить тип виджета как", - "save-widget-type-as-text": "Пожалуйста, введите название виджета и/или укажите целевой набор виджетов", - "toggle-fullscreen": "Во весь экран", - "run": "Запустить виджет", - "title": "Название виджета", - "title-required": "Название виджета обязательно.", - "type": "Тип виджета", - "resources": "Ресурсы", - "resource-url": "JavaScript/CSS URL", - "remove-resource": "Удалить ресурс", - "add-resource": "Добавить ресурс", - "html": "HTML", - "tidy": "Форматировать", - "css": "CSS", - "settings-schema": "Схема конфигурации", - "datakey-settings-schema": "Схема конфигурации ключа данных", - "javascript": "Javascript", - "add-widget-type": "Добавить новый тип виджета", - "widget-template-load-failed-error": "Не удалось загрузить шаблон виджета!", - "add": "Добавить виджет", - "undo": "Откатить изменения в виджете", - "export": "Экспортировать виджет" - }, - "widget-action": { - "header-button": "Кнопка заголовка виджета", - "open-dashboard-state": "Перейти к новому состоянию дашборда", - "update-dashboard-state": "Обновить текущее состояние дашборда", - "open-dashboard": "Перейти к другому дашборду", - "custom": "Пользовательское действие", - "custom-pretty": "Пользовательское действие (с HTML шаблоном)", - "target-dashboard-state": "Целевое состояние дашборда", - "target-dashboard-state-required": "Целевое состояние дашборда обязательно", - "set-entity-from-widget": "Установить объект из виджета", - "target-dashboard": "Целевой дашборд", - "open-right-layout": "Открыть мобильный режим дашборда" - }, - "widgets-bundle": { - "current": "Текущий набор", - "widgets-bundles": "Наборы виджетов", - "add": "Добавить набор виджетов", - "delete": "Удалить набор виджетов", - "title": "Название", - "title-required": "Название обязательно.", - "add-widgets-bundle-text": "Добавить новый набор виджетов", - "no-widgets-bundles-text": "Наборы виджетов не найдены", - "empty": "Пустой набор виджетов", - "details": "Подробности", - "widgets-bundle-details": "Подробности о наборе виджетов", - "delete-widgets-bundle-title": "Вы точно хотите удалить набор виджетов '{{widgetsBundleTitle}}'?", - "delete-widgets-bundle-text": "Внимание, после подтверждения набор виджетов и все связанные с ним данные будут безвозвратно утеряны.", - "delete-widgets-bundles-title": "Вы точно хотите удалить { count, plural, =1 {1 набор виджетов} few {# набора виджетов} other {# наборов виджетов} }?", - "delete-widgets-bundles-action-title": "Удалить { count, plural, =1 {1 набор виджетов} few {# набора виджетов} other {# наборов виджетов} }", - "delete-widgets-bundles-text": "Внимание, после подтверждения выбранные наборы виджетов и все связанные с ними данные будут безвозвратно утеряны..", - "no-widgets-bundles-matching": "Набор виджетов '{{widgetsBundle}}' не найден.", - "widgets-bundle-required": "Набор виджетов обязателен.", - "system": "Системный", - "import": "Импортировать набор виджетов", - "export": "Экспортировать набор виджетов", - "export-failed-error": "Не удалось экспортировать набор виджетов: {{error}}", - "create-new-widgets-bundle": "Создать новый набор виджетов", - "widgets-bundle-file": "Файл набора виджетов", - "invalid-widgets-bundle-file-error": "Не удалось импортировать набор виджетов: неизвестная схема данных набора виджетов." - }, - "widget-config": { - "data": "Данные", - "settings": "Настройки", - "advanced": "Дополнительно", - "title": "Название", - "general-settings": "Общие настройки", - "display-title": "Показать название на виджете", - "drop-shadow": "Тень", - "enable-fullscreen": "Во весь экран", - "background-color": "Цвет фона", - "text-color": "Цвет текста", - "padding": "Отступ", - "margin": "Margin", - "widget-style": "Стиль виджета", - "title-style": "Стиль названия", - "mobile-mode-settings": "Мобильный режим", - "order": "Порядок", - "height": "Высота", - "units": "Специальный символ после значения", - "decimals": "Количество цифр после запятой", - "timewindow": "Временное окно", - "use-dashboard-timewindow": "Использовать временное окно дашборда", - "display-timewindow": "Показывать временное окно", - "legend": "Легенда", - "display-legend": "Показать легенду", - "datasources": "Источники данных", - "maximum-datasources": "Максимальной количество источников данных равно {{count}}", - "datasource-type": "Тип", - "datasource-parameters": "Параметры", - "remove-datasource": "Удалить источник данных", - "add-datasource": "Добавить источник данных", - "target-device": "Целевое устройство", - "alarm-source": "Источник оповещения", - "actions": "Действия", - "action": "Действие", - "add-action": "Добавить действие", - "search-actions": "Поиск действий", - "action-source": "Источник действий", - "action-source-required": "Источник действий обязателен.", - "action-name": "Название", - "action-name-required": "Название действия обязательно.", - "action-name-not-unique": "Действие с таким именем уже существует.
Название должно быть уникально в рамках одного источника действий.", - "action-icon": "Иконка", - "action-type": "Тип", - "action-type-required": "Тип действий обязателен.", - "edit-action": "Редактировать действие", - "delete-action": "Удалить действие", - "delete-action-title": "Удалить действие виджета", - "delete-action-text": "Вы точно хотите удалить действие виджета '{{actionName}}'?", - "title-icon": "Иконка в названии виджета", - "display-icon": "Показывать иконку в названии виджета", - "icon-color": "Цвет иконки", - "icon-size": "Размер иконки", - "advanced-settings": "Расширенные настройки", - "data-settings": "Настройки данных", - "no-data-display-message": "\"Нет данных для отображения\" альтернативный текст" - }, - "widget-type": { - "import": "Импортировать тип виджета", - "export": "Экспортировать тип виджета", - "export-failed-error": "Не удалось экспортировать тип виджета: {{error}}", - "create-new-widget-type": "Создать новый тип виджета", - "widget-type-file": "Файл типа виджета", - "invalid-widget-type-file-error": "Не удалось импортировать виджет: неизвестная схема данных типа виджета." - }, - "widgets": { - "date-range-navigator": { - "localizationMap": { - "Sun": "Вс", - "Mon": "Пн", - "Tue": "Вт", - "Wed": "Ср", - "Thu": "Чт", - "Fri": "Пт", - "Sat": "Сб", - "Jan": "Янв.", - "Feb": "Февр.", - "Mar": "Март", - "Apr": "Апр.", - "May": "Май", - "Jun": "Июнь", - "Jul": "Июль", - "Aug": "Авг.", - "Sep": "Сент.", - "Oct": "Окт.", - "Nov": "Нояб.", - "Dec": "Дек.", - "January": "Январь", - "February": "Февраль", - "March": "Март", - "April": "Апрель", - "June": "Июнь", - "July": "Июль", - "August": "Август", - "September": "Сентябрь", - "October": "Октября", - "November": "Ноябрь", - "December": "Декабрь", - "Custom Date Range": "Пользовательский диапазон дат", - "Date Range Template": "Шаблон диапазона дат", - "Today": "Сегодня", - "Yesterday": "Вчера", - "This Week": "На этой неделе", - "Last Week": "Прошлая неделя", - "This Month": "Этот месяц", - "Last Month": "Прошлый месяц", - "Year": "Год", - "This Year": "В этом году", - "Last Year": "Прошлый год", - "Date picker": "Выбор даты", - "Hour": "Час", - "Day": "День", - "Week": "Неделю", - "2 weeks": "2 Недели", - "Month": "Месяц", - "3 months": "3 Месяца", - "6 months": "6 Месяцев", - "Custom interval": "Пользовательский интервал", - "Interval": "Интервал", - "Step size": "Размер шага", - "Ok": "Ok" - } - }, - "input-widgets": { - "attribute-not-allowed": "Атрибут не может быть выбран в этом виджете", - "date": "Дата", - "blocked-location": "Геолокация заблокирована в вашем браузере", - "claim-device": "Подтвердить устройство", - "claim-failed": "Не удалось подтвердить устройство!", - "claim-not-found": "Устройство не найдено!", - "claim-successful": "Устройство успешно подтверждено!", - "discard-changes": "Отменить изменения", - "device-name": "Название устройства", - "device-name-required": "Необходимо указать название устройства", - "entity-attribute-required": "Значение атрибута обязателено", - "entity-coordinate-required": "Необходимо указать широту и долготу", - "entity-timeseries-required": "Значение телеметрии обязательно", - "get-location": "Получить текущее местоположение", - "latitude": "Широта", - "longitude": "Долгота", - "not-allowed-entity": "Выбраный объект не имеет общих атрибутов", - "no-attribute-selected": "Атрибут не выбран", - "no-datakey-selected": "Ни один datakey не выбран", - "no-entity-selected": "Объект не выбран", - "no-coordinate-specified": "Ключ для широты/долготы не указан", - "no-support-geolocation": "Ваш браузер не поддерживает геолокацию", - "no-image": "Нет изображения", - "no-support-web-camera": "Нет поддерживаемой веб-камеры", - "no-timeseries-selected": "Параметр телеметрии не выбран", - "secret-key": "Секретный ключ", - "secret-key-required": "Необходимо указать секретный ключ", - "switch-attribute-value": "Изменить значение атрибута", - "switch-camera": "Изменить камеру", - "switch-timeseries-value": "Изменить значение телеметрии", - "take-photo": "Сделать фото", - "time": "Время", - "timeseries-not-allowed": "Телеметрия не может быть выбрана в этом виджете", - "update-failed": "Не удалось обновить", - "update-successful": "Успешно обновлено", - "update-attribute": "Обновить атрибут", - "update-timeseries": "Обновить телеметрию", - "value": "Значение" - }, - "persistent-table": { - "rpc-id": "RPC ID", - "message-type": "Тип сообщения", - "method": "Метод", - "params": "Параметры", - "created-time": "Время создания", - "expiration-time": "Время жизни", - "retries": "Повторные попытки", - "status": "Статус", - "filter": "Фильтр", - "refresh": "Обновить", - "add": "Добавить RPC запрос", - "details": "Детали", - "delete": "Удалить", - "delete-request-title": "Удалить RPC запрос", - "delete-request-text": "Вы точно хотите удалить RPC запрос?", - "details-title": "Детали RPC ID: ", - "additional-info": "Дополнительная информация", - "response": "Ответ", - "any-status": "Любой статус", - "rpc-status-list": "Список RPC статусов", - "no-request-prompt": "Запросы не найдены", - "send-request": "Отправить запрос", - "add-title": "Добавить новый RPC запрос", - "method-error": "Метод обязателен.", - "white-space-error": "Пробелы не допускаются.", - "rpc-status": { - "QUEUED": "В ОЧЕРЕДИ", - "SENT": "ОТПРАВЛЕННО", - "DELIVERED": "ДОСТАВЛЕННО", - "SUCCESSFUL": "УСПЕШНО", - "TIMEOUT": "ВРЕМЯ ИСТЕКЛО", - "EXPIRED": "ПРОСРОЧЕНО", - "FAILED": "НЕУДАЧНО" - }, - "rpc-search-status-all": "ВСЕ", - "message-types": { - "false": "Двусторонний", - "true": "Односторонний" - } - } - }, - "icon": { - "icon": "Иконка", - "select-icon": "Выбрать иконку", - "material-icons": "Иконки в стиле Material", - "show-all": "Показать все иконки" - }, - "custom": { - "widget-action": { - "action-cell-button": "Кнопка действия в ячейке таблицы", - "row-click": "Действий при щелчке на строку", - "marker-click": "Действия при щелчке на маркер", - "polygon-click": "Действия при щелчке на полигон", - "tooltip-tag-action": "Действие при нажатии на ссылку в подсказке", - "node-selected": "Действий при выборе ноды", - "element-click": "Действий при щелчке на HTML элементе", - "pie-slice-click": "Действий при щелчке на секции круговой диаграммы", - "row-double-click": "Действий при двойном щелчке на строку" - } - }, - "language": { - "language": "Язык" - } -} From 9543cfd353fa99bf727cf19612a7e948e7166995 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Fri, 8 Dec 2023 14:26:40 +0200 Subject: [PATCH 34/38] Refactoring - renaming --- .../DefaultEdgeInstallInstructionsService.java | 7 ++----- .../DefaultEdgeUpgradeInstructionsService.java | 7 ++----- .../EdgeInstallInstructionsService.java | 2 +- .../EdgeUpgradeInstructionsService.java | 2 +- .../service/update/DefaultUpdateService.java | 14 ++++++-------- 5 files changed, 12 insertions(+), 20 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java index fef3c2644a..ba2cd06ee1 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.edge.instructions; import lombok.RequiredArgsConstructor; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -52,6 +53,7 @@ public class DefaultEdgeInstallInstructionsService implements EdgeInstallInstruc private boolean sslEnabled; @Value("${app.version:unknown}") + @Setter private String appVersion; @Override @@ -68,11 +70,6 @@ public class DefaultEdgeInstallInstructionsService implements EdgeInstallInstruc } } - @Override - public void updateApplicationVersion(String version) { - appVersion = version; - } - private EdgeInstructions getDockerInstallInstructions(Edge edge, HttpServletRequest request) { String dockerInstallInstructions = readFile(resolveFile("docker", "instructions.md")); String baseUrl = request.getServerName(); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java index 212fe781de..940a813fc6 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.edge.instructions; import lombok.RequiredArgsConstructor; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -48,6 +49,7 @@ public class DefaultEdgeUpgradeInstructionsService implements EdgeUpgradeInstruc private final InstallScripts installScripts; @Value("${app.version:unknown}") + @Setter private String appVersion; @Override @@ -65,11 +67,6 @@ public class DefaultEdgeUpgradeInstructionsService implements EdgeUpgradeInstruc } } - @Override - public void updateApplicationVersion(String version) { - appVersion = version; - } - @Override public void updateInstructionMap(Map map) { for (String key : map.keySet()) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallInstructionsService.java index 2c1ef4fe87..9df1f86e86 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallInstructionsService.java @@ -24,5 +24,5 @@ public interface EdgeInstallInstructionsService { EdgeInstructions getInstallInstructions(Edge edge, String installationMethod, HttpServletRequest request); - void updateApplicationVersion(String version); + void setAppVersion(String version); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java index e6f5cb0753..be40bc6c89 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java @@ -26,5 +26,5 @@ public interface EdgeUpgradeInstructionsService { void updateInstructionMap(Map upgradeVersions); - void updateApplicationVersion(String version); + void setAppVersion(String version); } diff --git a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java index 6a86d022df..48a4187efc 100644 --- a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java @@ -155,15 +155,13 @@ public class DefaultUpdateService implements UpdateService { .build()); } ObjectNode edgeRequest = JacksonUtil.newObjectNode().put(VERSION_PARAM, version); - String prevEdgeInstallVersion = edgeInstallVersion; - edgeInstallVersion = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v1/edge/installMapping", new HttpEntity<>(edgeRequest.toString(), headers), String.class); - if (edgeInstallVersion != null && !edgeInstallVersion.equals(prevEdgeInstallVersion)) { - edgeInstallInstructionsService.updateApplicationVersion(edgeInstallVersion); - edgeUpgradeInstructionsService.updateApplicationVersion(edgeInstallVersion); + String edgeInstallVersion = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v1/edge/installMapping", new HttpEntity<>(edgeRequest.toString(), headers), String.class); + if (edgeInstallVersion != null) { + edgeInstallInstructionsService.setAppVersion(edgeInstallVersion); + edgeUpgradeInstructionsService.setAppVersion(edgeInstallVersion); } - EdgeUpgradeMessage prevEdgeUpgradeMessage = edgeUpgradeMessage; - edgeUpgradeMessage = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v1/edge/upgradeMapping", new HttpEntity<>(edgeRequest.toString(), headers), EdgeUpgradeMessage.class); - if (edgeUpgradeMessage != null && !edgeUpgradeMessage.equals(prevEdgeUpgradeMessage)) { + EdgeUpgradeMessage edgeUpgradeMessage = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v1/edge/upgradeMapping", new HttpEntity<>(edgeRequest.toString(), headers), EdgeUpgradeMessage.class); + if (edgeUpgradeMessage != null) { edgeUpgradeInstructionsService.updateInstructionMap(edgeUpgradeMessage.getEdgeVersions()); } } catch (Exception e) { From cfe47f88b682127f0cf2bc97376e6a1537770ec0 Mon Sep 17 00:00:00 2001 From: ShvaykaD Date: Fri, 8 Dec 2023 14:32:26 +0200 Subject: [PATCH 35/38] rollback sorting in sql --- dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java | 3 +++ .../server/dao/asset/AssetProfileServiceImpl.java | 6 +++++- .../server/dao/device/DeviceProfileServiceImpl.java | 6 +++++- .../server/dao/sql/asset/AssetProfileRepository.java | 4 ++-- .../server/dao/sql/device/DeviceProfileRepository.java | 4 ++-- 5 files changed, 17 insertions(+), 6 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java index 1d9b276f65..1c4aed5393 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java @@ -32,6 +32,7 @@ import org.thingsboard.server.dao.model.ToData; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Optional; @@ -163,6 +164,7 @@ public abstract class DaoUtil { .collect(Collectors.toList()); } + @Deprecated // used only in deprecated DAO api public static List convertTenantEntityInfosToDto(UUID tenantUUID, EntityType entityType, List entityInfos) { if (CollectionUtils.isEmpty(entityInfos)) { return Collections.emptyList(); @@ -170,6 +172,7 @@ public abstract class DaoUtil { var tenantId = TenantId.fromUUID(tenantUUID); return entityInfos.stream() .map(info -> new EntitySubtype(tenantId, entityType, info.getName())) + .sorted(Comparator.comparing(EntitySubtype::getType)) .collect(Collectors.toList()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java index a46978bec1..0b9e949c7c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java @@ -43,9 +43,11 @@ import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -323,7 +325,9 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService findAssetProfileNamesByTenantId(TenantId tenantId, boolean activeOnly) { log.trace("Executing findAssetProfileNamesByTenantId, tenantId [{}]", tenantId); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - return assetProfileDao.findTenantAssetProfileNames(tenantId.getId(), activeOnly); + return assetProfileDao.findTenantAssetProfileNames(tenantId.getId(), activeOnly) + .stream().sorted(Comparator.comparing(EntityInfo::getName)) + .collect(Collectors.toList()); } private final PaginatedRemover tenantAssetProfilesRemover = diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index c67babdab0..7e3567c888 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -58,11 +58,13 @@ import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateString; @@ -378,7 +380,9 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService findDeviceProfileNamesByTenantId(TenantId tenantId, boolean activeOnly) { log.trace("Executing findDeviceProfileNamesByTenantId, tenantId [{}]", tenantId); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - return deviceProfileDao.findTenantDeviceProfileNames(tenantId.getId(), activeOnly); + return deviceProfileDao.findTenantDeviceProfileNames(tenantId.getId(), activeOnly) + .stream().sorted(Comparator.comparing(EntityInfo::getName)) + .collect(Collectors.toList()); } private final PaginatedRemover tenantDeviceProfilesRemover = diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java index faf4510879..27dde2fc51 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java @@ -74,11 +74,11 @@ public interface AssetProfileRepository extends JpaRepository findActiveTenantAssetProfileNames(@Param("tenantId") UUID tenantId); @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(a.id, 'ASSET_PROFILE', a.name) " + - "FROM AssetProfileEntity a WHERE a.tenantId = :tenantId ORDER BY a.name ASC") + "FROM AssetProfileEntity a WHERE a.tenantId = :tenantId") List findAllTenantAssetProfileNames(@Param("tenantId") UUID tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java index cc50b06e6e..3b74de9e98 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java @@ -85,11 +85,11 @@ public interface DeviceProfileRepository extends JpaRepository findActiveTenantDeviceProfileNames(@Param("tenantId") UUID tenantId); @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(d.id, 'DEVICE_PROFILE', d.name) " + - "FROM DeviceProfileEntity d WHERE d.tenantId = :tenantId ORDER BY d.name ASC") + "FROM DeviceProfileEntity d WHERE d.tenantId = :tenantId") List findAllTenantDeviceProfileNames(@Param("tenantId") UUID tenantId); } From b7b889117ae580f9e15356ac6dd5bd1721dc7180 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 8 Dec 2023 16:25:47 +0200 Subject: [PATCH 36/38] Improve images update methods. --- .../service/install/update/ImagesUpdater.java | 45 ++++++++++++-- .../server/dao/util/ImageUtils.java | 60 +++++++++++++------ 2 files changed, 82 insertions(+), 23 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/ImagesUpdater.java b/application/src/main/java/org/thingsboard/server/service/install/update/ImagesUpdater.java index e711bf2682..8ff0124481 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/ImagesUpdater.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/ImagesUpdater.java @@ -17,17 +17,21 @@ package org.thingsboard.server.service.install.update; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.HasImage; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageDataIterable; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.asset.AssetProfileDao; import org.thingsboard.server.dao.dashboard.DashboardDao; import org.thingsboard.server.dao.device.DeviceProfileDao; import org.thingsboard.server.dao.resource.ImageService; +import org.thingsboard.server.dao.tenant.TenantDao; import org.thingsboard.server.dao.widget.WidgetTypeDao; import org.thingsboard.server.dao.widget.WidgetsBundleDao; @@ -41,6 +45,7 @@ public class ImagesUpdater { private final ImageService imageService; private final WidgetsBundleDao widgetsBundleDao; private final WidgetTypeDao widgetTypeDao; + private final TenantDao tenantDao; private final DashboardDao dashboardDao; private final DeviceProfileDao deviceProfileDao; private final AssetProfileDao assetProfileDao; @@ -59,8 +64,7 @@ public class ImagesUpdater { public void updateDashboardsImages() { log.info("Updating dashboards images..."); - var dashboardsIds = new PageDataIterable<>(dashboardDao::findAllIds, 1024); - updateImages(dashboardsIds, "dashboard", imageService::replaceBase64WithImageUrl, dashboardDao); + updateImages("dashboard", dashboardDao::findIdsByTenantId, imageService::replaceBase64WithImageUrl, dashboardDao); } public void createSystemImages(Dashboard defaultDashboard) { @@ -108,11 +112,44 @@ public class ImagesUpdater { private void updateImages(Iterable entitiesIds, String type, Function updater, Dao dao) { + int totalCount = 0; int updatedCount = 0; + var counts = updateImages(entitiesIds, type, updater, dao, totalCount, updatedCount); + totalCount = counts[0]; + updatedCount = counts[1]; + log.info("Updated {} {}s out of {}", updatedCount, type, totalCount); + } + + private void updateImages(String type, BiFunction> entityIdsByTenantId, + Function updater, Dao dao) { + int tenantCount = 0; int totalCount = 0; + int updatedCount = 0; + var tenantIds = new PageDataIterable<>(tenantDao::findTenantsIds, 128); + for (var tenantId : tenantIds) { + tenantCount++; + var entitiesIds = new PageDataIterable<>(link -> entityIdsByTenantId.apply(tenantId, link), 128); + var counts = updateImages(entitiesIds, type, updater, dao, totalCount, updatedCount); + totalCount = counts[0]; + updatedCount = counts[1]; + if (tenantCount % 100 == 0) { + log.info("Update {}s images: processed {} tenants so far", type, tenantCount); + } + } + log.info("Updated {} {}s out of {}", updatedCount, type, totalCount); + } + + private int[] updateImages(Iterable entitiesIds, String type, + Function updater, Dao dao, int totalCount, int updatedCount) { for (EntityId id : entitiesIds) { totalCount++; - E entity = dao.findById(TenantId.SYS_TENANT_ID, id.getId()); + E entity; + try { + entity = dao.findById(TenantId.SYS_TENANT_ID, id.getId()); + } catch (Exception e) { + log.error("Failed to update {} images: error fetching {} by id [{}]: {}", type, type, id.getId(), StringUtils.abbreviate(e.toString(), 1000)); + continue; + } try { boolean updated = updater.apply(entity); if (updated) { @@ -127,7 +164,7 @@ public class ImagesUpdater { log.info("Processed {} {}s so far", totalCount, type); } } - log.info("Updated {} {}s out of {}", updatedCount, type, totalCount); + return new int[]{totalCount, updatedCount}; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java b/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java index 466e9d4bab..29f7b32fae 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java @@ -34,6 +34,7 @@ import org.apache.batik.transcoder.TranscoderInput; import org.apache.batik.transcoder.TranscoderOutput; import org.apache.batik.transcoder.image.PNGTranscoder; import org.apache.batik.util.XMLResourceDescriptor; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; import org.thingsboard.server.common.data.StringUtils; @@ -69,13 +70,17 @@ public class ImageUtils { public static ProcessedImage processImage(byte[] data, String mediaType, int thumbnailMaxDimension) throws Exception { if (mediaTypeToFileExtension(mediaType).equals("svg")) { - return processSvgImage(data, mediaType, thumbnailMaxDimension); + try { + return processSvgImage(data, mediaType, thumbnailMaxDimension); + } catch (Exception e) { + if (log.isDebugEnabled()) { // printing stacktrace + log.warn("Couldn't process SVG image, leaving preview as original image", e); + } else { + log.warn("Couldn't process SVG image, leaving preview as original image: {}", ExceptionUtils.getMessage(e)); + } + return previewAsOriginalImage(data, mediaType); + } } - ProcessedImage image = new ProcessedImage(); - image.setMediaType(mediaType); - image.setData(data); - image.setSize(data.length); - BufferedImage bufferedImage = null; try { bufferedImage = ImageIO.read(new ByteArrayInputStream(data)); @@ -83,6 +88,8 @@ public class ImageUtils { } if (bufferedImage == null) { // means that media type is not supported by ImageIO; extracting width and height from metadata and leaving preview as original image Metadata metadata = ImageMetadataReader.readMetadata(new ByteArrayInputStream(data)); + ProcessedImage image = previewAsOriginalImage(data, mediaType); + String dirName = "Unknown"; for (Directory dir : metadata.getDirectories()) { Tag widthTag = dir.getTags().stream() .filter(tag -> tag.getTagName().toLowerCase().contains("width")) @@ -94,24 +101,22 @@ public class ImageUtils { continue; } int width = Integer.parseInt(dir.getObject(widthTag.getTagType()).toString()); - int height = Integer.parseInt(dir.getObject(widthTag.getTagType()).toString()); + int height = Integer.parseInt(dir.getObject(heightTag.getTagType()).toString()); image.setWidth(width); image.setHeight(height); - - ProcessedImage preview = new ProcessedImage(); - preview.setWidth(image.getWidth()); - preview.setHeight(image.getHeight()); - preview.setMediaType(mediaType); - preview.setData(null); - preview.setSize(data.length); - image.setPreview(preview); - log.warn("Couldn't process {} ({}) with ImageIO, leaving preview as original image", mediaType, dir.getName()); - return image; + image.getPreview().setWidth(width); + image.getPreview().setHeight(height); + dirName = dir.getName(); + break; } - log.warn("Image media type {} not supported", mediaType); - throw new IllegalArgumentException("Media type " + mediaType + " not supported"); + log.warn("Couldn't process {} ({}) with ImageIO, leaving preview as original image", mediaType, dirName); + return image; } + ProcessedImage image = new ProcessedImage(); + image.setMediaType(mediaType); + image.setData(data); + image.setSize(data.length); image.setWidth(bufferedImage.getWidth()); image.setHeight(bufferedImage.getHeight()); @@ -202,6 +207,23 @@ public class ImageUtils { return image; } + private static ProcessedImage previewAsOriginalImage(byte[] data, String mediaType) { + ProcessedImage image = new ProcessedImage(); + image.setMediaType(mediaType); + image.setData(data); + image.setSize(data.length); + image.setWidth(0); + image.setHeight(0); + ProcessedImage preview = new ProcessedImage(); + preview.setMediaType(mediaType); + preview.setData(null); + preview.setSize(data.length); + preview.setWidth(0); + preview.setHeight(0); + image.setPreview(preview); + return image; + } + private static int[] getThumbnailDimensions(int originalWidth, int originalHeight, int maxDimension) { if (originalWidth <= maxDimension && originalHeight <= maxDimension) { return new int[]{originalWidth, originalHeight}; From 32fe5b3a520f14abb99e11eb33df3d9d3980a2b6 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Fri, 8 Dec 2023 19:09:00 +0200 Subject: [PATCH 37/38] Widget type export fix. Image delete validation fix --- .../server/dao/resource/BaseImageService.java | 17 ++++++++++++++--- .../sql/widget/WidgetTypeInfoRepository.java | 2 +- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java index ad3934f76e..4683706151 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java @@ -79,6 +79,7 @@ import java.util.regex.Pattern; public class BaseImageService extends BaseResourceService implements ImageService { private static final int MAX_ENTITIES_TO_FIND = 10; + private static final String DEFAULT_CONFIG_TAG = "defaultConfig"; public static Map DASHBOARD_BASE64_MAPPING = new HashMap<>(); public static Map WIDGET_TYPE_BASE64_MAPPING = new HashMap<>(); @@ -302,12 +303,12 @@ public class BaseImageService extends BaseResourceService implements ImageServic boolean updated = result.isUpdated(); if (entity.getDescriptor().isObject()) { ObjectNode descriptor = (ObjectNode) entity.getDescriptor(); - JsonNode defaultConfig = Optional.ofNullable(descriptor.get("defaultConfig")) + JsonNode defaultConfig = Optional.ofNullable(descriptor.get(DEFAULT_CONFIG_TAG)) .filter(JsonNode::isTextual).map(JsonNode::asText) .map(JacksonUtil::toJsonNode).orElse(null); if (defaultConfig != null) { updated |= base64ToImageUrlUsingMapping(entity.getTenantId(), WIDGET_TYPE_BASE64_MAPPING, Collections.singletonMap("prefix", prefix), defaultConfig); - descriptor.put("defaultConfig", defaultConfig.toString()); + descriptor.put(DEFAULT_CONFIG_TAG, defaultConfig.toString()); } } updated |= base64ToImageUrlRecursively(entity.getTenantId(), prefix, entity.getDescriptor()); @@ -524,7 +525,17 @@ public class BaseImageService extends BaseResourceService implements ImageServic public void inlineImages(WidgetTypeDetails widgetTypeDetails) { log.trace("Executing inlineImage [{}] [WidgetTypeDetails] [{}]", widgetTypeDetails.getTenantId(), widgetTypeDetails.getId()); inlineImage(widgetTypeDetails); - inlineIntoJson(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor()); + ObjectNode descriptor = (ObjectNode) widgetTypeDetails.getDescriptor(); + inlineIntoJson(widgetTypeDetails.getTenantId(), descriptor); + if (descriptor.has(DEFAULT_CONFIG_TAG) && descriptor.get(DEFAULT_CONFIG_TAG).isTextual()) { + try { + var defaultConfig = JacksonUtil.toJsonNode(descriptor.get(DEFAULT_CONFIG_TAG).asText()); + inlineIntoJson(widgetTypeDetails.getTenantId(), defaultConfig); + descriptor.put(DEFAULT_CONFIG_TAG, JacksonUtil.toString(defaultConfig)); + } catch (Exception e) { + log.debug("[{}][{}] Failed to process default config: ", widgetTypeDetails.getTenantId(), widgetTypeDetails.getId(), e); + } + } } private void inlineIntoJson(TenantId tenantId, JsonNode root) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java index 83797d7baa..221642699e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java @@ -204,7 +204,7 @@ public interface WidgetTypeInfoRepository extends JpaRepository findByImageUrl(@Param("imageLink") String imageLink, @Param("lmt") int lmt); } From 6e46c5f2d007869cf9881825d550ba7447bbdab3 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 11 Dec 2023 13:18:22 +0200 Subject: [PATCH 38/38] UI: make the same behavior when uploading an image and when importing json --- .../app/shared/components/image/image-gallery.component.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/components/image/image-gallery.component.ts b/ui-ngx/src/app/shared/components/image/image-gallery.component.ts index 7d76c6bde8..652c9dc214 100644 --- a/ui-ngx/src/app/shared/components/image/image-gallery.component.ts +++ b/ui-ngx/src/app/shared/components/image/image-gallery.component.ts @@ -596,7 +596,11 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe importImage(): void { this.importExportService.importImage().subscribe((image) => { if (image) { - this.updateData(); + if (this.selectionMode) { + this.imageSelected.next(image); + } else { + this.updateData(); + } } }); }