Merge branch 'master' into websocket-improvements
This commit is contained in:
		
						commit
						e0d49d504d
					
				@ -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.
 | 
			
		||||
@ -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}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
@ -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 <a href="https://docs.docker.com/engine/install/" target="_blank"> Docker CE</a> and <a href="https://docs.docker.com/compose/install/" target="_blank"> Docker Compose</a>.
 | 
			
		||||
 | 
			
		||||
#### 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}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
@ -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}
 | 
			
		||||
```
 | 
			
		||||
@ -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}
 | 
			
		||||
@ -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}
 | 
			
		||||
@ -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}
 | 
			
		||||
```
 | 
			
		||||
@ -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}
 | 
			
		||||
```
 | 
			
		||||
@ -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}
 | 
			
		||||
```
 | 
			
		||||
@ -0,0 +1,6 @@
 | 
			
		||||
Start the service
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
sudo systemctl tb-edge start
 | 
			
		||||
{:copy-code}
 | 
			
		||||
```
 | 
			
		||||
@ -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}
 | 
			
		||||
@ -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}
 | 
			
		||||
```
 | 
			
		||||
@ -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.
 | 
			
		||||
@ -250,9 +250,9 @@
 | 
			
		||||
                "useShowWidgetActionFunction": null,
 | 
			
		||||
                "showWidgetActionFunction": "return true;",
 | 
			
		||||
                "type": "customPretty",
 | 
			
		||||
                "customHtml": "<form #addEntityForm=\"ngForm\" [formGroup]=\"addEntityFormGroup\"\r\n      (ngSubmit)=\"save()\" class=\"add-entity-form\">\r\n    <mat-toolbar fxLayout=\"row\" color=\"primary\">\r\n        <h2>Add gateway</h2>\r\n        <span fxFlex></span>\r\n        <button mat-icon-button (click)=\"cancel()\" type=\"button\">\r\n            <mat-icon class=\"material-icons\">close</mat-icon>\r\n        </button>\r\n    </mat-toolbar>\r\n    <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\r\n    </mat-progress-bar>\r\n    <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\r\n    <div mat-dialog-content fxLayout=\"column\">\r\n        <div fxLayout=\"row\" fxLayoutGap=\"8px\" fxLayout.xs=\"column\"  fxLayoutGap.xs=\"0\">\r\n            <mat-form-field fxFlex class=\"mat-block\">\r\n                <mat-label>Name</mat-label>\r\n                <input matInput formControlName=\"entityName\" required>\r\n                <mat-error *ngIf=\"addEntityFormGroup.get('entityName').hasError('required')\">\r\n                    Gateway name is required.\r\n                </mat-error>\r\n            </mat-form-field>\r\n        </div>\r\n        <div fxLayout=\"row\" fxLayoutGap=\"8px\" fxLayout.xs=\"column\"  fxLayoutGap.xs=\"0\">\r\n            <tb-entity-subtype-autocomplete\r\n                    fxFlex\r\n                    class=\"mat-block\"\r\n                    formControlName=\"type\"\r\n                    [required]=\"true\"\r\n                    [entityType]=\"'DEVICE'\"\r\n            ></tb-entity-subtype-autocomplete>\r\n        </div>\r\n    </div>\r\n    <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\r\n        <button mat-button color=\"primary\"\r\n                type=\"button\"\r\n                [disabled]=\"(isLoading$ | async)\"\r\n                (click)=\"cancel()\" cdkFocusInitial>\r\n            Cancel\r\n        </button>\r\n        <button mat-button mat-raised-button color=\"primary\"\r\n                type=\"submit\"\r\n                [disabled]=\"(isLoading$ | async) || addEntityForm.invalid || !addEntityForm.dirty\">\r\n            Create\r\n        </button>\r\n    </div>\r\n</form>\r\n",
 | 
			
		||||
                "customHtml": "<form #addEntityForm=\"ngForm\" [formGroup]=\"addEntityFormGroup\"\r\n      (ngSubmit)=\"save($event)\" class=\"add-entity-form\">\r\n    <mat-toolbar fxLayout=\"row\" color=\"primary\">\r\n        <h2>Add gateway</h2>\r\n        <span fxFlex></span>\r\n        <button mat-icon-button (click)=\"cancel()\" type=\"button\">\r\n            <mat-icon class=\"material-icons\">close</mat-icon>\r\n        </button>\r\n    </mat-toolbar>\r\n    <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\r\n    </mat-progress-bar>\r\n    <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\r\n    <div mat-dialog-content fxLayout=\"column\">\r\n        <div fxLayout=\"row\" fxLayoutGap=\"8px\" fxLayout.xs=\"column\"  fxLayoutGap.xs=\"0\">\r\n            <mat-form-field fxFlex class=\"mat-block\">\r\n                <mat-label>Name</mat-label>\r\n                <input matInput formControlName=\"entityName\" required>\r\n                <mat-error *ngIf=\"addEntityFormGroup.get('entityName').hasError('required')\">\r\n                    Gateway name is required.\r\n                </mat-error>\r\n            </mat-form-field>\r\n        </div>\r\n        <div fxLayout=\"row\" fxLayoutGap=\"8px\" fxLayout.xs=\"column\"  fxLayoutGap.xs=\"0\">\r\n            <tb-entity-subtype-autocomplete\r\n                    fxFlex\r\n                    class=\"mat-block\"\r\n                    formControlName=\"type\"\r\n                    [required]=\"true\"\r\n                    [entityType]=\"'DEVICE'\"\r\n            ></tb-entity-subtype-autocomplete>\r\n        </div>\r\n    </div>\r\n    <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\r\n        <button mat-button color=\"primary\"\r\n                type=\"button\"\r\n                [disabled]=\"(isLoading$ | async)\"\r\n                (click)=\"cancel()\" cdkFocusInitial>\r\n            Cancel\r\n        </button>\r\n        <button mat-button mat-raised-button color=\"primary\"\r\n                type=\"submit\"\r\n                [disabled]=\"(isLoading$ | async) || addEntityForm.invalid || !addEntityForm.dirty\">\r\n            Create\r\n        </button>\r\n    </div>\r\n</form>\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": "<div style=\"width: 100%; height: 100%; padding: 0;\" fxFlex fxLayout=\"column\">\r\n  <mat-tab-group [(selectedIndex)]=\"selectedTabIndex\">\r\n    <mat-tab label=\"All\" value=\"gateway_devices_0\"></mat-tab>\r\n    <mat-tab label=\"MQTT\" value=\"gateway_devices_1\"></mat-tab>\r\n    <mat-tab label=\"MODBUS\" value=\"gateway_devices_2\"></mat-tab>\r\n    <mat-tab label=\"GRPC\" value=\"gateway_devices_3\"></mat-tab>\r\n    <mat-tab label=\"OPCUA\" value=\"gateway_devices_4\"> </mat-tab>\r\n    <mat-tab label=\"OPCUA ASYNCIO\" value=\"gateway_devices_5\"></mat-tab>\r\n    <mat-tab label=\"BLE\" value=\"gateway_devices_6\"></mat-tab>\r\n    <mat-tab label=\"REQUEST\" value=\"gateway_devices_7\"></mat-tab>\r\n    <mat-tab label=\"CAN\" value=\"gateway_devices_8\"></mat-tab>\r\n    <mat-tab label=\"BACNET\" value=\"gateway_devices_9\"></mat-tab>\r\n    <mat-tab label=\"ODBC\" value=\"gateway_devices_10\"></mat-tab>\r\n    <mat-tab label=\"REST\" value=\"gateway_devices_11\"></mat-tab>\r\n    <mat-tab label=\"SNMP\" value=\"gateway_devices_12\"></mat-tab>\r\n    <mat-tab label=\"FTP\" value=\"gateway_devices_13\"></mat-tab>\r\n    <mat-tab label=\"SOCKET\" value=\"gateway_devices_14\"></mat-tab>\r\n    <mat-tab label=\"XMPP\" value=\"gateway_devices_15\"></mat-tab>\r\n    <mat-tab label=\"OCCP\" value=\"gateway_devices_16\"></mat-tab>\r\n    <mat-tab label=\"CUSTOM\" value=\"gateway_devices_17\"></mat-tab>\r\n  </mat-tab-group><tb-dashboard-state *ngIf=\"selectedTabIndex == 1\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_1\"></tb-dashboard-state>\r\n   <tb-dashboard-state *ngIf=\"selectedTabIndex == 2\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_2\"></tb-dashboard-state>\r\n   <tb-dashboard-state *ngIf=\"selectedTabIndex == 3\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_3\"></tb-dashboard-state>\r\n   <tb-dashboard-state *ngIf=\"selectedTabIndex == 4\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_4\"></tb-dashboard-state>\r\n   <tb-dashboard-state *ngIf=\"selectedTabIndex == 5\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_5\"></tb-dashboard-state>\r\n   <tb-dashboard-state *ngIf=\"selectedTabIndex == 6\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_6\"></tb-dashboard-state>\r\n   <tb-dashboard-state *ngIf=\"selectedTabIndex == 7\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_7\"></tb-dashboard-state>\r\n   <tb-dashboard-state *ngIf=\"selectedTabIndex == 8\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_8\"></tb-dashboard-state>\r\n   <tb-dashboard-state *ngIf=\"selectedTabIndex == 9\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_9\"></tb-dashboard-state>\r\n   <tb-dashboard-state *ngIf=\"selectedTabIndex == 10\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_10\"></tb-dashboard-state>\r\n   <tb-dashboard-state *ngIf=\"selectedTabIndex == 11\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_11\"></tb-dashboard-state>\r\n   <tb-dashboard-state *ngIf=\"selectedTabIndex == 12\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_12\"></tb-dashboard-state>\r\n   <tb-dashboard-state *ngIf=\"selectedTabIndex == 13\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_13\"></tb-dashboard-state>\r\n   <tb-dashboard-state *ngIf=\"selectedTabIndex == 14\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_14\"></tb-dashboard-state>\r\n   <tb-dashboard-state *ngIf=\"selectedTabIndex == 15\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_15\"></tb-dashboard-state>\r\n   <tb-dashboard-state *ngIf=\"selectedTabIndex == 16\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_16\"></tb-dashboard-state>\r\n   <tb-dashboard-state *ngIf=\"selectedTabIndex == 17\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_17\"></tb-dashboard-state>\r\n   <tb-dashboard-state *ngIf=\"!selectedTabIndex\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_0\"></tb-dashboard-state>\r\n</div>\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"
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@ -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<EntitySubtype> getAssetTypes() throws ThingsboardException, ExecutionException, InterruptedException {
 | 
			
		||||
        SecurityUser user = getCurrentUser();
 | 
			
		||||
        TenantId tenantId = user.getTenantId();
 | 
			
		||||
 | 
			
		||||
@ -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<EntityInfo> 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));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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: <b>\"Access token\"</b> with <b>device profile ID</b> 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: <b>\"Access token\"</b> with  <b>device profile default</b> 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: <b>\"X509\"</b> with <b>device profile ID</b> below: \n\n" +
 | 
			
		||||
                    "Note: <b>credentialsId</b> -  format <b>Sha3Hash</b>, <b>certificateValue</b> - format <b>PEM</b> (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: <b>\"MQTT_BASIC\"</b> with <b>device profile ID</b> 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 <b>LwM2M</b> device and <b>RPK</b> 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: <b>\"Access token\"</b> with <b>device ID</b> and with <b>device ID</b> 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: <b>\"X509\"</b> with <b>device profile ID</b> below: \n\n" +
 | 
			
		||||
                    "Note: <b>credentialsId</b> -  format <b>Sha3Hash</b>, <b>certificateValue</b> - format <b>PEM</b> (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: <b>\"MQTT_BASIC\"</b> with <b>device profile ID</b> 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 <b>LwM2M</b> device and <b>RPK</b> 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<EntitySubtype> getDeviceTypes() throws ThingsboardException, ExecutionException, InterruptedException {
 | 
			
		||||
        SecurityUser user = getCurrentUser();
 | 
			
		||||
        TenantId tenantId = user.getTenantId();
 | 
			
		||||
 | 
			
		||||
@ -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<EntityInfo> 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));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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<EdgeRpcService> edgeRpcServiceOpt;
 | 
			
		||||
    private final Optional<EdgeInstallService> edgeInstallServiceOpt;
 | 
			
		||||
    private final Optional<EdgeInstallInstructionsService> edgeInstallServiceOpt;
 | 
			
		||||
    private final Optional<EdgeUpgradeInstructionsService> 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);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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<String, EdgeUpgradeInfo> 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<String, EdgeUpgradeInfo> 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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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);
 | 
			
		||||
}
 | 
			
		||||
@ -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<String, EdgeUpgradeInfo> upgradeVersions);
 | 
			
		||||
 | 
			
		||||
    void setAppVersion(String version);
 | 
			
		||||
}
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
@ -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:
 | 
			
		||||
 | 
			
		||||
@ -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 <E extends HasImage> void updateImages(Iterable<? extends EntityId> entitiesIds, String type,
 | 
			
		||||
                                                   Function<E, Boolean> updater, Dao<E> dao) {
 | 
			
		||||
        int updatedCount = 0;
 | 
			
		||||
        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 <E extends HasImage> void updateImages(String type, BiFunction<TenantId, PageLink, PageData<? extends EntityId>> entityIdsByTenantId,
 | 
			
		||||
                                                   Function<E, Boolean> updater, Dao<E> 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 <E extends HasImage> int[] updateImages(Iterable<? extends EntityId> entitiesIds, String type,
 | 
			
		||||
                                                    Function<E, Boolean> updater, Dao<E> 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};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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<PageData<AssetProfileInfo>>() {
 | 
			
		||||
                }, 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<EntityInfo> 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<List<EntityInfo>>() {
 | 
			
		||||
        });
 | 
			
		||||
        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);
 | 
			
		||||
 | 
			
		||||
@ -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<PageData<DeviceProfileInfo>>() {
 | 
			
		||||
                }, 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<EntityInfo> 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<List<EntityInfo>>() {
 | 
			
		||||
        });
 | 
			
		||||
        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);
 | 
			
		||||
 | 
			
		||||
@ -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<String, EdgeUpgradeInfo> 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"));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -52,4 +52,4 @@ public class MqttV5ClientSparkplugBAttributesInProfileTest extends AbstractMqttV
 | 
			
		||||
        processClientDeviceWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -78,4 +78,4 @@ public class MqttV5ClientSparkplugBAttributesTest extends AbstractMqttV5ClientSp
 | 
			
		||||
        processClientDeviceWithCorrectAccessTokenPublishWithBirth_SharedAttributes_LongType_IfMetricFailedTypeCheck_SendValueOk();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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<EntityInfo> findAssetProfileNamesByTenantId(TenantId tenantId, boolean activeOnly);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -81,6 +81,7 @@ public interface AssetService extends EntityDaoService {
 | 
			
		||||
 | 
			
		||||
    ListenableFuture<List<Asset>> findAssetsByQuery(TenantId tenantId, AssetSearchQuery query);
 | 
			
		||||
 | 
			
		||||
    @Deprecated(since = "3.6.2", forRemoval = true)
 | 
			
		||||
    ListenableFuture<List<EntitySubtype>> findAssetTypesByTenantId(TenantId tenantId);
 | 
			
		||||
 | 
			
		||||
    Asset assignAssetToEdge(TenantId tenantId, AssetId assetId, EdgeId edgeId);
 | 
			
		||||
 | 
			
		||||
@ -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<EntityInfo> findDeviceProfileNamesByTenantId(TenantId tenantId, boolean activeOnly);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -95,6 +95,7 @@ public interface DeviceService extends EntityDaoService {
 | 
			
		||||
 | 
			
		||||
    ListenableFuture<List<Device>> findDevicesByQuery(TenantId tenantId, DeviceSearchQuery query);
 | 
			
		||||
 | 
			
		||||
    @Deprecated(since = "3.6.2", forRemoval = true)
 | 
			
		||||
    ListenableFuture<List<EntitySubtype>> findDeviceTypesByTenantId(TenantId tenantId);
 | 
			
		||||
 | 
			
		||||
    Device assignDeviceToTenant(TenantId tenantId, Device device);
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
}
 | 
			
		||||
@ -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<String, EdgeUpgradeInfo> edgeVersions;
 | 
			
		||||
}
 | 
			
		||||
@ -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;
 | 
			
		||||
}
 | 
			
		||||
@ -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<EntitySubtype> convertTenantEntityTypesToDto(UUID tenantId, EntityType entityType, List<String> types) {
 | 
			
		||||
    public static List<EntitySubtype> convertTenantEntityTypesToDto(UUID tenantUUID, EntityType entityType, List<String> types) {
 | 
			
		||||
        if (CollectionUtils.isEmpty(types)) {
 | 
			
		||||
            return Collections.emptyList();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        List<EntitySubtype> list = new ArrayList<>(types.size());
 | 
			
		||||
        for (String type : types) {
 | 
			
		||||
            list.add(new EntitySubtype(TenantId.fromUUID(tenantId), entityType, type));
 | 
			
		||||
        }
 | 
			
		||||
        return list;
 | 
			
		||||
        TenantId tenantId = TenantId.fromUUID(tenantUUID);
 | 
			
		||||
        return types.stream()
 | 
			
		||||
                .map(type -> new EntitySubtype(tenantId, entityType, type))
 | 
			
		||||
                .collect(Collectors.toList());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Deprecated // used only in deprecated DAO api
 | 
			
		||||
    public static List<EntitySubtype> convertTenantEntityInfosToDto(UUID tenantUUID, EntityType entityType, List<EntityInfo> entityInfos) {
 | 
			
		||||
        if (CollectionUtils.isEmpty(entityInfos)) {
 | 
			
		||||
            return Collections.emptyList();
 | 
			
		||||
        }
 | 
			
		||||
        var tenantId = TenantId.fromUUID(tenantUUID);
 | 
			
		||||
        return entityInfos.stream()
 | 
			
		||||
                .map(info -> new EntitySubtype(tenantId, entityType, info.getName()))
 | 
			
		||||
                .sorted(Comparator.comparing(EntitySubtype::getType))
 | 
			
		||||
                .collect(Collectors.toList());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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<Asset>, TenantEntityDao, ExportableEntityD
 | 
			
		||||
     *
 | 
			
		||||
     * @return the list of tenant asset type objects
 | 
			
		||||
     */
 | 
			
		||||
    @Deprecated(since = "3.6.2", forRemoval = true)
 | 
			
		||||
    ListenableFuture<List<EntitySubtype>> findTenantAssetTypesAsync(UUID tenantId);
 | 
			
		||||
 | 
			
		||||
    Long countAssetsByAssetProfileId(TenantId tenantId, UUID assetProfileId);
 | 
			
		||||
 | 
			
		||||
@ -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<AssetProfile>, ExportableEntityDao<AssetProfileId, AssetProfile>, ImageContainerDao<AssetProfileInfo> {
 | 
			
		||||
@ -47,4 +49,6 @@ public interface AssetProfileDao extends Dao<AssetProfile>, ExportableEntityDao<
 | 
			
		||||
 | 
			
		||||
    PageData<AssetProfile> findAllWithImages(PageLink pageLink);
 | 
			
		||||
 | 
			
		||||
    List<EntityInfo> findTenantAssetProfileNames(UUID tenantId, boolean activeOnly);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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<AssetPr
 | 
			
		||||
        return EntityType.ASSET_PROFILE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private PaginatedRemover<TenantId, AssetProfile> tenantAssetProfilesRemover =
 | 
			
		||||
    @Override
 | 
			
		||||
    public List<EntityInfo> 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<TenantId, AssetProfile> tenantAssetProfilesRemover =
 | 
			
		||||
            new PaginatedRemover<>() {
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
 | 
			
		||||
@ -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<AssetCacheKey,
 | 
			
		||||
            return Futures.successfulAsList(futures);
 | 
			
		||||
        }, MoreExecutors.directExecutor());
 | 
			
		||||
        assets = Futures.transform(assets, assetList ->
 | 
			
		||||
                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<AssetCacheKey,
 | 
			
		||||
    public ListenableFuture<List<EntitySubtype>> findAssetTypesByTenantId(TenantId tenantId) {
 | 
			
		||||
        log.trace("Executing findAssetTypesByTenantId, tenantId [{}]", tenantId);
 | 
			
		||||
        validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
 | 
			
		||||
        ListenableFuture<List<EntitySubtype>> 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
 | 
			
		||||
 | 
			
		||||
@ -162,6 +162,7 @@ public interface DeviceDao extends Dao<Device>, TenantEntityDao, ExportableEntit
 | 
			
		||||
     *
 | 
			
		||||
     * @return the list of tenant device type objects
 | 
			
		||||
     */
 | 
			
		||||
    @Deprecated(since = "3.6.2", forRemoval = true)
 | 
			
		||||
    ListenableFuture<List<EntitySubtype>> findTenantDeviceTypesAsync(UUID tenantId);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -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<DeviceProfile>, ExportableEntityDao<DeviceProfileId, DeviceProfile>, ImageContainerDao<DeviceProfileInfo> {
 | 
			
		||||
@ -49,4 +51,6 @@ public interface DeviceProfileDao extends Dao<DeviceProfile>, ExportableEntityDa
 | 
			
		||||
 | 
			
		||||
    PageData<DeviceProfile> findAllWithImages(PageLink pageLink);
 | 
			
		||||
 | 
			
		||||
    List<EntityInfo> findTenantDeviceProfileNames(UUID tenantId, boolean activeOnly);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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<Device
 | 
			
		||||
        return EntityType.DEVICE_PROFILE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private PaginatedRemover<TenantId, DeviceProfile> tenantDeviceProfilesRemover =
 | 
			
		||||
    @Override
 | 
			
		||||
    public List<EntityInfo> 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<TenantId, DeviceProfile> tenantDeviceProfilesRemover =
 | 
			
		||||
            new PaginatedRemover<>() {
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
@ -422,7 +434,8 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
 | 
			
		||||
            if (certificates.length > 1) {
 | 
			
		||||
                return EncryptionUtil.certTrimNewLinesForChainInDeviceProfile(certificateValue);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (CertificateException ignored) {}
 | 
			
		||||
        } catch (CertificateException ignored) {
 | 
			
		||||
        }
 | 
			
		||||
        return EncryptionUtil.certTrimNewLines(certificateValue);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -486,12 +486,7 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
 | 
			
		||||
    public ListenableFuture<List<EntitySubtype>> findDeviceTypesByTenantId(TenantId tenantId) {
 | 
			
		||||
        log.trace("Executing findDeviceTypesByTenantId, tenantId [{}]", tenantId);
 | 
			
		||||
        validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
 | 
			
		||||
        ListenableFuture<List<EntitySubtype>> 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
 | 
			
		||||
 | 
			
		||||
@ -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<String, String> DASHBOARD_BASE64_MAPPING = new HashMap<>();
 | 
			
		||||
    public static Map<String, String> 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) {
 | 
			
		||||
 | 
			
		||||
@ -54,7 +54,7 @@ public class TbSqlBlockingQueue<E> implements TbSqlQueue<E> {
 | 
			
		||||
            String logName = params.getLogName();
 | 
			
		||||
            int batchSize = params.getBatchSize();
 | 
			
		||||
            long maxDelay = params.getMaxDelay();
 | 
			
		||||
            List<TbSqlQueueElement<E>> entities = new ArrayList<>(batchSize);
 | 
			
		||||
            final List<TbSqlQueueElement<E>> entities = new ArrayList<>(batchSize);
 | 
			
		||||
            while (!Thread.interrupted()) {
 | 
			
		||||
                try {
 | 
			
		||||
                    long currentTs = System.currentTimeMillis();
 | 
			
		||||
@ -83,19 +83,23 @@ public class TbSqlBlockingQueue<E> implements TbSqlQueue<E> {
 | 
			
		||||
                            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(() -> {
 | 
			
		||||
 | 
			
		||||
@ -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<AssetProfileEntity
 | 
			
		||||
 | 
			
		||||
    Page<AssetProfileEntity> 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<EntityInfo> 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<EntityInfo> findAllTenantAssetProfileNames(@Param("tenantId") UUID tenantId);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -167,9 +167,6 @@ public interface AssetRepository extends JpaRepository<AssetEntity, UUID>, Expor
 | 
			
		||||
                                                                                 @Param("textSearch") String textSearch,
 | 
			
		||||
                                                                                 Pageable pageable);
 | 
			
		||||
 | 
			
		||||
    @Query("SELECT DISTINCT a.type FROM AssetEntity a WHERE a.tenantId = :tenantId")
 | 
			
		||||
    List<String> findTenantAssetTypes(@Param("tenantId") UUID tenantId);
 | 
			
		||||
 | 
			
		||||
    Long countByAssetProfileId(UUID assetProfileId);
 | 
			
		||||
 | 
			
		||||
    @Query("SELECT a FROM AssetEntity a, RelationEntity re WHERE a.tenantId = :tenantId " +
 | 
			
		||||
 | 
			
		||||
@ -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<AssetEntity, Asset> implements A
 | 
			
		||||
    @Autowired
 | 
			
		||||
    private AssetRepository assetRepository;
 | 
			
		||||
 | 
			
		||||
    @Autowired
 | 
			
		||||
    private AssetProfileRepository assetProfileRepository;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected Class<AssetEntity> getEntityClass() {
 | 
			
		||||
        return AssetEntity.class;
 | 
			
		||||
@ -193,7 +195,7 @@ public class JpaAssetDao extends JpaAbstractDao<AssetEntity, Asset> implements A
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public ListenableFuture<List<EntitySubtype>> findTenantAssetTypesAsync(UUID tenantId) {
 | 
			
		||||
        return service.submit(() -> convertTenantEntityTypesToDto(tenantId, EntityType.ASSET, assetRepository.findTenantAssetTypes(tenantId)));
 | 
			
		||||
        return service.submit(() -> convertTenantEntityInfosToDto(tenantId, EntityType.ASSET, assetProfileRepository.findActiveTenantAssetProfileNames(tenantId)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
 | 
			
		||||
@ -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<AssetProfileEntity, Asset
 | 
			
		||||
        return DaoUtil.toPageData(assetProfileRepository.findAllByImageNotNull(DaoUtil.toPageable(pageLink)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public List<EntityInfo> 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));
 | 
			
		||||
 | 
			
		||||
@ -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<DeviceProfileEnti
 | 
			
		||||
 | 
			
		||||
    Page<DeviceProfileEntity> 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<EntityInfo> 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<EntityInfo> findAllTenantDeviceProfileNames(@Param("tenantId") UUID tenantId);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -145,9 +145,6 @@ public interface DeviceRepository extends JpaRepository<DeviceEntity, UUID>, Exp
 | 
			
		||||
                                                   @Param("textSearch") String textSearch,
 | 
			
		||||
                                                   Pageable pageable);
 | 
			
		||||
 | 
			
		||||
    @Query("SELECT DISTINCT d.type FROM DeviceEntity d WHERE d.tenantId = :tenantId")
 | 
			
		||||
    List<String> findTenantDeviceTypes(@Param("tenantId") UUID tenantId);
 | 
			
		||||
 | 
			
		||||
    DeviceEntity findByTenantIdAndName(UUID tenantId, String name);
 | 
			
		||||
 | 
			
		||||
    List<DeviceEntity> findDevicesByTenantIdAndCustomerIdAndIdIn(UUID tenantId, UUID customerId, List<UUID> deviceIds);
 | 
			
		||||
 | 
			
		||||
@ -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<DeviceEntity, Device> implement
 | 
			
		||||
    @Autowired
 | 
			
		||||
    private NativeDeviceRepository nativeDeviceRepository;
 | 
			
		||||
 | 
			
		||||
    @Autowired
 | 
			
		||||
    private DeviceProfileRepository deviceProfileRepository;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected Class<DeviceEntity> getEntityClass() {
 | 
			
		||||
        return DeviceEntity.class;
 | 
			
		||||
@ -217,7 +219,7 @@ public class JpaDeviceDao extends JpaAbstractDao<DeviceEntity, Device> implement
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public ListenableFuture<List<EntitySubtype>> findTenantDeviceTypesAsync(UUID tenantId) {
 | 
			
		||||
        return service.submit(() -> convertTenantEntityTypesToDto(tenantId, EntityType.DEVICE, deviceRepository.findTenantDeviceTypes(tenantId)));
 | 
			
		||||
        return service.submit(() -> convertTenantEntityInfosToDto(tenantId, EntityType.DEVICE, deviceProfileRepository.findActiveTenantDeviceProfileNames(tenantId)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
 | 
			
		||||
@ -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<DeviceProfileEntity, Dev
 | 
			
		||||
        return DaoUtil.toPageData(deviceProfileRepository.findAllByImageNotNull(DaoUtil.toPageable(pageLink)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public List<EntityInfo> 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));
 | 
			
		||||
 | 
			
		||||
@ -204,7 +204,7 @@ public interface WidgetTypeInfoRepository extends JpaRepository<WidgetTypeInfoEn
 | 
			
		||||
 | 
			
		||||
    @Query(nativeQuery = true,
 | 
			
		||||
            value = "SELECT * FROM widget_type_info_view wti WHERE wti.id IN " +
 | 
			
		||||
                    "(select id from widget_type where image = :imageLink or descriptor ILIKE CONCAT('%\"', :imageLink, '\"%') limit :lmt)"
 | 
			
		||||
                    "(select id from widget_type where image = :imageLink or descriptor ILIKE CONCAT('%', :imageLink, '%') limit :lmt)"
 | 
			
		||||
    )
 | 
			
		||||
    List<WidgetTypeInfoEntity> findByImageUrl(@Param("imageLink") String imageLink, @Param("lmt") int lmt);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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();
 | 
			
		||||
 | 
			
		||||
@ -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};
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
@ -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<AssetProfile>(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<EntityInfo> 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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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<DeviceProfile>(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<EntityInfo> 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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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<List<EntitySubtype>>() {
 | 
			
		||||
        }).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<EntitySubtype> getAssetTypes() {
 | 
			
		||||
        return restTemplate.exchange(URI.create(
 | 
			
		||||
                        baseURL + "/api/asset/types"),
 | 
			
		||||
@ -712,6 +713,15 @@ public class RestClient implements Closeable {
 | 
			
		||||
                }).getBody();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public List<EntitySubtype> getAssetProfileNames(boolean activeOnly) {
 | 
			
		||||
        return restTemplate.exchange(
 | 
			
		||||
                baseURL + "/api/assetProfile/names?activeOnly={activeOnly}",
 | 
			
		||||
                HttpMethod.GET,
 | 
			
		||||
                HttpEntity.EMPTY,
 | 
			
		||||
                new ParameterizedTypeReference<List<EntitySubtype>>() {
 | 
			
		||||
                }, activeOnly).getBody();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public BulkImportResult<Asset> 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<EntitySubtype> getDeviceTypes() {
 | 
			
		||||
        return restTemplate.exchange(
 | 
			
		||||
                baseURL + "/api/device/types",
 | 
			
		||||
@ -1403,6 +1414,15 @@ public class RestClient implements Closeable {
 | 
			
		||||
                }).getBody();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public List<EntitySubtype> getDeviceProfileNames(boolean activeOnly) {
 | 
			
		||||
        return restTemplate.exchange(
 | 
			
		||||
                baseURL + "/api/deviceProfile/names?activeOnly={activeOnly}",
 | 
			
		||||
                HttpMethod.GET,
 | 
			
		||||
                HttpEntity.EMPTY,
 | 
			
		||||
                new ParameterizedTypeReference<List<EntitySubtype>>() {
 | 
			
		||||
                }, 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<EdgeInstallInstructions> getEdgeDockerInstallInstructions(EdgeId edgeId) {
 | 
			
		||||
        ResponseEntity<EdgeInstallInstructions> edgeInstallInstructionsResult =
 | 
			
		||||
                restTemplate.getForEntity(baseURL + "/api/edge/instructions/{edgeId}", EdgeInstallInstructions.class, edgeId.getId());
 | 
			
		||||
    public Optional<EdgeInstructions> getEdgeInstallInstructions(EdgeId edgeId, String method) {
 | 
			
		||||
        ResponseEntity<EdgeInstructions> edgeInstallInstructionsResult =
 | 
			
		||||
                restTemplate.getForEntity(baseURL + "/api/edge/instructions/install/{edgeId}/{method}", EdgeInstructions.class, edgeId.getId(), method);
 | 
			
		||||
        return Optional.ofNullable(edgeInstallInstructionsResult.getBody());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Optional<EdgeInstructions> getEdgeUpgradeInstructions(String edgeVersion, String method) {
 | 
			
		||||
        ResponseEntity<EdgeInstructions> 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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -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}"
 | 
			
		||||
 | 
			
		||||
@ -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}"
 | 
			
		||||
 | 
			
		||||
@ -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}"
 | 
			
		||||
 | 
			
		||||
@ -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}"
 | 
			
		||||
 | 
			
		||||
@ -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}"
 | 
			
		||||
 | 
			
		||||
@ -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<BulkImportResult>('/api/edge/bulk_import', entitiesData, defaultHttpOptionsFromConfig(config));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getEdgeInstallInstructions(edgeId: string, method: string = 'ubuntu', config?: RequestConfig): Observable<EdgeInstallInstructions> {
 | 
			
		||||
    return this.http.get<EdgeInstallInstructions>(`/api/edge/instructions/${edgeId}/${method}`, defaultHttpOptionsFromConfig(config));
 | 
			
		||||
  public getEdgeInstallInstructions(edgeId: string, method: string = 'ubuntu', config?: RequestConfig): Observable<EdgeInstructions> {
 | 
			
		||||
    return this.http.get<EdgeInstructions>(`/api/edge/instructions/install/${edgeId}/${method}`, defaultHttpOptionsFromConfig(config));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getEdgeUpgradeInstructions(edgeVersion: string, method: string = 'ubuntu', config?: RequestConfig): Observable<EdgeInstructions> {
 | 
			
		||||
    return this.http.get<EdgeInstructions>(`/api/edge/instructions/upgrade/${edgeVersion}/${method}`, defaultHttpOptionsFromConfig(config));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
@ -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();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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) {
 | 
			
		||||
 | 
			
		||||
@ -36,7 +36,7 @@
 | 
			
		||||
  <ng-template #connectorForm>
 | 
			
		||||
    <mat-form-field>
 | 
			
		||||
      <mat-label>{{ 'gateway.statistics.command' | translate }}</mat-label>
 | 
			
		||||
      <input matInput formControlName="command" />
 | 
			
		||||
      <input matInput formControlName="command"/>
 | 
			
		||||
    </mat-form-field>
 | 
			
		||||
    <mat-form-field fxFlex>
 | 
			
		||||
      <mat-label>{{ 'widget-config.datasource-parameters' | translate }}</mat-label>
 | 
			
		||||
@ -56,8 +56,10 @@
 | 
			
		||||
  </button>
 | 
			
		||||
</div>
 | 
			
		||||
<section class="result-block" [formGroup]="commandForm">
 | 
			
		||||
  <span>{{ 'gateway.rpc-command-result' | translate }}</span>
 | 
			
		||||
  <mat-divider></mat-divider>
 | 
			
		||||
  <span>{{ 'gateway.rpc-command-result' | translate }}
 | 
			
		||||
    <div *ngIf="resultTime" class="result-time" fxFlex fxLayout="row" fxLayoutAlign="center center"> <mat-icon
 | 
			
		||||
      class="material-icons">schedule</mat-icon>
 | 
			
		||||
      <span>{{ resultTime | date: 'yyyy/MM/dd HH:mm:ss' }}</span> </div></span>
 | 
			
		||||
  <tb-json-content [contentType]="contentTypes.JSON" readonly="true" formControlName="result"></tb-json-content>
 | 
			
		||||
</section>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -37,6 +37,8 @@ export class GatewayServiceRPCComponent implements AfterViewInit {
 | 
			
		||||
 | 
			
		||||
  contentTypes = ContentType;
 | 
			
		||||
 | 
			
		||||
  resultTime: number | null;
 | 
			
		||||
 | 
			
		||||
  @Input()
 | 
			
		||||
  dialogRef: MatDialogRef<any>;
 | 
			
		||||
 | 
			
		||||
@ -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));
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@ -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<EdgeInstruc
 | 
			
		||||
              protected router: Router,
 | 
			
		||||
              @Inject(MAT_DIALOG_DATA) private data: EdgeInstructionsDialogData,
 | 
			
		||||
              public dialogRef: MatDialogRef<EdgeInstructionsDialogComponent>,
 | 
			
		||||
              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<EdgeInstruc
 | 
			
		||||
  getInstructions(method: string) {
 | 
			
		||||
    if (!this.contentData[method]) {
 | 
			
		||||
      this.loadedInstructions = false;
 | 
			
		||||
      this.edgeService.getEdgeInstallInstructions(this.data.edge.id.id, method).subscribe(
 | 
			
		||||
        res => {
 | 
			
		||||
          this.contentData[method] = res.installInstructions;
 | 
			
		||||
          this.loadedInstructions = true;
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
      let edgeInstructions$: Observable<EdgeInstructions>;
 | 
			
		||||
      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;
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -114,13 +114,26 @@
 | 
			
		||||
    </button>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div fxLayout="row" fxLayout.xs="column">
 | 
			
		||||
    <button mat-raised-button color="primary"
 | 
			
		||||
            [disabled]="(isLoading$ | async)"
 | 
			
		||||
            (click)="onEntityAction($event, 'openInstructions')"
 | 
			
		||||
            [fxShow]="!isEdit && edgeScope !== 'customer_user'">
 | 
			
		||||
      <mat-icon>info_outline</mat-icon>
 | 
			
		||||
      <span>{{ 'edge.install-connect-instructions' | translate }}</span>
 | 
			
		||||
    </button>
 | 
			
		||||
    <div [ngSwitch]="upgradeAvailable">
 | 
			
		||||
      <ng-container *ngSwitchCase="false">
 | 
			
		||||
        <button mat-raised-button color="primary"
 | 
			
		||||
                [disabled]="(isLoading$ | async)"
 | 
			
		||||
                (click)="onEntityAction($event, 'openInstallInstructions')"
 | 
			
		||||
                [fxShow]="!isEdit && edgeScope !== 'customer_user'">
 | 
			
		||||
          <mat-icon>info_outline</mat-icon>
 | 
			
		||||
          <span>{{ 'edge.install-connect-instructions' | translate }}</span>
 | 
			
		||||
        </button>
 | 
			
		||||
      </ng-container>
 | 
			
		||||
      <ng-container *ngSwitchCase="true">
 | 
			
		||||
        <button mat-raised-button color="primary"
 | 
			
		||||
                [disabled]="(isLoading$ | async)"
 | 
			
		||||
                (click)="onEntityAction($event, 'openUpgradeInstructions')"
 | 
			
		||||
                [fxShow]="!isEdit && edgeScope !== 'customer_user'">
 | 
			
		||||
          <mat-icon>info_outline</mat-icon>
 | 
			
		||||
          <span>{{ 'edge.upgrade-instructions' | translate }}</span>
 | 
			
		||||
        </button>
 | 
			
		||||
      </ng-container>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
<div class="mat-padding" fxLayout="column">
 | 
			
		||||
 | 
			
		||||
@ -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<EdgeInfo> {
 | 
			
		||||
  entityType = EntityType;
 | 
			
		||||
 | 
			
		||||
  edgeScope: 'tenant' | 'customer' | 'customer_user';
 | 
			
		||||
  upgradeAvailable: boolean = false;
 | 
			
		||||
 | 
			
		||||
  constructor(protected store: Store<AppState>,
 | 
			
		||||
              protected translate: TranslateService,
 | 
			
		||||
              private attributeService: AttributeService,
 | 
			
		||||
              @Inject('entity') protected entityValue: EdgeInfo,
 | 
			
		||||
              @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<EdgeInfo>,
 | 
			
		||||
              public fb: UntypedFormBuilder,
 | 
			
		||||
@ -95,6 +100,7 @@ export class EdgeComponent extends EntityComponent<EdgeInfo> {
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    this.generateRoutingKeyAndSecret(entity, this.entityForm);
 | 
			
		||||
    this.checkEdgeVersion();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateFormState() {
 | 
			
		||||
@ -133,4 +139,25 @@ export class EdgeComponent extends EntityComponent<EdgeInfo> {
 | 
			
		||||
      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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -558,7 +558,7 @@ export class EdgesTableConfigResolver implements Resolve<EntityTableConfig<EdgeI
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  openInstructions($event, edge: EdgeInfo, afterAdd = false) {
 | 
			
		||||
  openInstructions($event: Event, edge: EdgeInfo, afterAdd = false, upgradeAvailable = false) {
 | 
			
		||||
    if ($event) {
 | 
			
		||||
      $event.stopPropagation();
 | 
			
		||||
    }
 | 
			
		||||
@ -568,7 +568,8 @@ export class EdgesTableConfigResolver implements Resolve<EntityTableConfig<EdgeI
 | 
			
		||||
      panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
 | 
			
		||||
      data: {
 | 
			
		||||
        edge,
 | 
			
		||||
        afterAdd
 | 
			
		||||
        afterAdd,
 | 
			
		||||
        upgradeAvailable
 | 
			
		||||
      }
 | 
			
		||||
    }).afterClosed().subscribe(() => {
 | 
			
		||||
        if (afterAdd) {
 | 
			
		||||
@ -610,9 +611,12 @@ export class EdgesTableConfigResolver implements Resolve<EntityTableConfig<EdgeI
 | 
			
		||||
      case 'syncEdge':
 | 
			
		||||
        this.syncEdge(action.event, action.entity);
 | 
			
		||||
        return true;
 | 
			
		||||
      case 'openInstructions':
 | 
			
		||||
      case 'openInstallInstructions':
 | 
			
		||||
        this.openInstructions(action.event, action.entity);
 | 
			
		||||
        return true;
 | 
			
		||||
      case 'openUpgradeInstructions':
 | 
			
		||||
        this.openInstructions(action.event, action.entity, false, true);
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -596,7 +596,11 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
 | 
			
		||||
  importImage(): void {
 | 
			
		||||
    this.importExportService.importImage().subscribe((image) => {
 | 
			
		||||
      if (image) {
 | 
			
		||||
        this.updateData();
 | 
			
		||||
        if (this.selectionMode) {
 | 
			
		||||
          this.imageSelected.next(image);
 | 
			
		||||
        } else {
 | 
			
		||||
          this.updateData();
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@ -179,8 +179,8 @@ export interface EdgeEvent extends BaseData<EventId> {
 | 
			
		||||
  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';
 | 
			
		||||
 | 
			
		||||
@ -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. <br><br> List of missing rule chain(s): <br> {{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": "Українська",
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -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³",
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user