diff --git a/application/src/main/data/json/edge/install_instructions/docker/localhost_warning.md b/application/src/main/data/json/edge/install_instructions/docker/localhost_warning.md
deleted file mode 100644
index d2a5bad26b..0000000000
--- a/application/src/main/data/json/edge/install_instructions/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/install_instructions/centos/instructions.md b/application/src/main/data/json/edge/instructions/install/centos/instructions.md
similarity index 91%
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..22bb3f63a0 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:
@@ -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/install_instructions/docker/instructions.md b/application/src/main/data/json/edge/instructions/install/docker/instructions.md
similarity index 77%
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 d8f9731889..9910124b3e 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,25 +1,11 @@
-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
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/install_instructions/ubuntu/instructions.md b/application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md
similarity index 97%
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..6fcdcd50f2 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:
@@ -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/centos/instructions.md b/application/src/main/data/json/edge/instructions/upgrade/centos/instructions.md
new file mode 100644
index 0000000000..c0e721ace2
--- /dev/null
+++ b/application/src/main/data/json/edge/instructions/upgrade/centos/instructions.md
@@ -0,0 +1,15 @@
+#### Upgrading to ${TB_EDGE_VERSION}EDGE
+
+**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
+
+Install package:
+```bash
+sudo rpm -Uvh tb-edge-${TB_EDGE_TAG}.rpm
+{:copy-code}
+```
+${UPGRADE_DB}
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..f1b9931e8b
--- /dev/null
+++ b/application/src/main/data/json/edge/instructions/upgrade/docker/instructions.md
@@ -0,0 +1,10 @@
+#### Upgrading to ${TB_EDGE_VERSION}
+
+Execute the following command to pull **${TB_EDGE_VERSION}** image:
+
+```bash
+docker pull thingsboard/tb-edge:${TB_EDGE_VERSION}
+{:copy-code}
+```
+
+${UPGRADE_DB}
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..a6c9c6731c
--- /dev/null
+++ b/application/src/main/data/json/edge/instructions/upgrade/docker/start_service.md
@@ -0,0 +1,23 @@
+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.8'
+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
+docker compose up -d
+docker compose logs -f 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..d922fa9155
--- /dev/null
+++ b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md
@@ -0,0 +1,61 @@
+Create docker compose file for ThingsBoard Edge upgrade process:
+
+```bash
+> docker-compose-upgrade.yml && nano docker-compose-upgrade.yml
+{:copy-code}
+```
+
+Add the following lines to the yml file:
+
+```bash
+version: '3.8'
+services:
+ mytbedge:
+ restart: on-failure
+ image: "thingsboard/tb-edge:${TB_EDGE_VERSION}"
+ environment:
+ SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/tb-edge
+ volumes:
+ - tb-edge-data:/data
+ - tb-edge-logs:/var/log/tb-edge
+ entrypoint: upgrade-tb-edge.sh
+ postgres:
+ restart: always
+ image: "postgres:15"
+ ports:
+ - "5432"
+ environment:
+ POSTGRES_DB: tb-edge
+ POSTGRES_PASSWORD: postgres
+ volumes:
+ - 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}
+```
+
+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..bb3ce268b8
--- /dev/null
+++ b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md
@@ -0,0 +1,83 @@
+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}
+```
+
+**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
+```
+
+##### 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 volume before upgrading:
+
+```bash
+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/start_service.md b/application/src/main/data/json/edge/instructions/upgrade/start_service.md
new file mode 100644
index 0000000000..c26c183154
--- /dev/null
+++ b/application/src/main/data/json/edge/instructions/upgrade/start_service.md
@@ -0,0 +1,6 @@
+Start the service
+
+```bash
+sudo systemctl tb-edge start
+{: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..59d243c63c
--- /dev/null
+++ b/application/src/main/data/json/edge/instructions/upgrade/ubuntu/instructions.md
@@ -0,0 +1,15 @@
+#### Upgrading to ${TB_EDGE_VERSION}EDGE
+
+**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
+
+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_db.md b/application/src/main/data/json/edge/instructions/upgrade/upgrade_db.md
new file mode 100644
index 0000000000..6e7beb1425
--- /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=${FROM_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..bdd6c4ea18
--- /dev/null
+++ b/application/src/main/data/json/edge/instructions/upgrade/upgrade_preparing.md
@@ -0,0 +1,36 @@
+Here is the list of commands, that can be used to quickly upgrade ThingsBoard Edge on ${OS}
+
+#### 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/data/json/tenant/dashboards/gateways.json b/application/src/main/data/json/tenant/dashboards/gateways.json
index 972aed299f..13902a9158 100644
--- a/application/src/main/data/json/tenant/dashboards/gateways.json
+++ b/application/src/main/data/json/tenant/dashboards/gateways.json
@@ -250,9 +250,9 @@
"useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;",
"type": "customPretty",
- "customHtml": "
\r\n",
+ "customHtml": "\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": "Gateway devices",
@@ -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/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/main/java/org/thingsboard/server/controller/EdgeController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java
index fa16fbff81..7556ab589b 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;
@@ -59,7 +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.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;
@@ -101,7 +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 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,23 +555,41 @@ public class EdgeController extends BaseController {
return edgeBulkImportService.processBulkImport(request, user);
}
- @ApiOperation(value = "Get Edge Docker Install Instructions (getEdgeDockerInstallInstructions)",
- notes = "Get a docker install instructions for provided edge id." + TENANT_AUTHORITY_PARAGRAPH,
+ @ApiOperation(value = "Get Edge Install Instructions (getEdgeInstallInstructions)",
+ 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/{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()) {
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);
+ }
+ }
+
+ @ApiOperation(value = "Get Edge Upgrade Instructions (getEdgeUpgradeInstructions)",
+ 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)
+ @ResponseBody
+ public EdgeInstructions getEdgeUpgradeInstructions(
+ @ApiParam(value = "Edge version", required = true)
+ @PathVariable("edgeVersion") String edgeVersion,
+ @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));
} 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/DefaultEdgeInstallInstructionsService.java
similarity index 78%
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 dd17f83317..ba2cd06ee1 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
@@ -16,13 +16,14 @@
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;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.edge.Edge;
-import org.thingsboard.server.common.data.edge.EdgeInstallInstructions;
-import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.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;
@@ -37,11 +38,11 @@ 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 EDGE_INSTALL_INSTRUCTIONS_DIR = "install_instructions";
+ private static final String INSTRUCTIONS_DIR = "instructions";
+ private static final String INSTALL_DIR = "install";
private final InstallScripts installScripts;
@@ -52,10 +53,11 @@ public class DefaultEdgeInstallService implements EdgeInstallService {
private boolean sslEnabled;
@Value("${app.version:unknown}")
+ @Setter
private String appVersion;
@Override
- public EdgeInstallInstructions 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);
@@ -68,41 +70,41 @@ 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")) {
- 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";
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 +129,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/DefaultEdgeUpgradeInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java
new file mode 100644
index 0000000000..940a813fc6
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java
@@ -0,0 +1,158 @@
+/**
+ * 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.RequiredArgsConstructor;
+import lombok.Setter;
+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.EdgeUpgradeInfo;
+import org.thingsboard.server.common.data.edge.EdgeInstructions;
+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;
+import java.util.Map;
+
+@Service
+@Slf4j
+@RequiredArgsConstructor
+@ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true")
+@TbCoreComponent
+public class DefaultEdgeUpgradeInstructionsService implements EdgeUpgradeInstructionsService {
+
+ private static final Map upgradeVersionHashMap = new HashMap<>();
+
+ 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;
+
+ @Value("${app.version:unknown}")
+ @Setter
+ private String appVersion;
+
+ @Override
+ public EdgeInstructions getUpgradeInstructions(String edgeVersion, String upgradeMethod) {
+ String tbVersion = appVersion.replace("-SNAPSHOT", "");
+ String currentEdgeVersion = convertEdgeVersionToDocsFormat(edgeVersion);
+ switch (upgradeMethod.toLowerCase()) {
+ case "docker":
+ return getDockerUpgradeInstructions(tbVersion, currentEdgeVersion);
+ case "ubuntu":
+ case "centos":
+ return getLinuxUpgradeInstructions(tbVersion, currentEdgeVersion, upgradeMethod.toLowerCase());
+ default:
+ throw new IllegalArgumentException("Unsupported upgrade method for Edge: " + upgradeMethod);
+ }
+ }
+
+ @Override
+ public void updateInstructionMap(Map map) {
+ for (String key : map.keySet()) {
+ upgradeVersionHashMap.put(key, map.get(key));
+ }
+ }
+
+ private EdgeInstructions getDockerUpgradeInstructions(String tbVersion, String 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.");
+ }
+ StringBuilder result = new StringBuilder(readFile(resolveFile("docker", "upgrade_preparing.md")));
+ while (edgeUpgradeInfo.getNextEdgeVersion() != null || !tbVersion.equals(currentEdgeVersion)) {
+ String edgeVersion = edgeUpgradeInfo.getNextEdgeVersion();
+ String dockerUpgradeInstructions = readFile(resolveFile("docker", "instructions.md"));
+ if (edgeUpgradeInfo.isRequiresUpdateDb()) {
+ String upgradeDb = readFile(resolveFile("docker", "upgrade_db.md"));
+ dockerUpgradeInstructions = dockerUpgradeInstructions.replace("${UPGRADE_DB}", upgradeDb);
+ } else {
+ dockerUpgradeInstructions = dockerUpgradeInstructions.replace("${UPGRADE_DB}", "");
+ }
+ 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(dockerUpgradeInstructions);
+ }
+ 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) {
+ 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 (edgeUpgradeInfo.getNextEdgeVersion() != null || !tbVersion.equals(currentEdgeVersion)) {
+ String edgeVersion = edgeUpgradeInfo.getNextEdgeVersion();
+ String linuxUpgradeInstructions = readFile(resolveFile(os, "instructions.md"));
+ if (edgeUpgradeInfo.isRequiresUpdateDb()) {
+ String upgradeDb = readFile(resolveFile("upgrade_db.md"));
+ linuxUpgradeInstructions = linuxUpgradeInstructions.replace("${UPGRADE_DB}", upgradeDb);
+ } else {
+ linuxUpgradeInstructions = linuxUpgradeInstructions.replace("${UPGRADE_DB}", "");
+ }
+ 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(linuxUpgradeInstructions);
+ }
+ String startService = readFile(resolveFile("start_service.md"));
+ result.append(startService);
+ 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 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);
+ }
+}
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 72%
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 20cac25e33..9df1f86e86 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
@@ -16,12 +16,13 @@
package org.thingsboard.server.service.edge.instructions;
import org.thingsboard.server.common.data.edge.Edge;
-import org.thingsboard.server.common.data.edge.EdgeInstallInstructions;
-import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.edge.EdgeInstructions;
import javax.servlet.http.HttpServletRequest;
-public interface EdgeInstallService {
+public interface EdgeInstallInstructionsService {
- EdgeInstallInstructions getInstallInstructions(TenantId tenantId, Edge edge, String installationMethod, HttpServletRequest request);
+ EdgeInstructions getInstallInstructions(Edge edge, String installationMethod, HttpServletRequest request);
+
+ 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
new file mode 100644
index 0000000000..be40bc6c89
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java
@@ -0,0 +1,30 @@
+/**
+ * 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.EdgeUpgradeInfo;
+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 setAppVersion(String version);
+}
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 e1dc1c667f..c9fb796831 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;
@@ -794,6 +795,7 @@ public final class EdgeGrpcSession implements Closeable {
if (edge.getSecret().equals(request.getEdgeSecret())) {
sessionOpenListener.accept(edge.getId(), this);
this.edgeVersion = request.getEdgeVersion();
+ processSaveEdgeVersionAsAttribute(request.getEdgeVersion().name());
return ConnectResponseMsg.newBuilder()
.setResponseCode(ConnectResponseCode.ACCEPTED)
.setErrorMsg("")
@@ -819,6 +821,11 @@ public final class EdgeGrpcSession implements Closeable {
.setConfiguration(EdgeConfiguration.getDefaultInstance()).build();
}
+ 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);
+ }
+
@Override
public void close() {
log.debug("[{}][{}] Closing session", this.tenantId, sessionId);
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/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 extends EntityId> 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 extends EntityId> 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/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..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
@@ -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(required = false)
+ private EdgeInstallInstructionsService edgeInstallInstructionsService;
+
+ @Autowired(required = false)
+ 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,16 @@ public class DefaultUpdateService implements UpdateService {
.updateInfo(updateMessage)
.build());
}
+ ObjectNode edgeRequest = JacksonUtil.newObjectNode().put(VERSION_PARAM, version);
+ 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 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) {
log.trace(e.getMessage());
}
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/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/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java
index 28d48948d4..d4f4a4a241 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.EdgeUpgradeInfo;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
@@ -74,6 +75,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;
@@ -82,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;
@@ -115,6 +119,9 @@ public class EdgeControllerTest extends AbstractControllerTest {
@Autowired
private EdgeDao edgeDao;
+ @Autowired
+ private EdgeUpgradeInstructionsService edgeUpgradeInstructionsService;
+
static class Config {
@Bean
@Primary
@@ -1172,8 +1179,25 @@ 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"));
}
+
+ @Test
+ public void testGetEdgeUpgradeInstructions() throws Exception {
+ // UpdateInfo config is updating from Thingsboard Update server
+ 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);
+ 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/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/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/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUpgradeInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUpgradeInfo.java
new file mode 100644
index 0000000000..cadd589b49
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUpgradeInfo.java
@@ -0,0 +1,26 @@
+/**
+ * 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;
+import lombok.Getter;
+
+@AllArgsConstructor
+@Getter
+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
new file mode 100644
index 0000000000..bb17479607
--- /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/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/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java
index 76f5a1948b..1c4aed5393 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;
@@ -31,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;
@@ -152,15 +154,26 @@ 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();
}
+ TenantId tenantId = TenantId.fromUUID(tenantUUID);
+ return types.stream()
+ .map(type -> new EntitySubtype(tenantId, entityType, type))
+ .collect(Collectors.toList());
+ }
- List list = new ArrayList<>(types.size());
- for (String type : types) {
- list.add(new EntitySubtype(TenantId.fromUUID(tenantId), entityType, type));
+ @Deprecated // used only in deprecated DAO api
+ public static List convertTenantEntityInfosToDto(UUID tenantUUID, EntityType entityType, List entityInfos) {
+ if (CollectionUtils.isEmpty(entityInfos)) {
+ return Collections.emptyList();
}
- return list;
+ 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/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/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/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/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/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/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(() -> {
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..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
@@ -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,13 @@ 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..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
@@ -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,13 @@ 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/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);
}
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();
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};
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);
+ }
+
}
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/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..5fe2561f3f 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;
@@ -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",
@@ -3241,12 +3261,18 @@ 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 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();
}
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}"
diff --git a/ui-ngx/src/app/core/http/edge.service.ts b/ui-ngx/src/app/core/http/edge.service.ts
index 4ddcc69458..30fbb64c54 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 '@shared/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/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..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
@@ -183,7 +183,7 @@ 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]) && tsValue[0] !== 0) {
ts = tsValue[0];
value = tsValue[1];
this.valueText = formatValue(value, this.decimals, this.units, false);
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..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
@@ -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) {
+ 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') {
@@ -210,7 +225,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;
@@ -307,6 +322,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}
private clearOutConnectorForm(): void {
+ this.initialConnector = null;
this.connectorForm.setValue({
name: '',
type: 'mqtt',
@@ -316,7 +332,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
configuration: '',
configurationJson: {}
});
- this.initialConnector = null;
this.connectorForm.markAsPristine();
}
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) {
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/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..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
@@ -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,12 +58,16 @@ export class EdgeInstructionsDialogComponent extends DialogComponent,
+ private attributeService: AttributeService,
private edgeService: EdgeService) {
super(store, router, dialogRef);
if (this.data.afterAdd) {
this.dialogTitle = 'edge.install-connect-instructions-edge-created';
this.showDontShowAgain = true;
+ } else if (this.data.upgradeAvailable) {
+ this.dialogTitle = 'edge.upgrade-instructions';
+ this.showDontShowAgain = false;
} else {
this.dialogTitle = 'edge.install-connect-instructions';
this.showDontShowAgain = false;
@@ -85,12 +98,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 @@
-
- info_outline
- {{ 'edge.install-connect-instructions' | translate }}
-
+
+
+
+ info_outline
+ {{ 'edge.install-connect-instructions' | translate }}
+
+
+
+
+ info_outline
+ {{ 'edge.upgrade-instructions' | translate }}
+
+
+
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..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 {
if (afterAdd) {
@@ -610,9 +611,12 @@ export class EdgesTableConfigResolver implements Resolve {
if (image) {
- this.updateData();
+ if (this.selectionMode) {
+ this.imageSelected.next(image);
+ } else {
+ this.updateData();
+ }
}
});
}
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..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(100, 0, null, {
+ const pageLink = new PageLink(1024, 0, null, {
property: 'name',
direction: Direction.ASC
});
diff --git a/ui-ngx/src/app/shared/models/edge.models.ts b/ui-ngx/src/app/shared/models/edge.models.ts
index 01789636b8..26337b5e35 100644
--- a/ui-ngx/src/app/shared/models/edge.models.ts
+++ b/ui-ngx/src/app/shared/models/edge.models.ts
@@ -179,8 +179,8 @@ export interface EdgeEvent extends BaseData {
body: string;
}
-export interface EdgeInstallInstructions {
- installInstructions: string;
+export interface EdgeInstructions {
+ instructions: string;
}
export enum EdgeInstructionsMethod {
@@ -188,3 +188,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 5e585783d5..d5a282ec4c 100644
--- a/ui-ngx/src/assets/locale/locale.constant-en_US.json
+++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json
@@ -2021,6 +2021,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": {
@@ -2688,7 +2689,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.",
@@ -4533,6 +4534,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",
@@ -6692,7 +6694,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": "Язык"
- }
-}
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³",