Browse Source

Merge branch 'develop/3.6' of github.com:thingsboard/thingsboard into bug-fix/mat-icon-button_highlight_behaviour

pull/9259/head
Dmitriymush 3 years ago
parent
commit
4f65568fba
  1. 703
      application/src/main/data/json/demo/dashboards/gateway.json
  2. 230
      application/src/main/data/json/edge/install_instructions/centos/instructions.md
  3. 14
      application/src/main/data/json/edge/install_instructions/docker/instructions.md
  4. 163
      application/src/main/data/json/edge/install_instructions/ubuntu/instructions.md
  5. 11
      application/src/main/data/json/system/widget_bundles/gateway_widgets.json
  6. 19
      application/src/main/data/json/system/widget_types/gateway_configuration2.json
  7. 19
      application/src/main/data/json/system/widget_types/gateway_connector.json
  8. 19
      application/src/main/data/json/system/widget_types/gateway_connectors.json
  9. 23
      application/src/main/data/json/system/widget_types/gateway_custom_statistics.json
  10. 10
      application/src/main/data/json/system/widget_types/gateway_general_chart_statistics.json
  11. 19
      application/src/main/data/json/system/widget_types/gateway_general_configuration.json
  12. 10
      application/src/main/data/json/system/widget_types/gateway_logs.json
  13. 23
      application/src/main/data/json/system/widget_types/gateway_statistics.json
  14. 6
      application/src/main/data/json/system/widget_types/service_rpc.json
  15. 6
      application/src/main/java/org/thingsboard/server/controller/EdgeController.java
  16. 50
      application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallService.java
  17. 3
      application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallService.java
  18. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/AbstractRuleChainMetadataConstructor.java
  19. 1
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  20. 2
      application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java
  21. 13
      application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java
  22. 175
      application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java
  23. 2
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmModificationRequest.java
  24. 4
      common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeInstallInstructions.java
  25. 2
      common/edge-api/src/main/proto/edge.proto
  26. 24
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java
  27. 7
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmResult.java
  28. 93
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java
  29. 3
      ui-ngx/src/app/core/http/device.service.ts
  30. 4
      ui-ngx/src/app/core/http/edge.service.ts
  31. 5
      ui-ngx/src/app/core/utils.ts
  32. 14
      ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts
  33. 48
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.html
  34. 37
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.scss
  35. 34
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.ts
  36. 1437
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html
  37. 124
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.scss
  38. 310
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts
  39. 117
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html
  40. 74
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.scss
  41. 204
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts
  42. 28
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.html
  43. 155
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.ts
  44. 75
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-remote-configuration-dialog.html
  45. 12
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-remote-configuration-dialog.ts
  46. 74
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html
  47. 16
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss
  48. 67
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts
  49. 26
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html
  50. 127
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.ts
  51. 142
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts
  52. 19
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-logs-settings.component.html
  53. 29
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-logs-settings.component.ts
  54. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component.html
  55. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component.ts
  56. 95
      ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.html
  57. 123
      ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.scss
  58. 84
      ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.ts
  59. 65
      ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts
  60. 9
      ui-ngx/src/app/shared/components/file-input.component.html
  61. 2
      ui-ngx/src/app/shared/components/file-input.component.scss
  62. 5
      ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html
  63. 4
      ui-ngx/src/app/shared/components/hint-tooltip-icon.component.scss
  64. 3
      ui-ngx/src/app/shared/components/hint-tooltip-icon.component.ts
  65. 8
      ui-ngx/src/app/shared/models/edge.models.ts
  66. 1
      ui-ngx/src/app/shared/models/user-settings.models.ts
  67. 71
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  68. 5
      ui-ngx/src/form.scss

703
application/src/main/data/json/demo/dashboards/gateway_list.json → application/src/main/data/json/demo/dashboards/gateway.json

File diff suppressed because it is too large

230
application/src/main/data/json/edge/install_instructions/centos/instructions.md

@ -0,0 +1,230 @@
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.
#### Prerequisites
Before continue to installation execute the following commands in order to install necessary tools:
```bash
sudo yum install -y nano wget
sudo yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
```
#### Install Java 11 (OpenJDK)
ThingsBoard service is running on Java 11. Follow these instructions to install OpenJDK 11:
```bash
sudo yum install java-11-openjdk
{:copy-code}
```
Please don't forget to configure your operating system to use OpenJDK 11 by default.
You can configure which version is the default using the following command:
```bash
sudo update-alternatives --config java
{:copy-code}
```
You can check the installation using the following command:
```bash
java -version
{:copy-code}
```
Expected command output is:
```text
openjdk version "11.0.xx"
OpenJDK Runtime Environment (...)
OpenJDK 64-Bit Server VM (build ...)
```
#### Configure PostgreSQL
ThingsBoard Edge uses PostgreSQL database as a local storage.
Instructions listed below will help you to install PostgreSQL.
```bash
# Update your system
sudo yum update
{:copy-code}
```
**For CentOS 7:**
```bash
# Install the repository RPM (for CentOS 7):
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
# Initialize your PostgreSQL DB
sudo /usr/pgsql-12/bin/postgresql-12-setup initdb
sudo systemctl start postgresql-12
# Optional: Configure PostgreSQL to start on boot
sudo systemctl enable --now postgresql-12
{:copy-code}
```
**For CentOS 8:**
```bash
# Install the repository RPM (for CentOS 8):
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
# Initialize your PostgreSQL DB
sudo /usr/pgsql-12/bin/postgresql-12-setup initdb
sudo systemctl start postgresql-12
# Optional: Configure PostgreSQL to start on boot
sudo systemctl enable --now postgresql-12
{:copy-code}
```
Once PostgreSQL is installed you may want to create a new user or set the password for the main user.
The instructions below will help to set the password for main PostgreSQL user:
```text
sudo su - postgres
psql
\password
\q
```
Then, press "Ctrl+D" to return to main user console.
After configuring the password, edit the pg_hba.conf to use MD5 authentication with the postgres user.
Edit pg_hba.conf file:
```bash
sudo nano /var/lib/pgsql/12/data/pg_hba.conf
{:copy-code}
```
Locate the following lines:
```text
# IPv4 local connections:
host all all 127.0.0.1/32 ident
```
Replace `ident` with `md5`:
```text
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
{:copy-code}
```
Connect to the database to create ThingsBoard Edge DB:
```bash
psql -U postgres -d postgres -h 127.0.0.1 -W
{:copy-code}
```
Execute create database statement:
```bash
CREATE DATABASE tb_edge;
\q
{:copy-code}
```
#### ThingsBoard Edge service installation
Download installation package:
```bash
wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_VERSION}/tb-edge-${TB_EDGE_VERSION}.rpm
{:copy-code}
```
Go to the download repository and install ThingsBoard Edge service:
```bash
sudo rpm -Uvh tb-edge-${TB_EDGE_VERSION}.rpm
{:copy-code}
```
#### Configure ThingsBoard Edge
To configure ThingsBoard Edge, you can use the following command to automatically update the configuration file with specific values:
```bash
sudo sh -c 'cat <<EOL >> /etc/tb-edge/conf/tb-edge.conf
export CLOUD_ROUTING_KEY=${CLOUD_ROUTING_KEY}
export CLOUD_ROUTING_SECRET=${CLOUD_ROUTING_SECRET}
export CLOUD_RPC_HOST=${BASE_URL}
export CLOUD_RPC_PORT=${CLOUD_RPC_PORT}
export CLOUD_RPC_SSL_ENABLED=${CLOUD_RPC_SSL_ENABLED}
EOL'
{:copy-code}
```
##### [Optional] Database Configuration
In case you changed default PostgreSQL datasource settings (**postgres**/**postgres**) please update the configuration file (**/etc/tb-edge/conf/tb-edge.conf**) with your actual values:
```bash
sudo nano /etc/tb-edge/conf/tb-edge.conf
{:copy-code}
```
Please update the following lines in your configuration file. Make sure **to replace**:
- Replace 'postgres' with your actual PostgreSQL username;
- Replace 'PUT_YOUR_POSTGRESQL_PASSWORD_HERE' with your actual PostgreSQL password.
```bash
export SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/tb_edge
export SPRING_DATASOURCE_USERNAME=postgres
export SPRING_DATASOURCE_PASSWORD=PUT_YOUR_POSTGRESQL_PASSWORD_HERE
{:copy-code}
```
##### [Optional] Update bind ports
If ThingsBoard Edge is going to be running on the same machine where ThingsBoard server (cloud) is running, you'll need to update configuration parameters to avoid port collision between ThingsBoard server and ThingsBoard Edge.
Please execute the following command to update ThingsBoard Edge configuration file (**/etc/tb-edge/conf/tb-edge.conf**):
```bash
sudo sh -c 'cat <<EOL >> /etc/tb-edge/conf/tb-edge.conf
export HTTP_BIND_PORT=18080
export MQTT_BIND_PORT=11883
export COAP_BIND_PORT=15683
export LWM2M_ENABLED=false
EOL'
{:copy-code}
```
Make sure that ports above (18080, 11883, 15683) are not used by any other application.
#### Run installation script
Once ThingsBoard Edge is installed and configured please execute the following install script:
```bash
sudo /usr/share/tb-edge/bin/install/install.sh
{:copy-code}
```
#### Restart ThingsBoard Edge service
```bash
sudo service tb-edge restart
{:copy-code}
```
#### Open ThingsBoard Edge UI
Once started, you will be able to open **ThingsBoard Edge UI** using the following link http://localhost:8080.
###### NOTE: Edge HTTP bind port update
Use next **ThingsBoard Edge UI** link **http://localhost:18080** if you updated HTTP 8080 bind port to **18080**.

14
application/src/main/data/json/edge/install_instructions/docker/instructions.md

@ -1,12 +1,10 @@
## Install ThingsBoard Edge and connect to cloud instructions 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 and connect ThingsBoard Edge to the cloud using docker compose. #### Prerequisites
### 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>. 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 #### Create data and logs folders
Run following commands, before starting docker container(s), to create folders for storing data and logs. 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. These commands additionally will change owner of newly created folders to docker container user.
@ -18,7 +16,7 @@ mkdir -p ~/.mytb-edge-logs && sudo chown -R 799:799 ~/.mytb-edge-logs
{:copy-code} {:copy-code}
``` ```
### Running ThingsBoard Edge as docker service #### Running ThingsBoard Edge as docker service
${LOCALHOST_WARNING} ${LOCALHOST_WARNING}
@ -64,12 +62,12 @@ services:
{:copy-code} {:copy-code}
``` ```
#### [Optional] Update bind ports ##### [Optional] Update bind ports
If ThingsBoard Edge is going to be running on the same machine where ThingsBoard server (cloud) is running, you'll need to update docker compose port mapping to avoid port collision between ThingsBoard server and ThingsBoard Edge. If ThingsBoard Edge is going to be running on the same machine where ThingsBoard server (cloud) is running, you'll need to update docker compose port mapping to avoid port collision between ThingsBoard server and ThingsBoard Edge.
Please update next lines of `docker-compose.yml` file: Please update next lines of `docker-compose.yml` file:
```bash ```text
ports: ports:
- "18080:8080" - "18080:8080"
- "11883:1883" - "11883:1883"

163
application/src/main/data/json/edge/install_instructions/ubuntu/instructions.md

@ -0,0 +1,163 @@
Here is the list of commands, that can be used to quickly install ThingsBoard Edge on Ubuntu Server and connect to the cloud.
#### Install Java 11 (OpenJDK)
ThingsBoard service is running on Java 11. Follow these instructions to install OpenJDK 11:
```bash
sudo apt update
sudo apt install openjdk-11-jdk
{:copy-code}
```
Please don't forget to configure your operating system to use OpenJDK 11 by default.
You can configure which version is the default using the following command:
```bash
sudo update-alternatives --config java
{:copy-code}
```
You can check the installation using the following command:
```bash
java -version
{:copy-code}
```
Expected command output is:
```text
openjdk version "11.0.xx"
OpenJDK Runtime Environment (...)
OpenJDK 64-Bit Server VM (build ...)
```
#### Configure PostgreSQL
ThingsBoard Edge uses PostgreSQL database as a local storage.
Instructions listed below will help you to install PostgreSQL.
```bash
# install **wget** if not already installed:
sudo apt install -y wget
# import the repository signing key:
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
# add repository contents to your system:
RELEASE=$(lsb_release -cs)
echo "deb http://apt.postgresql.org/pub/repos/apt/ ${RELEASE}"-pgdg main | sudo tee /etc/apt/sources.list.d/pgdg.list
# install and launch the postgresql service:
sudo apt update
sudo apt -y install postgresql-12
sudo service postgresql start
{:copy-code}
```
Once PostgreSQL is installed you may want to create a new user or set the password for the main user.
The instructions below will help to set the password for main PostgreSQL user:
```text
sudo su - postgres
psql
\password
\q
```
Then, press “Ctrl+D” to return to main user console and connect to the database to create ThingsBoard Edge DB:
```text
psql -U postgres -d postgres -h 127.0.0.1 -W
CREATE DATABASE tb_edge;
\q
```
#### Thingsboard Edge service installation
Download installation package:
```bash
wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_VERSION}/tb-edge-${TB_EDGE_VERSION}.deb
{:copy-code}
```
Go to the download repository and install ThingsBoard Edge service:
```bash
sudo dpkg -i tb-edge-${TB_EDGE_VERSION}.deb
{:copy-code}
```
#### Configure ThingsBoard Edge
To configure ThingsBoard Edge, you can use the following command to automatically update the configuration file with specific values:
```bash
sudo sh -c 'cat <<EOL >> /etc/tb-edge/conf/tb-edge.conf
export CLOUD_ROUTING_KEY=${CLOUD_ROUTING_KEY}
export CLOUD_ROUTING_SECRET=${CLOUD_ROUTING_SECRET}
export CLOUD_RPC_HOST=${BASE_URL}
export CLOUD_RPC_PORT=${CLOUD_RPC_PORT}
export CLOUD_RPC_SSL_ENABLED=${CLOUD_RPC_SSL_ENABLED}
EOL'
{:copy-code}
```
##### [Optional] Database Configuration
In case you changed default PostgreSQL datasource settings (**postgres**/**postgres**) please update the configuration file (**/etc/tb-edge/conf/tb-edge.conf**) with your actual values:
```bash
sudo nano /etc/tb-edge/conf/tb-edge.conf
{:copy-code}
```
Please update the following lines in your configuration file. Make sure **to replace**:
- Replace 'postgres' with your actual PostgreSQL username;
- Replace 'PUT_YOUR_POSTGRESQL_PASSWORD_HERE' with your actual PostgreSQL password.
```bash
export SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/tb_edge
export SPRING_DATASOURCE_USERNAME=postgres
export SPRING_DATASOURCE_PASSWORD=PUT_YOUR_POSTGRESQL_PASSWORD_HERE
{:copy-code}
```
##### [Optional] Update bind ports
If ThingsBoard Edge is going to be running on the same machine where ThingsBoard server (cloud) is running, you'll need to update configuration parameters to avoid port collision between ThingsBoard server and ThingsBoard Edge.
Please execute the following command to update ThingsBoard Edge configuration file (**/etc/tb-edge/conf/tb-edge.conf**):
```bash
sudo sh -c 'cat <<EOL >> /etc/tb-edge/conf/tb-edge.conf
export HTTP_BIND_PORT=18080
export MQTT_BIND_PORT=11883
export COAP_BIND_PORT=15683
export LWM2M_ENABLED=false
EOL'
{:copy-code}
```
Make sure that ports above (18080, 11883, 15683) are not used by any other application.
#### Run installation script
Once ThingsBoard Edge is installed and configured please execute the following install script:
```bash
sudo /usr/share/tb-edge/bin/install/install.sh
{:copy-code}
```
#### Restart ThingsBoard Edge service
```bash
sudo service tb-edge restart
{:copy-code}
```
#### Open ThingsBoard Edge UI
Once started, you will be able to open **ThingsBoard Edge UI** using the following link http://localhost:8080.
###### NOTE: Edge HTTP bind port update
Use next **ThingsBoard Edge UI** link **http://localhost:18080** if you updated HTTP 8080 bind port to **18080**.

11
application/src/main/data/json/system/widget_bundles/gateway_widgets.json

@ -10,13 +10,12 @@
"widgetTypeFqns": [ "widgetTypeFqns": [
"gateway_widgets.gateway_configuration", "gateway_widgets.gateway_configuration",
"gateway_widgets.attributes_card", "gateway_widgets.attributes_card",
"gateway_widgets.gateway_configuration2", "gateway_widgets.gateway_general_configuration",
"gateway_widgets.config_form_latest", "gateway_widgets.config_form_latest",
"gateway_widgets.gateway_events", "gateway_widgets.gateway_connectors",
"gateway_widgets.gateway_connector",
"gateway_widgets.gateway_logs", "gateway_widgets.gateway_logs",
"gateway_widgets.gateway_statistics", "gateway_widgets.gateway_custom_statistics",
"gateway_widgets.gateway_general_statistics", "gateway_widgets.gateway_general_chart_statistics",
"gateway_widgets.service_rpc" "gateway_widgets.service_rpc"
] ]
} }

19
application/src/main/data/json/system/widget_types/gateway_configuration2.json

File diff suppressed because one or more lines are too long

19
application/src/main/data/json/system/widget_types/gateway_connector.json

File diff suppressed because one or more lines are too long

19
application/src/main/data/json/system/widget_types/gateway_connectors.json

@ -0,0 +1,19 @@
{
"fqn": "gateway_widgets.gateway_connectors",
"name": "Gateway connectors",
"deprecated": false,
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAA7VBMVEXg4ODf39/g4ODg4OAAAAD////88vPg4OD39/fo6Ojv7+9hYWHt7e2GhobLy8upqan99fb9+Pn++/zDw8OXl5fb29vnjJHspao9PT1VVVXPz8/l5eX65efd3d1tbW2enp7hc3nz8/O2tra6urrU1NT32dvcWmHxv8KPj4+YmJixsbGgoKBzc3P+/f332Nuqqqp6enrXQEj0zM7Z2dlJSUnZTVR9fX3pmZ2wsLDusraSkpLr6+tpaWmRkZGNjY3eZ23k5OTkf4Xjf4XkgIWBgYHeZm3aTVXvsrbfZ23UMTnFxcXusrWzs7PkgIbeZmxYj4YaAAAABXRSTlPvIMCwAL23wXkAAAxzSURBVHja7NbBquMgGIbhdM75KLbILILw/b+g6L4YqKvCuf/bmtjNTLMJswm2+BIU1M2DWTh9n35N53fv6/Q9nabLDW/e7bIyvi74gC7rb/X299G6TdMZH9F5QDprQHprQHprQHprC1FrM96xDSQyLAzYTWVGX71CDC1QqEi5GEDVlRnITqMBoFHbWLJJkdYhlXZo1nl+LuKw9iEzFSvCG6mBBbZWYQblKjXB1sCIzKuIu1KyabsKqbSZQeQ4yT4k0qD1oIetaf1QLbhAmWdmxJpkgWdWZgRJCAEiq1GQrOKo9iGFd7SCAJl3K4BY0MIwR4YgdIxYaxCxTQwJgIosGYe1D1FmwAVdBCgbSGFWVfABmLSBwM+WPd0IRFSv1WcWI4IXiPLhtCCIK5wdHylWp2KfkBCMYcZh7UP8lZQ78NOmVwiKsP6GF/IHEMa0kME3yHMxJBzWPgRI/u+0zad/xnYobbb+v/FEGZDeG5DeGpDeGpDeGpDeGpDeGpDeGpDe+lDIH/LqoMVtGAjD8OmDDO4kBzUgjWAGDXtY6EEC+STw//9btZO27CYBey+l0PdgsBnLPAgsEpHy58YBeMe9mvEhugJS8KLTCTtdReSKnYrQ06NwHGKJueNXogDUcS8JPmRzQaRXjvf3PcnSItvOTJg4CT5HDKWjEAagQ4maBkmNizo17aXp/ABpiFRVqbKOdn/h5vjxfVdCEcSwoKyEoY7njCFh+7Kt1zLUi7bQamwsxEcgc1LEzBbEJ0lFq3q79Mk5P0BYZYV0rrz0mFvlS543x3ltT0IRmCnl2Je2rVzwVG6pgi89VXbeZq6zSEIkG8ZHdyTCzJWTKMzU06RshvQAkRRJWY2NIth0Ul0hbzif8XYAMpoikign1ReQK2W1aVItqYVt5ppwg1Ca+hGIupcN0sZIMksK6mMED1N43BEskWZpvyHGwbG2QYB9iERHrIN9EnsBsSbNbASHJmwzd8gctvsDEDGzbBDJS61UF4f34kuH2+e/lgQUK2HxEKQYgmCdOgopBmDOiLYUOAte5EvFbc3u2GZKBVW4wcZfOke+nXCkxkAkfLmQ8r91IHYAGV8vl//rZP+pIQxGPTLYwKhHBhsYRh7xYh8WwGv4xMioRwYXALBftr1twkAA/nRXNIwdAgFpwEaFwSZvG0jNixpt7bq264f9/7+zI2xL46Rj+dAITXtELOQAukfH+cx/kdfCHWA3A7fvIi7+Yp7e5nWI6vEuTVPEOEhzpNMcf+L2WmTngQ95XagUwyBf3lL06XJexVWex7gz6Ra5zFbeNZwV02NeIFZbkeZAvCIXVYUh7nA7RbKNbdtTPmyYwfmY4Y6YIn96JlLFeK8qpfAZXSLcbmlb/vURlUyXDI5iJbAjOXKV1BaAluDRIzyH2A5mQohi+f02flCfw1ZkGawLrHAPt0PEsVv0RWsCJlzAqISIM5AJZyzhEiz6QSIZd6LmlOYAosw5FOELCXzKm0doAD+B7dCyt17FzWs0XyoaaiTWyxjXxtrVIWJvdMNmetECJmNOA3MoaD8Tk5HmCybGwuPvs5LrGzFejUc6A+KYiCjBF1xw+tcyRPBUOkQ2m68N9seXREQjkmWQeT4blSNBoUzFZEIBMVbCFGBBcy+JZH5UZmcRMV8tBiaegEhzAUL+FlkwYEKCxzQsGNN/EvEc0uCCUgodIiGtuQHOg8emnWCQhieKXLQe07dbjzdwyEr7N5YofRAsmkRjEIn0fcl8x7OcTPqfJM01+EeK3bN8GEtYOVQsdCPQYNTIbuGKqcJVGgZPeRhQyRf5aTUC/F3j8dqNkk2sgzayT5HWOL8qKBffwqCo0WTY3RC9D94lnB8jI7EqFN6HGIRf0oCk8ng/If3dogz3RYqmi6SKROKnILxbX+X7CemviGGy7SIqx5qOmhqJ4dHnTSPM8C8ZzHq9+yWG7qAbd9j375F/6AvxR3vn2ps2DIXh7nJSbzj2WifOh4QtUy7kAmikChd1gra7a5f//3N2nKwrkGyQSWMh6ys4GCuK+sousR/OCfdGTi5IJ3Rxwp50Qqw7U+veSLvUQiOj8506AiPnz/ZboZy33MidDStfZYM3uFR08xwZ13i1kloYhtlxkMa14QgdJ1zjWmGAPZONQdnDyAWmylzAAbWbNE4K0ui6jUjji57S/GWh8z9POCH0F32UqCe2UUWoJ415tjYiSBqzSbOtruiVEqdKV1UnJOa6gFqRtX4apXGVBwwJ0CEjsaEzZkyXRhFq4IN0XenkMgxKI4v82m1KGqe9Um9PCz2pI42Ug+AMPI/btsc9fJ8Q4nHKpz42KfYBYkjQSZU0CmB6gYOmvyWN2jjAoLbpDj4xBviuKWmcK70e/hbQIdMy+gZjqWnQJV2a84QLOzVjsjT92NR9QPkpVIwkKeg/SCP926SxV+iWNF7VItO+Io3eGmnkifqDNkgjjWpJo+AJU6eY/m3SyHuleGmkyoXMyBaxGRPjjjQufWonCeU0Ikbfx77CR7/OiDnsc1acYhdp/PouHIfa2J1JvJyMV+4brdnUejksSePpE6U6vsWMhAJLBQhCPcpA2JRzSnh6BXOmmrSYfJxzG7Zk+sCB9YGlugcgbFChnjSOc+3zQH5wB/LTwJI3lnYdNPtnh8uoh0T+bxM64ldh0KZkaCHccjWJDRkGf0AayeXF6TkcWNWPrYF2MysB3ey9xFcrOBbSONoijQh8SyNaKLP31/kW2WqvkU0nt1eR20e25aPNi0YYnWn76az1pBFR426dj1q/serSVve/N+I/7YT87hjpzNS6N9Iutc/IXhfE9hs5W19oOeoRoG737KpV6qzdRkbrFGU1K7jWTZgjzfoQ4gYRuVb2c9HYZiOjTa412SKNWVPS+HL+ujecw+H1rAlpRCf77dmnl0q1TJGwPuyhvk+rjNLH4NPiFDZDFaFmqztxxpPsoza7HZEMB6UhaXzRK5X+iqLQSKQCakX5OqIQUQ1ptBVppIZIPdPTeRlqdrpWmGfazcdVgARFUzQ+nGlhM/gw3CSNNmzLEwqNpgaHlEfCVGXGfhRTmsaCTxk2bTPVAQghU1IFMB5wnSUCiF4ljbvUkDQO9yGNjOOrYSPDSmFp633GOSMJjUGnNMI+lKnzGtKog+EdhDTuHpFEjYjYzGn0PJZukUZCYEmrgC5JFGkUeBTsMhI4jlNETT2CpkbmvVLz0giBbdkRSxLEuzr9acTwGBNcRHSKzYQXRkTqG6SONFLOaCSSZJeR/DocTBzLlaFmyXEu3zs7plY9jk+fFhpBVVT4KphgEuITfKHAGAFfEGCmamIfyhc1n1pXUBxPBedENQBDfSqgMwnGwQzJnJxYluJaC6chaTz/8ur5Ae7oae8ijYPwo6NNkDTO8pl8V51Zo91X9pdPCRxY1SEZW5r8QRoHE3mz0GS2OSDtXaJsfgCXV5GbgTZbqKqLVXh9PKTx95eSYyKNcNaRhIH9MjhGo/ZvrDq01b030plUwM4kZ3Zmat0baZdaZ6QjSf6jZ3d1iAv1Tfs4W1hFYetgEWjYdG7XKO02srYjWcnZm4JrLdwVGpHX1nsNN1hBs0KYy4tLOKSqPhxEP/lW0XE2CYJGpUmnqhJR/wdWtF2kUcompPGyTNgamlco+2kdQuQJhXqRzaJD2FaBJXhfnYL4HFWEmq1uHoxDdCOt0siN5eROJRWwEWmsOrF1KuIizZJiIORHrqXqMSOCTdWnTOl1OY19MIfM1k0vpuZSmEWo2VWN1d0ELBdNDD6rqXbtInhsRlGQYCv11gBdlWvZdMkjYrwwPMRwOvEM3feN1OO67xmGMKdLdVxSY8RIgMcsEQfIoOu9TpV2kEaRoCHDLrmWPWWJKjr2aQxTIHpBGik3a4x4EdUPRBr1Xin9V/XswgPKCyN3Rce+aac+9JURgka48ruMpv2qES9CG8Lbo+h4bA2cBU6tLFA17QOrAoQa3WHArsn71dPII1ES3ZFGL+a+r/OE6EyksYd9SrUjQhRpLE6xA2I7OZYch4ElJeJGaa0GeVPSWDLTL6QQ1MlU3TYBfKEY8ChKsWnjs2wSKERhW+pQFcAWafGuCPWkMbccZ6BIYy4t+S7TsqakUeXGxyYcXpWppb7eUUXHoawlja1domwOSeaihZI0jj9J+cbJg6MhjZtOLFc6mpVh0qxmfdbkN2vTR5sXjehkl46jEAZgtOdNkEZQMXLSjR8IPj152I2fbH7UnR/Rfvzg0emx6+Thg8ffAbOE0ZCNlb+aAAAAAElFTkSuQmCC",
"description": "Allow to create and manage gateway connectors.",
"descriptor": {
"type": "latest",
"sizeX": 11,
"sizeY": 8,
"resources": [],
"templateHtml": "<tb-gateway-connector [device]=\"entityId\" *ngIf=\"entityId\" [ctx]=\"ctx\"></tb-gateway-connector>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n if (self.ctx.datasources && self.ctx.datasources.length) {\n self.ctx.$scope.entityId = self.ctx.datasources[0].entity.id;\n }\n};\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n singleEntity: true\n };\n}",
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Gateway connectors\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":500},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}"
}
}

23
application/src/main/data/json/system/widget_types/gateway_custom_statistics.json

File diff suppressed because one or more lines are too long

10
application/src/main/data/json/system/widget_types/gateway_general_statistics.json → application/src/main/data/json/system/widget_types/gateway_general_chart_statistics.json

File diff suppressed because one or more lines are too long

19
application/src/main/data/json/system/widget_types/gateway_general_configuration.json

File diff suppressed because one or more lines are too long

10
application/src/main/data/json/system/widget_types/gateway_logs.json

File diff suppressed because one or more lines are too long

23
application/src/main/data/json/system/widget_types/gateway_statistics.json

File diff suppressed because one or more lines are too long

6
application/src/main/data/json/system/widget_types/service_rpc.json

File diff suppressed because one or more lines are too long

6
application/src/main/java/org/thingsboard/server/controller/EdgeController.java

@ -557,17 +557,19 @@ public class EdgeController extends BaseController {
notes = "Get a docker install instructions for provided edge id." + TENANT_AUTHORITY_PARAGRAPH, notes = "Get a docker install instructions for provided edge id." + TENANT_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE) produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/instructions/{edgeId}", method = RequestMethod.GET) @RequestMapping(value = "/edge/instructions/{edgeId}/{method}", method = RequestMethod.GET)
@ResponseBody @ResponseBody
public EdgeInstallInstructions getEdgeDockerInstallInstructions( public EdgeInstallInstructions getEdgeDockerInstallInstructions(
@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) @ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("edgeId") String strEdgeId, @PathVariable("edgeId") String strEdgeId,
@ApiParam(value = "Installation method ('docker', 'ubuntu' or 'centos')")
@PathVariable("method") String installationMethod,
HttpServletRequest request) throws ThingsboardException { HttpServletRequest request) throws ThingsboardException {
if (isEdgesEnabled() && edgeInstallServiceOpt.isPresent()) { if (isEdgesEnabled() && edgeInstallServiceOpt.isPresent()) {
EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); EdgeId edgeId = new EdgeId(toUUID(strEdgeId));
edgeId = checkNotNull(edgeId); edgeId = checkNotNull(edgeId);
Edge edge = checkEdgeId(edgeId, Operation.READ); Edge edge = checkEdgeId(edgeId, Operation.READ);
return checkNotNull(edgeInstallServiceOpt.get().getDockerInstallInstructions(getTenantId(), edge, request)); return checkNotNull(edgeInstallServiceOpt.get().getInstallInstructions(getTenantId(), edge, installationMethod, request));
} else { } else {
throw new ThingsboardException("Edges support disabled", ThingsboardErrorCode.GENERAL); throw new ThingsboardException("Edges support disabled", ThingsboardErrorCode.GENERAL);
} }

50
application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallService.java

@ -28,7 +28,6 @@ import org.thingsboard.server.service.install.InstallScripts;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
@ -56,7 +55,20 @@ public class DefaultEdgeInstallService implements EdgeInstallService {
private String appVersion; private String appVersion;
@Override @Override
public EdgeInstallInstructions getDockerInstallInstructions(TenantId tenantId, Edge edge, HttpServletRequest request) { public EdgeInstallInstructions getInstallInstructions(TenantId tenantId, Edge edge, String installationMethod, HttpServletRequest request) {
switch (installationMethod.toLowerCase()) {
case "docker":
return getDockerInstallInstructions(edge, request);
case "ubuntu":
return getUbuntuInstallInstructions(edge, request);
case "centos":
return getCentosInstallInstructions(edge, request);
default:
throw new IllegalArgumentException("Unsupported installation method for Edge: " + installationMethod);
}
}
private EdgeInstallInstructions getDockerInstallInstructions(Edge edge, HttpServletRequest request) {
String dockerInstallInstructions = readFile(resolveFile("docker", "instructions.md")); String dockerInstallInstructions = readFile(resolveFile("docker", "instructions.md"));
String baseUrl = request.getServerName(); String baseUrl = request.getServerName();
if (baseUrl.contains("localhost") || baseUrl.contains("127.0.0.1")) { if (baseUrl.contains("localhost") || baseUrl.contains("127.0.0.1")) {
@ -70,16 +82,40 @@ public class DefaultEdgeInstallService implements EdgeInstallService {
String edgeVersion = appVersion + "EDGE"; String edgeVersion = appVersion + "EDGE";
edgeVersion = edgeVersion.replace("-SNAPSHOT", ""); edgeVersion = edgeVersion.replace("-SNAPSHOT", "");
dockerInstallInstructions = dockerInstallInstructions.replace("${TB_EDGE_VERSION}", edgeVersion); dockerInstallInstructions = dockerInstallInstructions.replace("${TB_EDGE_VERSION}", edgeVersion);
dockerInstallInstructions = dockerInstallInstructions.replace("${CLOUD_ROUTING_KEY}", edge.getRoutingKey()); dockerInstallInstructions = replacePlaceholders(dockerInstallInstructions, edge);
dockerInstallInstructions = dockerInstallInstructions.replace("${CLOUD_ROUTING_SECRET}", edge.getSecret());
dockerInstallInstructions = dockerInstallInstructions.replace("${CLOUD_RPC_PORT}", Integer.toString(rpcPort));
dockerInstallInstructions = dockerInstallInstructions.replace("${CLOUD_RPC_SSL_ENABLED}", Boolean.toString(sslEnabled));
return new EdgeInstallInstructions(dockerInstallInstructions); return new EdgeInstallInstructions(dockerInstallInstructions);
} }
private EdgeInstallInstructions 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);
}
private EdgeInstallInstructions 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);
}
private String replacePlaceholders(String instructions, Edge edge) {
instructions = instructions.replace("${CLOUD_ROUTING_KEY}", edge.getRoutingKey());
instructions = instructions.replace("${CLOUD_ROUTING_SECRET}", edge.getSecret());
instructions = instructions.replace("${CLOUD_RPC_PORT}", Integer.toString(rpcPort));
instructions = instructions.replace("${CLOUD_RPC_SSL_ENABLED}", Boolean.toString(sslEnabled));
return instructions;
}
private String readFile(Path file) { private String readFile(Path file) {
try { try {
return new String(Files.readAllBytes(file), StandardCharsets.UTF_8); return Files.readString(file);
} catch (IOException e) { } catch (IOException e) {
log.warn("Failed to read file: {}", file, e); log.warn("Failed to read file: {}", file, e);
throw new RuntimeException(e); throw new RuntimeException(e);

3
application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallService.java

@ -23,6 +23,5 @@ import javax.servlet.http.HttpServletRequest;
public interface EdgeInstallService { public interface EdgeInstallService {
EdgeInstallInstructions getDockerInstallInstructions(TenantId tenantId, Edge edge, HttpServletRequest request); EdgeInstallInstructions getInstallInstructions(TenantId tenantId, Edge edge, String installationMethod, HttpServletRequest request);
} }

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/AbstractRuleChainMetadataConstructor.java

@ -97,6 +97,8 @@ public abstract class AbstractRuleChainMetadataConstructor implements RuleChainM
.setDebugMode(node.isDebugMode()) .setDebugMode(node.isDebugMode())
.setConfiguration(JacksonUtil.OBJECT_MAPPER.writeValueAsString(node.getConfiguration())) .setConfiguration(JacksonUtil.OBJECT_MAPPER.writeValueAsString(node.getConfiguration()))
.setAdditionalInfo(JacksonUtil.OBJECT_MAPPER.writeValueAsString(node.getAdditionalInfo())) .setAdditionalInfo(JacksonUtil.OBJECT_MAPPER.writeValueAsString(node.getAdditionalInfo()))
.setSingletonMode(node.isSingletonMode())
.setConfigurationVersion(node.getConfigurationVersion())
.build(); .build();
} }

1
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java

@ -501,6 +501,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
} }
} }
} }
partitionService.recalculatePartitions(serviceInfoProvider.getServiceInfo(), new ArrayList<>(partitionService.getOtherServices(ServiceType.TB_RULE_ENGINE)));
} }
private void forwardToRuleEngineActor(String queueName, TenantId tenantId, ToRuleEngineMsg toRuleEngineMsg, TbMsgCallback callback) { private void forwardToRuleEngineActor(String queueName, TenantId tenantId, ToRuleEngineMsg toRuleEngineMsg, TbMsgCallback callback) {

2
application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java

@ -1107,7 +1107,7 @@ public class EdgeControllerTest extends AbstractControllerTest {
public void testGetEdgeInstallInstructions() throws Exception { public void testGetEdgeInstallInstructions() throws Exception {
Edge edge = constructEdge(tenantId, "Edge for Test Docker Install Instructions", "default", "7390c3a6-69b0-9910-d155-b90aca4b772e", "l7q4zsjplzwhk16geqxy"); 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); Edge savedEdge = doPost("/api/edge", edge, Edge.class);
String installInstructions = doGet("/api/edge/instructions/" + savedEdge.getId().getId().toString(), String.class); String installInstructions = doGet("/api/edge/instructions/" + savedEdge.getId().getId().toString() + "/docker", String.class);
Assert.assertTrue(installInstructions.contains("l7q4zsjplzwhk16geqxy")); Assert.assertTrue(installInstructions.contains("l7q4zsjplzwhk16geqxy"));
Assert.assertTrue(installInstructions.contains("7390c3a6-69b0-9910-d155-b90aca4b772e")); Assert.assertTrue(installInstructions.contains("7390c3a6-69b0-9910-d155-b90aca4b772e"));
} }

13
application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java

@ -32,6 +32,7 @@ import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.RuleChainMetadataRequestMsg; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataRequestMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg;
import org.thingsboard.server.gen.edge.v1.RuleNodeProto;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UplinkMsg;
@ -46,6 +47,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@DaoSqlTest @DaoSqlTest
public class RuleChainEdgeTest extends AbstractEdgeTest { public class RuleChainEdgeTest extends AbstractEdgeTest {
private static final int CONFIGURATION_VERSION = 5;
@Test @Test
public void testRuleChains() throws Exception { public void testRuleChains() throws Exception {
// create rule chain // create rule chain
@ -138,6 +141,10 @@ public class RuleChainEdgeTest extends AbstractEdgeTest {
RuleChainId receivedRuleChainId = RuleChainId receivedRuleChainId =
new RuleChainId(new UUID(ruleChainMetadataUpdateMsg.getRuleChainIdMSB(), ruleChainMetadataUpdateMsg.getRuleChainIdLSB())); new RuleChainId(new UUID(ruleChainMetadataUpdateMsg.getRuleChainIdMSB(), ruleChainMetadataUpdateMsg.getRuleChainIdLSB()));
Assert.assertEquals(ruleChainId, receivedRuleChainId); Assert.assertEquals(ruleChainId, receivedRuleChainId);
for (RuleNodeProto ruleNodeProto : ruleChainMetadataUpdateMsg.getNodesList()) {
Assert.assertEquals(CONFIGURATION_VERSION, ruleNodeProto.getConfigurationVersion());
}
} }
private void createRuleChainMetadata(RuleChain ruleChain) { private void createRuleChainMetadata(RuleChain ruleChain) {
@ -147,7 +154,7 @@ public class RuleChainEdgeTest extends AbstractEdgeTest {
RuleNode ruleNode1 = new RuleNode(); RuleNode ruleNode1 = new RuleNode();
ruleNode1.setName("name1"); ruleNode1.setName("name1");
ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); ruleNode1.setConfigurationVersion(CONFIGURATION_VERSION);
TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration(); TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration();
configuration.setFetchTo(TbMsgSource.METADATA); configuration.setFetchTo(TbMsgSource.METADATA);
configuration.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); configuration.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
@ -156,13 +163,13 @@ public class RuleChainEdgeTest extends AbstractEdgeTest {
RuleNode ruleNode2 = new RuleNode(); RuleNode ruleNode2 = new RuleNode();
ruleNode2.setName("name2"); ruleNode2.setName("name2");
ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); ruleNode2.setConfigurationVersion(CONFIGURATION_VERSION);
ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration)); ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration));
RuleNode ruleNode3 = new RuleNode(); RuleNode ruleNode3 = new RuleNode();
ruleNode3.setName("name3"); ruleNode3.setName("name3");
ruleNode3.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode3.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode3.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); ruleNode3.setConfigurationVersion(CONFIGURATION_VERSION);
ruleNode3.setConfiguration(JacksonUtil.valueToTree(configuration)); ruleNode3.setConfiguration(JacksonUtil.valueToTree(configuration));
List<RuleNode> ruleNodes = new ArrayList<>(); List<RuleNode> ruleNodes = new ArrayList<>();

175
application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java

@ -23,6 +23,7 @@ import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
@ -35,7 +36,9 @@ import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.ServiceInfo; import org.thingsboard.server.gen.transport.TransportProtos.ServiceInfo;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
@ -49,12 +52,17 @@ import java.util.Random;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@Slf4j @Slf4j
@ -78,15 +86,7 @@ public class HashPartitionServiceTest {
applicationEventPublisher = mock(ApplicationEventPublisher.class); applicationEventPublisher = mock(ApplicationEventPublisher.class);
routingInfoService = mock(TenantRoutingInfoService.class); routingInfoService = mock(TenantRoutingInfoService.class);
queueRoutingInfoService = mock(QueueRoutingInfoService.class); queueRoutingInfoService = mock(QueueRoutingInfoService.class);
clusterRoutingService = new HashPartitionService(discoveryService, clusterRoutingService = createPartitionService();
routingInfoService,
applicationEventPublisher,
queueRoutingInfoService);
ReflectionTestUtils.setField(clusterRoutingService, "coreTopic", "tb.core");
ReflectionTestUtils.setField(clusterRoutingService, "corePartitions", 10);
ReflectionTestUtils.setField(clusterRoutingService, "vcTopic", "tb.vc");
ReflectionTestUtils.setField(clusterRoutingService, "vcPartitions", 10);
ReflectionTestUtils.setField(clusterRoutingService, "hashFunctionName", hashFunctionName);
ServiceInfo currentServer = ServiceInfo.newBuilder() ServiceInfo currentServer = ServiceInfo.newBuilder()
.setServiceId("tb-core-0") .setServiceId("tb-core-0")
.addAllServiceTypes(Collections.singletonList(ServiceType.TB_CORE.name())) .addAllServiceTypes(Collections.singletonList(ServiceType.TB_CORE.name()))
@ -101,8 +101,6 @@ public class HashPartitionServiceTest {
.build()); .build());
} }
clusterRoutingService.init();
clusterRoutingService.partitionsInit();
clusterRoutingService.recalculatePartitions(currentServer, otherServers); clusterRoutingService.recalculatePartitions(currentServer, otherServers);
} }
@ -194,25 +192,12 @@ public class HashPartitionServiceTest {
} }
List<Queue> queues = new ArrayList<>(); List<Queue> queues = new ArrayList<>();
Queue systemQueue = new Queue(); queues.add(createQueue(TenantId.SYS_TENANT_ID, 10));
systemQueue.setTenantId(TenantId.SYS_TENANT_ID);
systemQueue.setName("Main");
systemQueue.setTopic(DataConstants.MAIN_QUEUE_TOPIC);
systemQueue.setPartitions(10);
systemQueue.setId(new QueueId(UUID.randomUUID()));
queues.add(systemQueue);
tenants.forEach((tenantId, profileId) -> { tenants.forEach((tenantId, profileId) -> {
Queue isolatedQueue = new Queue(); queues.add(createQueue(tenantId, 2));
isolatedQueue.setTenantId(tenantId); mockRoutingInfo(tenantId, profileId, true);
isolatedQueue.setName("Main");
isolatedQueue.setTopic(DataConstants.MAIN_QUEUE_TOPIC);
isolatedQueue.setPartitions(2);
isolatedQueue.setId(new QueueId(UUID.randomUUID()));
queues.add(isolatedQueue);
when(routingInfoService.getRoutingInfo(eq(tenantId))).thenReturn(new TenantRoutingInfo(tenantId, profileId, true));
}); });
when(queueRoutingInfoService.getAllQueuesRoutingInfo()).thenReturn(queues.stream() mockQueues(queues);
.map(QueueRoutingInfo::new).collect(Collectors.toList()));
List<ServiceInfo> ruleEngines = new ArrayList<>(); List<ServiceInfo> ruleEngines = new ArrayList<>();
Map<TenantProfileId, List<ServiceInfo>> dedicatedServers = new HashMap<>(); Map<TenantProfileId, List<ServiceInfo>> dedicatedServers = new HashMap<>();
@ -275,6 +260,97 @@ public class HashPartitionServiceTest {
}); });
} }
@Test
public void testPartitionChangeEvents_isolatedProfile_oneCommonServer_oneDedicated() {
ServiceInfo commonRuleEngine = ServiceInfo.newBuilder()
.setServiceId("tb-rule-engine-1")
.addAllServiceTypes(List.of(ServiceType.TB_RULE_ENGINE.name()))
.build();
TenantProfileId tenantProfileId = new TenantProfileId(UUID.randomUUID());
ServiceInfo dedicatedRuleEngine = ServiceInfo.newBuilder()
.setServiceId("tb-rule-engine-isolated-1")
.addAllServiceTypes(List.of(ServiceType.TB_RULE_ENGINE.name()))
.addAssignedTenantProfiles(tenantProfileId.toString())
.build();
List<Queue> queues = new ArrayList<>();
Queue systemQueue = createQueue(TenantId.SYS_TENANT_ID, 10);
queues.add(systemQueue);
TenantId tenantId = new TenantId(UUID.randomUUID());
mockRoutingInfo(tenantId, tenantProfileId, false); // not isolated yet
mockQueues(queues);
when(discoveryService.isService(eq(ServiceType.TB_RULE_ENGINE))).thenReturn(true);
Mockito.reset(applicationEventPublisher);
HashPartitionService partitionService_common = createPartitionService();
partitionService_common.recalculatePartitions(commonRuleEngine, List.of(dedicatedRuleEngine));
verifyPartitionChangeEvent(event -> {
return event.getQueueKey().getTenantId().isSysTenantId() &&
event.getQueueKey().getQueueName().equals(DataConstants.MAIN_QUEUE_NAME) &&
event.getPartitions().stream().map(TopicPartitionInfo::getPartition).collect(Collectors.toSet())
.size() == systemQueue.getPartitions();
});
Mockito.reset(applicationEventPublisher);
HashPartitionService partitionService_dedicated = createPartitionService();
partitionService_dedicated.recalculatePartitions(dedicatedRuleEngine, List.of(commonRuleEngine));
verify(applicationEventPublisher, never()).publishEvent(any(PartitionChangeEvent.class));
Queue isolatedQueue = createQueue(tenantId, 3);
queues.add(isolatedQueue);
mockQueues(queues);
mockRoutingInfo(tenantId, tenantProfileId, true); // making isolated
TransportProtos.QueueUpdateMsg queueUpdateMsg = TransportProtos.QueueUpdateMsg.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setQueueIdMSB(isolatedQueue.getUuidId().getMostSignificantBits())
.setQueueIdLSB(isolatedQueue.getUuidId().getLeastSignificantBits())
.setQueueName(isolatedQueue.getName())
.setQueueTopic(isolatedQueue.getTopic())
.setPartitions(isolatedQueue.getPartitions())
.build();
partitionService_common.updateQueue(queueUpdateMsg);
partitionService_common.recalculatePartitions(commonRuleEngine, List.of(dedicatedRuleEngine));
// expecting event about no partitions for isolated queue key
verifyPartitionChangeEvent(event -> {
return event.getQueueKey().getTenantId().equals(tenantId) &&
event.getQueueKey().getQueueName().equals(DataConstants.MAIN_QUEUE_NAME) &&
event.getPartitions().isEmpty();
});
partitionService_dedicated.updateQueue(queueUpdateMsg);
partitionService_dedicated.recalculatePartitions(dedicatedRuleEngine, List.of(commonRuleEngine));
verifyPartitionChangeEvent(event -> {
return event.getQueueKey().getTenantId().equals(tenantId) &&
event.getQueueKey().getQueueName().equals(DataConstants.MAIN_QUEUE_NAME) &&
event.getPartitions().stream().map(TopicPartitionInfo::getPartition).collect(Collectors.toSet())
.size() == isolatedQueue.getPartitions();
});
queues = List.of(systemQueue);
mockQueues(queues);
mockRoutingInfo(tenantId, tenantProfileId, false); // turning off isolation
Mockito.reset(applicationEventPublisher);
TransportProtos.QueueDeleteMsg queueDeleteMsg = TransportProtos.QueueDeleteMsg.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setQueueIdMSB(isolatedQueue.getUuidId().getMostSignificantBits())
.setQueueIdLSB(isolatedQueue.getUuidId().getLeastSignificantBits())
.setQueueName(isolatedQueue.getName())
.build();
partitionService_dedicated.removeQueue(queueDeleteMsg);
partitionService_dedicated.recalculatePartitions(dedicatedRuleEngine, List.of(commonRuleEngine));
verifyPartitionChangeEvent(event -> {
return event.getQueueKey().getTenantId().equals(tenantId) &&
event.getQueueKey().getQueueName().equals(DataConstants.MAIN_QUEUE_NAME) &&
event.getPartitions().isEmpty();
});
}
@Test @Test
public void testIsManagedByCurrentServiceCheck() { public void testIsManagedByCurrentServiceCheck() {
TenantProfileId isolatedProfileId = new TenantProfileId(UUID.randomUUID()); TenantProfileId isolatedProfileId = new TenantProfileId(UUID.randomUUID());
@ -282,9 +358,9 @@ public class HashPartitionServiceTest {
TenantProfileId regularProfileId = new TenantProfileId(UUID.randomUUID()); TenantProfileId regularProfileId = new TenantProfileId(UUID.randomUUID());
TenantId isolatedTenantId = new TenantId(UUID.randomUUID()); TenantId isolatedTenantId = new TenantId(UUID.randomUUID());
when(routingInfoService.getRoutingInfo(eq(isolatedTenantId))).thenReturn(new TenantRoutingInfo(isolatedTenantId, isolatedProfileId, true)); mockRoutingInfo(isolatedTenantId, isolatedProfileId, true);
TenantId regularTenantId = new TenantId(UUID.randomUUID()); TenantId regularTenantId = new TenantId(UUID.randomUUID());
when(routingInfoService.getRoutingInfo(eq(regularTenantId))).thenReturn(new TenantRoutingInfo(regularTenantId, regularProfileId, false)); mockRoutingInfo(regularTenantId, regularProfileId, false);
assertThat(clusterRoutingService.isManagedByCurrentService(isolatedTenantId)).isTrue(); assertThat(clusterRoutingService.isManagedByCurrentService(isolatedTenantId)).isTrue();
assertThat(clusterRoutingService.isManagedByCurrentService(regularTenantId)).isFalse(); assertThat(clusterRoutingService.isManagedByCurrentService(regularTenantId)).isFalse();
@ -296,4 +372,43 @@ public class HashPartitionServiceTest {
assertThat(clusterRoutingService.isManagedByCurrentService(regularTenantId)).isTrue(); assertThat(clusterRoutingService.isManagedByCurrentService(regularTenantId)).isTrue();
} }
private void verifyPartitionChangeEvent(Predicate<PartitionChangeEvent> predicate) {
verify(applicationEventPublisher).publishEvent(argThat(event -> event instanceof PartitionChangeEvent && predicate.test((PartitionChangeEvent) event)));
}
private void mockRoutingInfo(TenantId tenantId, TenantProfileId tenantProfileId, boolean isolatedTbRuleEngine) {
when(routingInfoService.getRoutingInfo(eq(tenantId)))
.thenReturn(new TenantRoutingInfo(tenantId, tenantProfileId, isolatedTbRuleEngine));
}
private void mockQueues(List<Queue> queues) {
when(queueRoutingInfoService.getAllQueuesRoutingInfo()).thenReturn(queues.stream()
.map(QueueRoutingInfo::new).collect(Collectors.toList()));
}
private Queue createQueue(TenantId tenantId, int partitions) {
Queue systemQueue = new Queue();
systemQueue.setTenantId(tenantId);
systemQueue.setName("Main");
systemQueue.setTopic(DataConstants.MAIN_QUEUE_TOPIC);
systemQueue.setPartitions(partitions);
systemQueue.setId(new QueueId(UUID.randomUUID()));
return systemQueue;
}
private HashPartitionService createPartitionService() {
HashPartitionService partitionService = new HashPartitionService(discoveryService,
routingInfoService,
applicationEventPublisher,
queueRoutingInfoService);
ReflectionTestUtils.setField(partitionService, "coreTopic", "tb.core");
ReflectionTestUtils.setField(partitionService, "corePartitions", 10);
ReflectionTestUtils.setField(partitionService, "vcTopic", "tb.vc");
ReflectionTestUtils.setField(partitionService, "vcPartitions", 10);
ReflectionTestUtils.setField(partitionService, "hashFunctionName", hashFunctionName);
partitionService.init();
partitionService.partitionsInit();
return partitionService;
}
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmModificationRequest.java

@ -22,6 +22,8 @@ public interface AlarmModificationRequest {
TenantId getTenantId(); TenantId getTenantId();
AlarmSeverity getSeverity();
long getStartTs(); long getStartTs();
long getEndTs(); long getEndTs();

4
common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeInstallInstructions.java

@ -27,6 +27,6 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor @NoArgsConstructor
public class EdgeInstallInstructions { public class EdgeInstallInstructions {
@ApiModelProperty(position = 1, value = "Markdown with docker install instructions") @ApiModelProperty(position = 1, value = "Markdown with install instructions")
private String dockerInstallInstructions; private String installInstructions;
} }

2
common/edge-api/src/main/proto/edge.proto

@ -163,6 +163,8 @@ message RuleNodeProto {
bool debugMode = 5; bool debugMode = 5;
string configuration = 6; string configuration = 6;
string additionalInfo = 7; string additionalInfo = 7;
bool singletonMode = 8;
int32 configurationVersion = 9;
} }
message NodeConnectionInfoProto { message NodeConnectionInfoProto {

24
common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java

@ -179,7 +179,6 @@ public class HashPartitionService implements PartitionService {
public void removeQueue(TransportProtos.QueueDeleteMsg queueDeleteMsg) { public void removeQueue(TransportProtos.QueueDeleteMsg queueDeleteMsg) {
TenantId tenantId = new TenantId(new UUID(queueDeleteMsg.getTenantIdMSB(), queueDeleteMsg.getTenantIdLSB())); TenantId tenantId = new TenantId(new UUID(queueDeleteMsg.getTenantIdMSB(), queueDeleteMsg.getTenantIdLSB()));
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueDeleteMsg.getQueueName(), tenantId); QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueDeleteMsg.getQueueName(), tenantId);
myPartitions.remove(queueKey);
partitionTopicsMap.remove(queueKey); partitionTopicsMap.remove(queueKey);
partitionSizesMap.remove(queueKey); partitionSizesMap.remove(queueKey);
//TODO: remove after merging tb entity services //TODO: remove after merging tb entity services
@ -272,12 +271,23 @@ public class HashPartitionService implements PartitionService {
final ConcurrentMap<QueueKey, List<Integer>> oldPartitions = myPartitions; final ConcurrentMap<QueueKey, List<Integer>> oldPartitions = myPartitions;
myPartitions = newPartitions; myPartitions = newPartitions;
Set<QueueKey> removed = new HashSet<>();
oldPartitions.forEach((queueKey, partitions) -> { oldPartitions.forEach((queueKey, partitions) -> {
if (!myPartitions.containsKey(queueKey)) { if (!newPartitions.containsKey(queueKey)) {
log.info("[{}] NO MORE PARTITIONS FOR CURRENT KEY", queueKey); removed.add(queueKey);
applicationEventPublisher.publishEvent(new PartitionChangeEvent(this, queueKey, Collections.emptySet()));
} }
}); });
if (serviceInfoProvider.isService(ServiceType.TB_RULE_ENGINE)) {
partitionSizesMap.keySet().stream()
.filter(queueKey -> queueKey.getType() == ServiceType.TB_RULE_ENGINE &&
!queueKey.getTenantId().isSysTenantId() &&
!newPartitions.containsKey(queueKey))
.forEach(removed::add);
}
removed.forEach(queueKey -> {
log.info("[{}] NO MORE PARTITIONS FOR CURRENT KEY", queueKey);
applicationEventPublisher.publishEvent(new PartitionChangeEvent(this, queueKey, Collections.emptySet()));
});
myPartitions.forEach((queueKey, partitions) -> { myPartitions.forEach((queueKey, partitions) -> {
if (!partitions.equals(oldPartitions.get(queueKey))) { if (!partitions.equals(oldPartitions.get(queueKey))) {
@ -306,7 +316,11 @@ public class HashPartitionService implements PartitionService {
if (!changes.isEmpty()) { if (!changes.isEmpty()) {
applicationEventPublisher.publishEvent(new ClusterTopologyChangeEvent(this, changes)); applicationEventPublisher.publishEvent(new ClusterTopologyChangeEvent(this, changes));
responsibleServices.forEach((profileId, serviceInfos) -> { responsibleServices.forEach((profileId, serviceInfos) -> {
log.info("Servers responsible for tenant profile {}: {}", profileId, toServiceIds(serviceInfos)); if (profileId != null) {
log.info("Servers responsible for tenant profile {}: {}", profileId, toServiceIds(serviceInfos));
} else {
log.info("Servers responsible for system queues: {}", toServiceIds(serviceInfos));
}
}); });
} }
} }

7
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmResult.java

@ -37,6 +37,11 @@ public class TbAlarmResult {
} }
public static TbAlarmResult fromAlarmResult(AlarmApiCallResult result) { public static TbAlarmResult fromAlarmResult(AlarmApiCallResult result) {
return new TbAlarmResult(result.isCreated(), result.isModified(), result.isSeverityChanged(), result.isCleared(), result.getAlarm()); return new TbAlarmResult(
result.isCreated(),
result.isModified() && !result.isSeverityChanged(),
result.isSeverityChanged(),
result.isCleared(),
result.getAlarm());
} }
} }

93
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java

@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmModificationRequest;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.device.profile.AlarmCondition; import org.thingsboard.server.common.data.device.profile.AlarmCondition;
import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter; import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter;
@ -219,6 +220,94 @@ public class TbDeviceProfileNodeTest {
} }
@Test
public void testAlarmSeverityUpdate() throws Exception {
init();
DeviceProfile deviceProfile = new DeviceProfile();
DeviceProfileData deviceProfileData = new DeviceProfileData();
AlarmConditionFilter tempFilter = new AlarmConditionFilter();
tempFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature"));
tempFilter.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate temperaturePredicate = new NumericFilterPredicate();
temperaturePredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
temperaturePredicate.setValue(new FilterPredicateValue<>(30.0));
tempFilter.setPredicate(temperaturePredicate);
AlarmCondition alarmTempCondition = new AlarmCondition();
alarmTempCondition.setCondition(Collections.singletonList(tempFilter));
AlarmRule alarmTempRule = new AlarmRule();
alarmTempRule.setCondition(alarmTempCondition);
AlarmConditionFilter highTempFilter = new AlarmConditionFilter();
highTempFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature"));
highTempFilter.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate highTemperaturePredicate = new NumericFilterPredicate();
highTemperaturePredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
highTemperaturePredicate.setValue(new FilterPredicateValue<>(50.0));
highTempFilter.setPredicate(highTemperaturePredicate);
AlarmCondition alarmHighTempCondition = new AlarmCondition();
alarmHighTempCondition.setCondition(Collections.singletonList(highTempFilter));
AlarmRule alarmHighTempRule = new AlarmRule();
alarmHighTempRule.setCondition(alarmHighTempCondition);
DeviceProfileAlarm dpa = new DeviceProfileAlarm();
dpa.setId("highTemperatureAlarmID1");
dpa.setAlarmType("highTemperatureAlarm1");
TreeMap<AlarmSeverity, AlarmRule> createRules = new TreeMap<>();
createRules.put(AlarmSeverity.WARNING, alarmTempRule);
createRules.put(AlarmSeverity.CRITICAL, alarmHighTempRule);
dpa.setCreateRules(createRules);
deviceProfileData.setAlarms(Collections.singletonList(dpa));
deviceProfile.setProfileData(deviceProfileData);
Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile);
Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature")))
.thenReturn(Futures.immediateFuture(Collections.emptyList()));
Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm1")).thenReturn(null);
registerCreateAlarmMock(alarmService.createAlarm(any()), true);
TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING);
when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())).thenReturn(theMsg);
ObjectNode data = JacksonUtil.newObjectNode();
data.put("temperature", 42);
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY,
TbMsgDataType.JSON, JacksonUtil.toString(data), null, null);
node.onMsg(ctx, msg);
verify(ctx).tellSuccess(msg);
verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
TbMsg theMsg2 = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING);
when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())).thenReturn(theMsg2);
AlarmInfo alarm = new AlarmInfo(new Alarm(new AlarmId(UUID.randomUUID())));
alarm.setSeverity(AlarmSeverity.CRITICAL);
Alarm oldAlarm = new Alarm(new AlarmId(UUID.randomUUID()));
oldAlarm.setSeverity(AlarmSeverity.WARNING);
var result = AlarmApiCallResult.builder()
.successful(true)
.created(false)
.modified(true)
.alarm(alarm)
.old(oldAlarm)
.build();
when(alarmService.updateAlarm(any())).thenReturn(result);
data.put("temperature", 52);
TbMsg msg2 = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY,
TbMsgDataType.JSON, JacksonUtil.toString(data), null, null);
node.onMsg(ctx, msg2);
verify(ctx).tellSuccess(msg2);
verify(ctx).enqueueForTellNext(theMsg2, "Alarm Severity Updated");
}
@Test @Test
public void testConstantKeyFilterSimple() throws Exception { public void testConstantKeyFilterSimple() throws Exception {
init(); init();
@ -1605,9 +1694,9 @@ public class TbDeviceProfileNodeTest {
private void registerCreateAlarmMock(AlarmApiCallResult a, boolean created) { private void registerCreateAlarmMock(AlarmApiCallResult a, boolean created) {
when(a).thenAnswer(invocationOnMock -> { when(a).thenAnswer(invocationOnMock -> {
// AlarmCreateOrUpdateActiveRequest request = invocationOnMock.getArgument(0);
AlarmInfo alarm = new AlarmInfo(new Alarm(new AlarmId(UUID.randomUUID()))); AlarmInfo alarm = new AlarmInfo(new Alarm(new AlarmId(UUID.randomUUID())));
alarm.setSeverity(AlarmSeverity.CRITICAL); AlarmModificationRequest request = invocationOnMock.getArgument(0);
alarm.setSeverity(request.getSeverity());
return AlarmApiCallResult.builder() return AlarmApiCallResult.builder()
.successful(true) .successful(true)
.created(created) .created(created)

3
ui-ngx/src/app/core/http/device.service.ts

@ -27,7 +27,8 @@ import {
DeviceCredentials, DeviceCredentials,
DeviceInfo, DeviceInfo,
DeviceInfoQuery, DeviceInfoQuery,
DeviceSearchQuery, PublishLaunchCommand, DeviceSearchQuery,
PublishLaunchCommand,
PublishTelemetryCommand PublishTelemetryCommand
} from '@app/shared/models/device.models'; } from '@app/shared/models/device.models';
import { EntitySubtype } from '@app/shared/models/entity-type.models'; import { EntitySubtype } from '@app/shared/models/entity-type.models';

4
ui-ngx/src/app/core/http/edge.service.ts

@ -114,7 +114,7 @@ export class EdgeService {
return this.http.post<BulkImportResult>('/api/edge/bulk_import', entitiesData, defaultHttpOptionsFromConfig(config)); return this.http.post<BulkImportResult>('/api/edge/bulk_import', entitiesData, defaultHttpOptionsFromConfig(config));
} }
public getEdgeDockerInstallInstructions(edgeId: string, config?: RequestConfig): Observable<EdgeInstallInstructions> { public getEdgeInstallInstructions(edgeId: string, method: string = 'ubuntu', config?: RequestConfig): Observable<EdgeInstallInstructions> {
return this.http.get<EdgeInstallInstructions>(`/api/edge/instructions/${edgeId}`, defaultHttpOptionsFromConfig(config)); return this.http.get<EdgeInstallInstructions>(`/api/edge/instructions/${edgeId}/${method}`, defaultHttpOptionsFromConfig(config));
} }
} }

5
ui-ngx/src/app/core/utils.ts

@ -812,3 +812,8 @@ export const getOS = (): string => {
return os; return os;
}; };
export const camelCase = (str: string): string => {
return _.camelCase(str);
};

14
ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts

@ -17,8 +17,6 @@
import { Inject, Injectable, Type } from '@angular/core'; import { Inject, Injectable, Type } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core';
import { AuthService } from '@core/auth/auth.service';
import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { mergeMap, tap } from 'rxjs/operators'; import { mergeMap, tap } from 'rxjs/operators';
@ -28,7 +26,11 @@ import {
CustomDialogContainerData CustomDialogContainerData
} from '@home/components/widget/dialog/custom-dialog-container.component'; } from '@home/components/widget/dialog/custom-dialog-container.component';
import { SHARED_MODULE_TOKEN } from '@shared/components/tokens'; import { SHARED_MODULE_TOKEN } from '@shared/components/tokens';
import { HOME_COMPONENTS_MODULE_TOKEN, SHARED_HOME_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens'; import {
HOME_COMPONENTS_MODULE_TOKEN,
SHARED_HOME_COMPONENTS_MODULE_TOKEN,
WIDGET_COMPONENTS_MODULE_TOKEN
} from '@home/components/tokens';
@Injectable() @Injectable()
export class CustomDialogService { export class CustomDialogService {
@ -36,12 +38,11 @@ export class CustomDialogService {
private customModules: Array<Type<any>>; private customModules: Array<Type<any>>;
constructor( constructor(
private translate: TranslateService,
private authService: AuthService,
private dynamicComponentFactoryService: DynamicComponentFactoryService, private dynamicComponentFactoryService: DynamicComponentFactoryService,
@Inject(SHARED_MODULE_TOKEN) private sharedModule: Type<any>, @Inject(SHARED_MODULE_TOKEN) private sharedModule: Type<any>,
@Inject(SHARED_HOME_COMPONENTS_MODULE_TOKEN) private sharedHomeComponentsModule: Type<any>, @Inject(SHARED_HOME_COMPONENTS_MODULE_TOKEN) private sharedHomeComponentsModule: Type<any>,
@Inject(HOME_COMPONENTS_MODULE_TOKEN) private homeComponentsModule: Type<any>, @Inject(HOME_COMPONENTS_MODULE_TOKEN) private homeComponentsModule: Type<any>,
@Inject(WIDGET_COMPONENTS_MODULE_TOKEN) private widgetComponentsModule: Type<any>,
public dialog: MatDialog public dialog: MatDialog
) { ) {
} }
@ -52,7 +53,8 @@ export class CustomDialogService {
customDialog(template: string, controller: (instance: CustomDialogComponent) => void, data?: any, customDialog(template: string, controller: (instance: CustomDialogComponent) => void, data?: any,
config?: MatDialogConfig): Observable<any> { config?: MatDialogConfig): Observable<any> {
const modules = [this.sharedModule, CommonModule, this.sharedHomeComponentsModule, this.homeComponentsModule]; const modules = [this.sharedModule, CommonModule, this.sharedHomeComponentsModule, this.homeComponentsModule,
this.widgetComponentsModule];
if (Array.isArray(this.customModules)) { if (Array.isArray(this.customModules)) {
modules.push(...this.customModules); modules.push(...this.customModules);
} }

48
ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.html

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<div mat-dialog-content tb-toast fxLayout="column" toastTarget="dockerCommandDialogContent" style="padding-top: 5px"> <div mat-dialog-content fxLayout="column" style="padding: 0 8px 8px">
<div fxLayout="row" fxLayoutAlign="space-between center"> <div fxLayout="row" fxLayoutAlign="space-between center">
<span class="tb-no-data-text">{{ 'gateway.docker-label' | translate }}</span> <span class="tb-no-data-text">{{ 'gateway.docker-label' | translate }}</span>
<div fxFlexAlign="end" class="tb-help" [tb-help]="helpLink"></div> <div fxFlexAlign="end" class="tb-help" [tb-help]="helpLink"></div>
@ -29,14 +29,9 @@
Windows Windows
</ng-template> </ng-template>
<ng-template matTabContent> <ng-template matTabContent>
<div class="tb-form-panel no-padding no-border tb-tab-body"> <ng-container
<tb-markdown usePlainMarkdown containerClass="start-code" data=" *ngTemplateOutlet="commandsExample; context: {command: commands?.mqtt?.windows }">
```bash </ng-container>
{{windowsCode}}
{:copy-code}
```
"></tb-markdown>
</div>
</ng-template> </ng-template>
</mat-tab> </mat-tab>
<mat-tab> <mat-tab>
@ -45,14 +40,9 @@
Linux Linux
</ng-template> </ng-template>
<ng-template matTabContent> <ng-template matTabContent>
<div class="tb-form-panel no-padding no-border tb-tab-body"> <ng-container
<tb-markdown usePlainMarkdown containerClass="start-code" data=" *ngTemplateOutlet="commandsExample; context: {command: commands?.mqtt?.linux }">
```bash </ng-container>
{{linuxCode}}
{:copy-code}
```
"></tb-markdown>
</div>
</ng-template> </ng-template>
</mat-tab> </mat-tab>
<mat-tab> <mat-tab>
@ -61,15 +51,23 @@
MacOS MacOS
</ng-template> </ng-template>
<ng-template matTabContent> <ng-template matTabContent>
<div class="tb-form-panel no-padding no-border tb-tab-body"> <ng-container
<tb-markdown usePlainMarkdown containerClass="start-code" data=" *ngTemplateOutlet="commandsExample; context: {command: commands?.mqtt?.linux }">
```bash </ng-container>
{{linuxCode}}
{:copy-code}
```
"></tb-markdown>
</div>
</ng-template> </ng-template>
</mat-tab> </mat-tab>
</mat-tab-group> </mat-tab-group>
</div> </div>
<ng-template #commandsExample let-command="command">
<div class="tb-form-panel stroked tb-tab-body">
<div class="tb-form-panel-title" translate>device.connectivity.execute-following-command</div>
<tb-markdown usePlainMarkdown containerClass="start-code"
data="
```bash
{{ command }}
{:copy-code}
```
"></tb-markdown>
</div>
</ng-template>

37
ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.scss

@ -22,16 +22,47 @@
:host ::ng-deep { :host ::ng-deep {
.tb-markdown-view { .tb-markdown-view {
.start-code { .start-code {
code[class*="language-"], pre[class*="language-"] { code[class*="language-"] {
overflow: hidden;
white-space: break-spaces; white-space: break-spaces;
word-break: break-all; word-break: break-all;
} }
pre[class*="language-"] {
overflow: hidden;
background: #F3F6FA;
border-color: #305680;
}
.code-wrapper { .code-wrapper {
padding: 0; padding: 0;
} }
button.clipboard-btn {
right: 0;
p {
color: #305680;
}
p, div {
background-color: #F3F6FA;
}
div {
img {
display: none;
}
&:after {
content: "";
position: initial;
display: block;
width: 18px;
height: 18px;
background: #305680;
mask-image: url(/assets/copy-code-icon.svg);
mask-repeat: no-repeat;
}
}
}
} }
} }
.tb-form-panel.tb-tab-body {
margin-top: 16px;
}
} }

34
ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.ts

@ -15,15 +15,10 @@
/// ///
import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { DeviceService } from '@core/http/device.service'; import { DeviceService } from '@core/http/device.service';
import { helpBaseUrl } from '@shared/models/constants'; import { helpBaseUrl } from '@shared/models/constants';
import { getOS } from '@core/utils'; import { getOS } from '@core/utils';
import { PublishLaunchCommand } from '@shared/models/device.models';
@Component({ @Component({
selector: 'tb-gateway-command', selector: 'tb-gateway-command',
@ -39,17 +34,13 @@ export class DeviceGatewayCommandComponent implements OnInit {
@Input() @Input()
deviceId: string; deviceId: string;
linuxCode: string; commands: PublishLaunchCommand;
windowsCode: string;
helpLink: string = helpBaseUrl + '/docs/iot-gateway/install/docker-installation/'; helpLink: string = helpBaseUrl + '/docs/iot-gateway/install/docker-installation/';
tabIndex = 0; tabIndex = 0;
constructor(protected router: Router, constructor(private cd: ChangeDetectorRef,
protected store: Store<AppState>,
private translate: TranslateService,
private cd: ChangeDetectorRef,
private deviceService: DeviceService) { private deviceService: DeviceService) {
} }
@ -57,7 +48,7 @@ export class DeviceGatewayCommandComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
if (this.deviceId) { if (this.deviceId) {
this.deviceService.getDevicePublishLaunchCommands(this.deviceId).subscribe(commands => { this.deviceService.getDevicePublishLaunchCommands(this.deviceId).subscribe(commands => {
this.createRunCode(commands.mqtt); this.commands = commands;
this.cd.detectChanges(); this.cd.detectChanges();
}); });
} }
@ -78,21 +69,4 @@ export class DeviceGatewayCommandComponent implements OnInit {
this.tabIndex = 1; this.tabIndex = 1;
} }
} }
createRunCode(commands) {
this.linuxCode = commands.linux;
this.windowsCode = commands.windows;
}
onDockerCodeCopied() {
this.store.dispatch(new ActionNotificationShow(
{
message: this.translate.instant('gateway.command-copied-message'),
type: 'success',
target: 'dockerCommandDialogContent',
duration: 1200,
verticalPosition: 'bottom',
horizontalPosition: 'left'
}));
}
} }

1437
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html

File diff suppressed because it is too large

124
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.scss

@ -16,128 +16,76 @@
:host { :host {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: block; display: grid;
grid-template-rows: min-content minmax(auto, 1fr) min-content;
.mat-icon { .configuration-block {
color: rgba(0, 0, 0, .12); display: flex;
} flex-direction: column;
gap: 16px;
.tb-form-panel {
margin-bottom: 20px;
} }
.mat-toolbar { .mat-toolbar {
grid-row: 1;
background: transparent; background: transparent;
color: rgba(0, 0, 0, .87) !important; color: rgba(0, 0, 0, .87) !important;
} }
.mat-content { .tab-group-block {
.expansion-panel-header { min-width: 0;
font-weight: 600; height: 100%;
color: rgba(0, 0, 0, .87) !important; min-height: 0;
} grid-row: 2;
mat-slide-toggle {
margin-bottom: 16px;
}
mat-form-field {
margin-right: 15px;
}
.slider-icon {
position: absolute;
transform: translateY(-3px);
}
.block-title {
font-size: 20px;
font-weight: 400;
padding-top: 16px;
}
.hover-cursor {
cursor: pointer;
}
} }
.security-toggle-group { .toggle-group {
margin-bottom: 15px;
margin-right: auto; margin-right: auto;
} }
.logs-label {
font-weight: 500;
margin-bottom: 10px;
}
.statistics-block {
margin-bottom: 15px;
padding-left: 15px;
padding-top: 15px;
}
.first-capital { .first-capital {
text-transform: capitalize; text-transform: capitalize;
} }
mat-panel-title { textarea {
display: block; resize: none;
padding-top: 20px;
} }
mat-panel-title span { .saving-period {
display: block; flex: 1;
padding-left: 0;
padding-top: 5px;
} }
.tb-hint { .statistics-container {
font-size: 13px; width: 50%;
color: rgba(0, 0, 0, .54);
width: fit-content;
cursor: pointer;
text-transform: none !important;
}
.line-break { .command-container {
width: 100%; width: 100%;
}
} }
textarea { .actions {
resize: none; grid-row: 3;
padding: 8px;
display: flex;
gap: 8px;
justify-content: flex-end;
flex: 1;
} }
} }
:host ::ng-deep { :host ::ng-deep {
.mat-tab-label-active {
color: white;
opacity: 1;
}
.mat-tab-label, .mat-tab-label-active{
min-width: 50px !important;
padding: 3px !important;
margin: 3px !important;
flex-grow: 1;
}
.mat-ink-bar {
height: 100%;
z-index: -10;
border-radius: 5px;
}
.pointer-event { .pointer-event {
pointer-events: all; pointer-events: all;
} }
.mat-mdc-form-field-icon-suffix { .toggle-group span {
z-index: 100; padding: 0 25px;
} }
.security-toggle-group span { .mat-mdc-form-field-icon-suffix {
padding: 0 25px; color: #E0E0E0;
&:hover {
color: #9E9E9E;
}
} }
} }

310
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts

@ -15,15 +15,11 @@
/// ///
import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { import {
FormArray, FormArray,
FormBuilder, FormBuilder,
FormControl, FormControl,
FormGroup, FormGroup,
UntypedFormGroup,
ValidationErrors, ValidationErrors,
ValidatorFn, ValidatorFn,
Validators Validators
@ -41,79 +37,18 @@ import { Observable, of } from 'rxjs';
import { mergeMap } from 'rxjs/operators'; import { mergeMap } from 'rxjs/operators';
import { DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models'; import { DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
import {
export enum StorageTypes { GatewayLogLevel,
MEMORY = 'memory', GecurityTypesTranslationsMap,
FILE = 'file', LocalLogsConfigTranslateMap,
SQLITE = 'sqlite' LocalLogsConfigs,
} LogSavingPeriod,
LogSavingPeriodTranslations,
export enum GatewayLogLevel { SecurityTypes,
none = 'NONE', StorageTypes,
critical = 'CRITICAL', StorageTypesTranslationMap
error = 'ERROR', } from './gateway-widget.models';
warning = 'WARNING', import { deepTrim } from '@core/utils';
info = 'INFO',
debug = 'DEBUG'
}
export enum LogSavingPeriod {
days = 'D',
hours = 'H',
minutes = 'M',
seconds = 'S'
}
export enum LocalLogsConfigs {
service = 'service',
connector = 'connector',
converter = 'converter',
tb_connection = 'tb_connection',
storage = 'storage',
extension = 'extension'
}
export const localLogsConfigLabels = new Map<LocalLogsConfigs, string>([
[LocalLogsConfigs.service, 'Service'],
[LocalLogsConfigs.connector, 'Connector'],
[LocalLogsConfigs.converter, 'Converter'],
[LocalLogsConfigs.tb_connection, 'TB Connection'],
[LocalLogsConfigs.storage, 'Storage'],
[LocalLogsConfigs.extension, 'Extension']
]);
export const logSavingPeriodTranslations = new Map<LogSavingPeriod, string>(
[
[LogSavingPeriod.days, 'gateway.logs.days'],
[LogSavingPeriod.hours, 'gateway.logs.hours'],
[LogSavingPeriod.minutes, 'gateway.logs.minutes'],
[LogSavingPeriod.seconds, 'gateway.logs.seconds']
]
);
export const storageTypesTranslations = new Map<StorageTypes, string>(
[
[StorageTypes.MEMORY, 'gateway.storage-types.memory-storage'],
[StorageTypes.FILE, 'gateway.storage-types.file-storage'],
[StorageTypes.SQLITE, 'gateway.storage-types.sqlite']
]
);
export enum SecurityTypes {
ACCESS_TOKEN = 'accessToken',
USERNAME_PASSWORD = 'usernamePassword',
TLS_ACCESS_TOKEN = 'tlsAccessToken',
TLS_PRIVATE_KEY = 'tlsPrivateKey'
}
export const securityTypesTranslationsMap = new Map<SecurityTypes, string>(
[
[SecurityTypes.ACCESS_TOKEN, 'gateway.security-types.access-token'],
[SecurityTypes.USERNAME_PASSWORD, 'gateway.security-types.username-password'],
[SecurityTypes.TLS_ACCESS_TOKEN, 'gateway.security-types.tls-access-token'],
// [SecurityTypes.TLS_PRIVATE_KEY, 'gateway.security-types.tls-private-key'],
]
);
@Component({ @Component({
selector: 'tb-gateway-configuration', selector: 'tb-gateway-configuration',
@ -124,13 +59,16 @@ export class GatewayConfigurationComponent implements OnInit {
gatewayConfigGroup: FormGroup; gatewayConfigGroup: FormGroup;
storageTypes = storageTypesTranslations; StorageTypes = StorageTypes;
storageTypes = Object.values(StorageTypes) as StorageTypes[];
storageTypesTranslationMap = StorageTypesTranslationMap;
logSavingPeriods = logSavingPeriodTranslations; logSavingPeriods = LogSavingPeriodTranslations;
localLogsConfigLabels = localLogsConfigLabels; localLogsConfigs = Object.keys(LocalLogsConfigs) as LocalLogsConfigs[];
localLogsConfigTranslateMap = LocalLogsConfigTranslateMap;
securityTypes = securityTypesTranslationsMap; securityTypes = GecurityTypesTranslationsMap;
gatewayLogLevel = Object.values(GatewayLogLevel); gatewayLogLevel = Object.values(GatewayLogLevel);
@ -142,24 +80,19 @@ export class GatewayConfigurationComponent implements OnInit {
logSelector: FormControl; logSelector: FormControl;
securityType: SecurityTypes; private initialCredentials: DeviceCredentials;
initialCredentials: DeviceCredentials; constructor(private fb: FormBuilder,
private attributeService: AttributeService,
private deviceService: DeviceService,
constructor(protected router: Router,
protected store: Store<AppState>,
protected fb: FormBuilder,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
public dialog: MatDialog) { private dialog: MatDialog) {
} }
ngOnInit() { ngOnInit() {
this.gatewayConfigGroup = this.fb.group({ this.gatewayConfigGroup = this.fb.group({
thingsboard: this.fb.group({ thingsboard: this.fb.group({
host: [window.location.hostname, [Validators.required]], host: [window.location.hostname, [Validators.required, Validators.pattern(/^[^\s]+$/)]],
port: [1883, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]], port: [1883, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]],
remoteShell: [false, []], remoteShell: [false, []],
remoteConfiguration: [true, []], remoteConfiguration: [true, []],
@ -175,30 +108,30 @@ export class GatewayConfigurationComponent implements OnInit {
handleDeviceRenaming: [true, []], handleDeviceRenaming: [true, []],
checkingDeviceActivity: this.fb.group({ checkingDeviceActivity: this.fb.group({
checkDeviceInactivity: [false, []], checkDeviceInactivity: [false, []],
inactivityTimeoutSeconds: [200, [Validators.min(1)]], inactivityTimeoutSeconds: [200, [Validators.min(1), Validators.pattern(/^[^.\s]+$/)]],
inactivityCheckPeriodSeconds: [500, [Validators.min(1)]] inactivityCheckPeriodSeconds: [500, [Validators.min(1), Validators.pattern(/^[^.\s]+$/)]]
}), }),
security: this.fb.group({ security: this.fb.group({
type: [SecurityTypes.ACCESS_TOKEN, [Validators.required]], type: [SecurityTypes.ACCESS_TOKEN, [Validators.required]],
accessToken: [null, [Validators.required]], accessToken: [null, [Validators.required, Validators.pattern(/^[^.\s]+$/)]],
clientId: [null, []], clientId: [null, [Validators.pattern(/^[^.\s]+$/)]],
username: [null, []], username: [null, [Validators.pattern(/^[^.\s]+$/)]],
password: [null, []], password: [null, [Validators.pattern(/^[^.\s]+$/)]],
caCert: [null, []], caCert: [null, []],
cert: [null, []], cert: [null, []],
privateKey: [null, []], privateKey: [null, []],
}), }),
qos: [1, [Validators.min(0), Validators.max(1), Validators.required]] qos: [1, [Validators.min(0), Validators.max(1), Validators.required, Validators.pattern(/^[^.\s]+$/)]]
}), }),
storage: this.fb.group({ storage: this.fb.group({
type: [StorageTypes.MEMORY, [Validators.required]], type: [StorageTypes.MEMORY, [Validators.required]],
read_records_count: [100, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]], read_records_count: [100, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required, Validators.pattern(/^[^.\s]+$/)]],
max_records_count: [100000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]], max_records_count: [100000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required, Validators.pattern(/^[^.\s]+$/)]],
data_folder_path: ['./data/', []], data_folder_path: ['./data/', [Validators.pattern(/^[^\s]+$/)]],
max_file_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], max_file_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
max_read_records_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], max_read_records_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
max_records_per_file: [10000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], max_records_per_file: [10000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
data_file_path: ['./data/data.db', []], data_file_path: ['./data/data.db', [Validators.pattern(/^[^\s]+$/)]],
messages_ttl_check_in_hours: [1, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], messages_ttl_check_in_hours: [1, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
messages_ttl_in_days: [7, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], messages_ttl_in_days: [7, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
@ -215,13 +148,13 @@ export class GatewayConfigurationComponent implements OnInit {
}), }),
connectors: this.fb.array([]), connectors: this.fb.array([]),
logs: this.fb.group({ logs: this.fb.group({
dateFormat: ['%Y-%m-%d %H:%M:%S', [Validators.required]], dateFormat: ['%Y-%m-%d %H:%M:%S', [Validators.required, Validators.pattern(/^[^\s].*[^\s]$/)]],
logFormat: ['%(asctime)s - |%(levelname)s| - [%(filename)s] - %(module)s - %(funcName)s - %(lineno)d - %(message)s', logFormat: ['%(asctime)s - |%(levelname)s| - [%(filename)s] - %(module)s - %(funcName)s - %(lineno)d - %(message)s',
[Validators.required]], [Validators.required, Validators.pattern(/^[^\s].*[^\s]$/)]],
type: ['remote', [Validators.required]], type: ['remote', [Validators.required]],
remote: this.fb.group({ remote: this.fb.group({
enabled: [false], enabled: [false],
logLevel: [GatewayLogLevel.info, [Validators.required]], logLevel: [GatewayLogLevel.INFO, [Validators.required]],
}), }),
local: this.fb.group({}) local: this.fb.group({})
}) })
@ -231,7 +164,7 @@ export class GatewayConfigurationComponent implements OnInit {
if (password && password !== '') { if (password && password !== '') {
this.gatewayConfigGroup.get('thingsboard.security.username').setValidators([Validators.required]); this.gatewayConfigGroup.get('thingsboard.security.username').setValidators([Validators.required]);
} else { } else {
this.gatewayConfigGroup.get('thingsboard.security.username').setValidators([]); this.gatewayConfigGroup.get('thingsboard.security.username').clearValidators();
} }
this.gatewayConfigGroup.get('thingsboard.security.username').updateValueAndValidity({emitEvent: false}); this.gatewayConfigGroup.get('thingsboard.security.username').updateValueAndValidity({emitEvent: false});
}); });
@ -258,10 +191,10 @@ export class GatewayConfigurationComponent implements OnInit {
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').setValidators([Validators.min(1), Validators.required]); checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').setValidators([Validators.min(1), Validators.required]);
} else { } else {
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').clearValidators(); checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').clearValidators();
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').setErrors(null);
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').clearValidators(); checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').clearValidators();
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').setErrors(null);
} }
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').updateValueAndValidity({emitEvent: false});
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').updateValueAndValidity({emitEvent: false});
}); });
this.gatewayConfigGroup.get('grpc.enabled').valueChanges.subscribe(value => { this.gatewayConfigGroup.get('grpc.enabled').valueChanges.subscribe(value => {
@ -272,7 +205,7 @@ export class GatewayConfigurationComponent implements OnInit {
securityGroup.get('type').valueChanges.subscribe(type => { securityGroup.get('type').valueChanges.subscribe(type => {
this.removeAllSecurityValidators(); this.removeAllSecurityValidators();
if (type === SecurityTypes.ACCESS_TOKEN) { if (type === SecurityTypes.ACCESS_TOKEN) {
securityGroup.get('accessToken').addValidators([Validators.required]); securityGroup.get('accessToken').addValidators([Validators.required, Validators.pattern(/^[^.\s]+$/)]);
securityGroup.get('accessToken').updateValueAndValidity(); securityGroup.get('accessToken').updateValueAndValidity();
} else if (type === SecurityTypes.TLS_PRIVATE_KEY) { } else if (type === SecurityTypes.TLS_PRIVATE_KEY) {
securityGroup.get('caCert').addValidators([Validators.required]); securityGroup.get('caCert').addValidators([Validators.required]);
@ -282,21 +215,19 @@ export class GatewayConfigurationComponent implements OnInit {
securityGroup.get('cert').addValidators([Validators.required]); securityGroup.get('cert').addValidators([Validators.required]);
securityGroup.get('cert').updateValueAndValidity(); securityGroup.get('cert').updateValueAndValidity();
} else if (type === SecurityTypes.TLS_ACCESS_TOKEN) { } else if (type === SecurityTypes.TLS_ACCESS_TOKEN) {
securityGroup.get('accessToken').addValidators([Validators.required]); securityGroup.get('accessToken').addValidators([Validators.required, Validators.pattern(/^[^.\s]+$/)]);
securityGroup.get('accessToken').updateValueAndValidity(); securityGroup.get('accessToken').updateValueAndValidity();
securityGroup.get('caCert').addValidators([Validators.required]); securityGroup.get('caCert').addValidators([Validators.required]);
securityGroup.get('caCert').updateValueAndValidity(); securityGroup.get('caCert').updateValueAndValidity();
} else if (type === SecurityTypes.USERNAME_PASSWORD) { } else if (type === SecurityTypes.USERNAME_PASSWORD) {
securityGroup.addValidators([this.atLeastOneRequired(Validators.required, ['clientId', 'username'])]) securityGroup.addValidators([this.atLeastOneRequired(Validators.required, ['clientId', 'username'])]);
// securityGroup.get('password').addValidators([Validators.required]);
// securityGroup.get('password').updateValueAndValidity();
} }
securityGroup.updateValueAndValidity(); securityGroup.updateValueAndValidity();
}); });
securityGroup.get('caCert').valueChanges.subscribe(_ => this.cd.detectChanges()); securityGroup.get('caCert').valueChanges.subscribe(() => this.cd.detectChanges());
securityGroup.get('privateKey').valueChanges.subscribe(_ => this.cd.detectChanges()); securityGroup.get('privateKey').valueChanges.subscribe(() => this.cd.detectChanges());
securityGroup.get('cert').valueChanges.subscribe(_ => this.cd.detectChanges()); securityGroup.get('cert').valueChanges.subscribe(() => this.cd.detectChanges());
const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup; const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup;
storageGroup.get('type').valueChanges.subscribe(type => { storageGroup.get('type').valueChanges.subscribe(type => {
@ -306,28 +237,37 @@ export class GatewayConfigurationComponent implements OnInit {
[Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]); [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]);
storageGroup.get('max_records_count').addValidators( storageGroup.get('max_records_count').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('read_records_count').updateValueAndValidity({emitEvent: false});
storageGroup.get('max_records_count').updateValueAndValidity({emitEvent: false});
} else if (type === StorageTypes.FILE) { } else if (type === StorageTypes.FILE) {
storageGroup.get('data_folder_path').addValidators([Validators.required]); storageGroup.get('data_folder_path').addValidators([Validators.required, Validators.pattern(/^[^.\s]+$/)]);
storageGroup.get('max_file_count').addValidators( storageGroup.get('max_file_count').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('max_read_records_count').addValidators( storageGroup.get('max_read_records_count').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('max_records_per_file').addValidators( storageGroup.get('max_records_per_file').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('data_folder_path').updateValueAndValidity({emitEvent: false});
storageGroup.get('max_file_count').updateValueAndValidity({emitEvent: false});
storageGroup.get('max_read_records_count').updateValueAndValidity({emitEvent: false});
storageGroup.get('max_records_per_file').updateValueAndValidity({emitEvent: false});
} else if (type === StorageTypes.SQLITE) { } else if (type === StorageTypes.SQLITE) {
storageGroup.get('data_file_path').addValidators([Validators.required]); storageGroup.get('data_file_path').addValidators([Validators.required, Validators.pattern(/^[^.\s]+$/)]);
storageGroup.get('messages_ttl_check_in_hours').addValidators( storageGroup.get('messages_ttl_check_in_hours').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('messages_ttl_in_days').addValidators( storageGroup.get('messages_ttl_in_days').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('data_file_path').updateValueAndValidity({emitEvent: false});
storageGroup.get('messages_ttl_check_in_hours').updateValueAndValidity({emitEvent: false});
storageGroup.get('messages_ttl_in_days').updateValueAndValidity({emitEvent: false});
} }
}); });
this.fetchConfigAttribute(this.device); this.fetchConfigAttribute(this.device);
} }
atLeastOneRequired(validator: ValidatorFn, controls: string[] = null) { private atLeastOneRequired(validator: ValidatorFn, controls: string[] = null) {
return (group: UntypedFormGroup): ValidationErrors | null => { return (group: FormGroup): ValidationErrors | null => {
if (!controls) { if (!controls) {
controls = Object.keys(group.controls); controls = Object.keys(group.controls);
} }
@ -337,22 +277,10 @@ export class GatewayConfigurationComponent implements OnInit {
}; };
} }
updateSecurityValidators(value: SecurityTypes) { private fetchConfigAttribute(entityId: EntityId) {
this.gatewayConfigGroup.get('thingsboard.security.type').setValue(value, {emitEvent: true}); if (entityId.id === NULL_UUID) {
this.gatewayConfigGroup.get('thingsboard.security.type').markAsDirty(); return;
} }
updateLogType(value: LocalLogsConfigs) {
this.logSelector.setValue(value);
}
updateStorageType(value: StorageTypes) {
this.gatewayConfigGroup.get('storage.type').setValue(value, {emitEvent: true});
this.gatewayConfigGroup.get('storage.type').markAsDirty();
}
fetchConfigAttribute(entityId: EntityId) {
if (entityId.id === NULL_UUID) return;
this.attributeService.getEntityAttributes(entityId, AttributeScope.CLIENT_SCOPE, this.attributeService.getEntityAttributes(entityId, AttributeScope.CLIENT_SCOPE,
['general_configuration', 'grpc_configuration', 'logs_configuration', 'storage_configuration', 'RemoteLoggingLevel']).pipe( ['general_configuration', 'grpc_configuration', 'logs_configuration', 'storage_configuration', 'RemoteLoggingLevel']).pipe(
mergeMap(attributes => attributes.length ? of(attributes) : this.attributeService.getEntityAttributes( mergeMap(attributes => attributes.length ? of(attributes) : this.attributeService.getEntityAttributes(
@ -397,24 +325,26 @@ export class GatewayConfigurationComponent implements OnInit {
if (remoteLoggingLevel) { if (remoteLoggingLevel) {
const remoteLogsFormGroup = this.gatewayConfigGroup.get('logs.remote'); const remoteLogsFormGroup = this.gatewayConfigGroup.get('logs.remote');
remoteLogsFormGroup.patchValue({ remoteLogsFormGroup.patchValue({
enabled: remoteLoggingLevel !== GatewayLogLevel.none, enabled: remoteLoggingLevel !== GatewayLogLevel.NONE,
logLevel: remoteLoggingLevel logLevel: remoteLoggingLevel
}, {emitEvent: false}); }, {emitEvent: false});
remoteLogsFormGroup.markAsPristine(); remoteLogsFormGroup.markAsPristine();
} }
this.cd.detectChanges(); this.cd.detectChanges();
} else { } else {
this.checkAndFetchCredentials({}); this.checkAndFetchCredentials();
} }
}); });
} }
checkAndFetchCredentials(security): void { private checkAndFetchCredentials(security: any = {}): void {
if (security.type !== SecurityTypes.TLS_PRIVATE_KEY) { if (security.type !== SecurityTypes.TLS_PRIVATE_KEY) {
this.deviceService.getDeviceCredentials(this.device.id).subscribe(credentials => { this.deviceService.getDeviceCredentials(this.device.id).subscribe(credentials => {
this.initialCredentials = credentials; this.initialCredentials = credentials;
if (credentials.credentialsType === DeviceCredentialsType.ACCESS_TOKEN || security.type === SecurityTypes.TLS_ACCESS_TOKEN) { if (credentials.credentialsType === DeviceCredentialsType.ACCESS_TOKEN || security.type === SecurityTypes.TLS_ACCESS_TOKEN) {
this.gatewayConfigGroup.get('thingsboard.security.type').setValue(security.type === SecurityTypes.TLS_ACCESS_TOKEN? SecurityTypes.TLS_ACCESS_TOKEN : SecurityTypes.ACCESS_TOKEN); this.gatewayConfigGroup.get('thingsboard.security.type').setValue(security.type === SecurityTypes.TLS_ACCESS_TOKEN
? SecurityTypes.TLS_ACCESS_TOKEN
: SecurityTypes.ACCESS_TOKEN);
this.gatewayConfigGroup.get('thingsboard.security.accessToken').setValue(credentials.credentialsId); this.gatewayConfigGroup.get('thingsboard.security.accessToken').setValue(credentials.credentialsId);
if(security.type === SecurityTypes.TLS_ACCESS_TOKEN) { if(security.type === SecurityTypes.TLS_ACCESS_TOKEN) {
this.gatewayConfigGroup.get('thingsboard.security.caCert').setValue(security.caCert); this.gatewayConfigGroup.get('thingsboard.security.caCert').setValue(security.caCert);
@ -432,7 +362,7 @@ export class GatewayConfigurationComponent implements OnInit {
} }
} }
logsToObj(logsConfig) { private logsToObj(logsConfig: any) {
const logsObject = { const logsObject = {
local: {} local: {}
}; };
@ -453,43 +383,42 @@ export class GatewayConfigurationComponent implements OnInit {
return {local: logsObject, logFormat, dateFormat}; return {local: logsObject, logFormat, dateFormat};
} }
toggleRpcFields(enable: boolean) { private toggleRpcFields(enable: boolean) {
const grpcGroup = this.gatewayConfigGroup.get('grpc') as FormGroup; const grpcGroup = this.gatewayConfigGroup.get('grpc') as FormGroup;
if (enable) { if (enable) {
grpcGroup.get('serverPort').enable(); grpcGroup.get('serverPort').enable({emitEvent: false});
grpcGroup.get('keepAliveTimeMs').enable(); grpcGroup.get('keepAliveTimeMs').enable({emitEvent: false});
grpcGroup.get('keepAliveTimeoutMs').enable(); grpcGroup.get('keepAliveTimeoutMs').enable({emitEvent: false});
grpcGroup.get('keepalivePermitWithoutCalls').enable(); grpcGroup.get('keepalivePermitWithoutCalls').enable({emitEvent: false});
grpcGroup.get('maxPingsWithoutData').enable(); grpcGroup.get('maxPingsWithoutData').enable({emitEvent: false});
grpcGroup.get('minTimeBetweenPingsMs').enable(); grpcGroup.get('minTimeBetweenPingsMs').enable({emitEvent: false});
grpcGroup.get('minPingIntervalWithoutDataMs').enable(); grpcGroup.get('minPingIntervalWithoutDataMs').enable({emitEvent: false});
} else { } else {
grpcGroup.get('serverPort').disable(); grpcGroup.get('serverPort').disable({emitEvent: false});
grpcGroup.get('keepAliveTimeMs').disable(); grpcGroup.get('keepAliveTimeMs').disable({emitEvent: false});
grpcGroup.get('keepAliveTimeoutMs').disable(); grpcGroup.get('keepAliveTimeoutMs').disable({emitEvent: false});
grpcGroup.get('keepalivePermitWithoutCalls').disable(); grpcGroup.get('keepalivePermitWithoutCalls').disable({emitEvent: false});
grpcGroup.get('maxPingsWithoutData').disable(); grpcGroup.get('maxPingsWithoutData').disable({emitEvent: false});
grpcGroup.get('minTimeBetweenPingsMs').disable(); grpcGroup.get('minTimeBetweenPingsMs').disable({emitEvent: false});
grpcGroup.get('minPingIntervalWithoutDataMs').disable(); grpcGroup.get('minPingIntervalWithoutDataMs').disable({emitEvent: false});
} }
} }
addCommand(command?): void { addCommand(command: any = {}): void {
const data = command || {};
const commandsFormArray = this.commandFormArray(); const commandsFormArray = this.commandFormArray();
const commandFormGroup = this.fb.group({ const commandFormGroup = this.fb.group({
attributeOnGateway: [data.attributeOnGateway || null, [Validators.required]], attributeOnGateway: [command.attributeOnGateway || null, [Validators.required, Validators.pattern(/^[^.\s]+$/)]],
command: [data.command || null, [Validators.required]], command: [command.command || null, [Validators.required, Validators.pattern(/^[^.\s]+$/)]],
timeout: [data.timeout || null, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], timeout: [command.timeout || null, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.pattern(/^[^.\s]+$/)]],
}); });
commandsFormArray.push(commandFormGroup); commandsFormArray.push(commandFormGroup);
} }
addLocalLogConfig(name, config): void { private addLocalLogConfig(name: string, config: any): void {
const localLogsFormGroup = this.gatewayConfigGroup.get('logs.local') as FormGroup; const localLogsFormGroup = this.gatewayConfigGroup.get('logs.local') as FormGroup;
const configGroup = this.fb.group({ const configGroup = this.fb.group({
logLevel: [config.logLevel || GatewayLogLevel.info, [Validators.required]], logLevel: [config.logLevel || GatewayLogLevel.INFO, [Validators.required]],
filePath: [config.filePath || './logs', [Validators.required]], filePath: [config.filePath || './logs', [Validators.required]],
backupCount: [config.backupCount || 7, [Validators.required, Validators.min(0)]], backupCount: [config.backupCount || 7, [Validators.required, Validators.min(0)]],
savingTime: [config.savingTime || 3, [Validators.required, Validators.min(0)]], savingTime: [config.savingTime || 3, [Validators.required, Validators.min(0)]],
@ -507,12 +436,14 @@ export class GatewayConfigurationComponent implements OnInit {
} }
removeCommandControl(index: number, event: any): void { removeCommandControl(index: number, event: any): void {
if (event.pointerType === '') return; if (event.pointerType === '') {
return;
}
this.commandFormArray().removeAt(index); this.commandFormArray().removeAt(index);
this.gatewayConfigGroup.markAsDirty(); this.gatewayConfigGroup.markAsDirty();
} }
removeAllSecurityValidators(): void { private removeAllSecurityValidators(): void {
const securityGroup = this.gatewayConfigGroup.get('thingsboard.security') as FormGroup; const securityGroup = this.gatewayConfigGroup.get('thingsboard.security') as FormGroup;
securityGroup.clearValidators(); securityGroup.clearValidators();
for (const controlsKey in securityGroup.controls) { for (const controlsKey in securityGroup.controls) {
@ -524,7 +455,7 @@ export class GatewayConfigurationComponent implements OnInit {
} }
} }
removeAllStorageValidators(): void { private removeAllStorageValidators(): void {
const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup; const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup;
for (const storageKey in storageGroup.controls) { for (const storageKey in storageGroup.controls) {
if (storageKey !== 'type') { if (storageKey !== 'type') {
@ -535,7 +466,7 @@ export class GatewayConfigurationComponent implements OnInit {
} }
} }
removeEmpty(obj) { private removeEmpty(obj: any) {
return Object.fromEntries( return Object.fromEntries(
Object.entries(obj) Object.entries(obj)
.filter(([_, v]) => v != null) .filter(([_, v]) => v != null)
@ -543,7 +474,7 @@ export class GatewayConfigurationComponent implements OnInit {
); );
} }
generateLogsFile(logsObj) { private generateLogsFile(logsObj: any) {
const logAttrObj = { const logAttrObj = {
version: 1, version: 1,
disable_existing_loggers: false, disable_existing_loggers: false,
@ -591,7 +522,7 @@ export class GatewayConfigurationComponent implements OnInit {
return logAttrObj; return logAttrObj;
} }
createHandlerObj(logObj, key) { private createHandlerObj(logObj: any, key: string) {
return { return {
class: 'thingsboard_gateway.tb_utility.tb_handler.TimedRotatingFileHandler', class: 'thingsboard_gateway.tb_utility.tb_handler.TimedRotatingFileHandler',
formatter: 'LogFormatter', formatter: 'LogFormatter',
@ -603,7 +534,7 @@ export class GatewayConfigurationComponent implements OnInit {
}; };
} }
createLoggerObj(logObj, key) { private createLoggerObj(logObj: any, key: string) {
return { return {
handlers: [`${key}Handler`, 'consoleHandler'], handlers: [`${key}Handler`, 'consoleHandler'],
level: logObj.logLevel, level: logObj.logLevel,
@ -612,12 +543,12 @@ export class GatewayConfigurationComponent implements OnInit {
} }
saveConfig(): void { saveConfig(): void {
const value = this.removeEmpty(this.gatewayConfigGroup.value); const value = deepTrim(this.removeEmpty(this.gatewayConfigGroup.value));
value.thingsboard.statistics.commands = Object.values(value.thingsboard.statistics.commands); value.thingsboard.statistics.commands = Object.values(value.thingsboard.statistics.commands);
const attributes = []; const attributes = [];
attributes.push({ attributes.push({
key: 'RemoteLoggingLevel', key: 'RemoteLoggingLevel',
value: value.logs.remote.enabled ? value.logs.remote.logLevel : GatewayLogLevel.none value: value.logs.remote.enabled ? value.logs.remote.logLevel : GatewayLogLevel.NONE
}); });
delete value.connectors; delete value.connectors;
attributes.push({ attributes.push({
@ -642,18 +573,18 @@ export class GatewayConfigurationComponent implements OnInit {
this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributes).subscribe(_ => { this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributes).subscribe(_ => {
this.updateCredentials(value.thingsboard.security).subscribe(_ => { this.updateCredentials(value.thingsboard.security).subscribe(() => {
if (this.dialogRef) { if (this.dialogRef) {
this.dialogRef.close(); this.dialogRef.close();
} else { } else {
this.gatewayConfigGroup.markAsPristine(); this.gatewayConfigGroup.markAsPristine();
this.cd.detectChanges(); this.cd.detectChanges();
} }
}) });
}); });
} }
updateCredentials(securityConfig): Observable<any> { private updateCredentials(securityConfig: any): Observable<any> {
let updateCredentials = false; let updateCredentials = false;
let newCredentials = {}; let newCredentials = {};
if (securityConfig.type === SecurityTypes.USERNAME_PASSWORD) { if (securityConfig.type === SecurityTypes.USERNAME_PASSWORD) {
@ -661,17 +592,26 @@ export class GatewayConfigurationComponent implements OnInit {
updateCredentials = true; updateCredentials = true;
} else { } else {
const parsedCredentials = JSON.parse(this.initialCredentials.credentialsValue); const parsedCredentials = JSON.parse(this.initialCredentials.credentialsValue);
updateCredentials = !(parsedCredentials.clientId === securityConfig.clientId && parsedCredentials.userName === securityConfig.username && parsedCredentials.password === securityConfig.password); updateCredentials = !(
parsedCredentials.clientId === securityConfig.clientId &&
parsedCredentials.userName === securityConfig.username &&
parsedCredentials.password === securityConfig.password);
} }
if (updateCredentials) { if (updateCredentials) {
let credentialsValue: { clientId?, userName?, password? } = {}; const credentialsValue: { clientId?: string; userName?: string; password?: string } = {};
const credentialsType = DeviceCredentialsType.MQTT_BASIC; const credentialsType = DeviceCredentialsType.MQTT_BASIC;
if (securityConfig.clientId) credentialsValue.clientId = securityConfig.clientId; if (securityConfig.clientId) {
if (securityConfig.username) credentialsValue.userName = securityConfig.username; credentialsValue.clientId = securityConfig.clientId;
if (securityConfig.password) credentialsValue.password = securityConfig.password; }
if (securityConfig.username) {
credentialsValue.userName = securityConfig.username;
}
if (securityConfig.password) {
credentialsValue.password = securityConfig.password;
}
newCredentials = { newCredentials = {
credentialsType, credentialsType,
credentialsValue: JSON.stringify(credentialsValue) credentialsValue: JSON.stringify(credentialsValue)
}; };
} }
} else if (securityConfig.type === SecurityTypes.ACCESS_TOKEN || securityConfig.type === SecurityTypes.TLS_ACCESS_TOKEN) { } else if (securityConfig.type === SecurityTypes.ACCESS_TOKEN || securityConfig.type === SecurityTypes.TLS_ACCESS_TOKEN) {
@ -684,12 +624,12 @@ export class GatewayConfigurationComponent implements OnInit {
newCredentials = { newCredentials = {
credentialsType: DeviceCredentialsType.ACCESS_TOKEN, credentialsType: DeviceCredentialsType.ACCESS_TOKEN,
credentialsId: securityConfig.accessToken credentialsId: securityConfig.accessToken
} };
} }
} }
if (updateCredentials) { if (updateCredentials) {
return this.deviceService.saveDeviceCredentials({...this.initialCredentials,...newCredentials}) return this.deviceService.saveDeviceCredentials({...this.initialCredentials,...newCredentials});
} }
return of(null); return of(null);
} }

117
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html

@ -15,25 +15,25 @@
limitations under the License. limitations under the License.
--> -->
<form style="height: calc(100% - 7px)" fxLayout="column"> <div class="connector-container tb-form-panel no-border">
<div fxLayout="row" fxLayout.lt-lg="column" fxLayoutGap="15px" fxFlex class="connector-container"> <section class="table-section tb-form-panel no-padding flex section-container">
<mat-card fxLayout="column" fxFlex.lt-lg style="overflow: auto; min-height: 35vh"> <mat-toolbar class="mat-mdc-table-toolbar">
<mat-toolbar class="mat-mdc-table-toolbar"> <h2>{{ 'gateway.connectors' | translate }}</h2>
<h2>{{ 'gateway.connectors' | translate }}</h2> <span fxFlex></span>
<span fxFlex></span> <button mat-icon-button
<button mat-icon-button [disabled]="isLoading$ | async"
[disabled]="isLoading$ | async" (click)="addAttribute()"
(click)="addAttribute()" matTooltip="{{ 'action.add' | translate }}"
matTooltip="{{ 'action.add' | translate }}" matTooltipPosition="above">
matTooltipPosition="above"> <mat-icon>add</mat-icon>
<mat-icon>add</mat-icon> </button>
</button> </mat-toolbar>
</mat-toolbar> <div class="table-container">
<table mat-table [dataSource]="dataSource" <table mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()"
matSortDisableClear> matSortDisableClear>
<ng-container matColumnDef="enabled" sticky> <ng-container matColumnDef="enabled" sticky>
<mat-header-cell *matHeaderCellDef style="width: 30px;"> <mat-header-cell *matHeaderCellDef style="width: 60px;min-width: 60px;">
{{ 'gateway.connectors-table-enabled' | translate }} {{ 'gateway.connectors-table-enabled' | translate }}
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let attribute"> <mat-cell *matCellDef="let attribute">
@ -61,12 +61,9 @@
{{ 'gateway.configuration' | translate }} {{ 'gateway.configuration' | translate }}
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let attribute" style="text-transform: uppercase"> <mat-cell *matCellDef="let attribute" style="text-transform: uppercase">
<div <div class="status" [class]="isConnectorSynced(attribute) ? 'status-sync' : 'status-unsync'">
[ngClass]="{ {{ isConnectorSynced(attribute) ? 'sync' : 'out of sync' }}
'status-block': true, </div>
'status-sync':isConnectorSynced(attribute),
'status-unsync':!isConnectorSynced(attribute)
}">{{isConnectorSynced(attribute)?'sync' : 'out of sync'}}</div>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="errors"> <ng-container matColumnDef="errors">
@ -77,10 +74,8 @@
<span class="dot" <span class="dot"
matTooltip="{{ 'Errors: '+ getErrorsCount(attribute)}}" matTooltip="{{ 'Errors: '+ getErrorsCount(attribute)}}"
matTooltipPosition="above" matTooltipPosition="above"
[ngClass]="{ [class]="{'hasErrors': +getErrorsCount(attribute) > 0,
'hasErrors': +getErrorsCount(attribute) > 0, 'noErrors': +getErrorsCount(attribute) === 0 || getErrorsCount(attribute) === ''}"></span>
'noErrors': +getErrorsCount(attribute) == 0 || getErrorsCount(attribute) == ''
}"></span>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="actions" stickyEnd> <ng-container matColumnDef="actions" stickyEnd>
@ -139,60 +134,60 @@
</div> </div>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}" <mat-header-row class="mat-row-select"
*matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row [ngClass]="{'mat-row-select': true, <mat-row class="mat-row-select" [class]="{'tb-current-entity': isSameConnector(attribute)}"
'tb-current-entity': isSameConnector(attribute)}"
*matRowDef="let attribute; columns: displayedColumns;" (click)="selectConnector(attribute)"></mat-row> *matRowDef="let attribute; columns: displayedColumns;" (click)="selectConnector(attribute)"></mat-row>
</table> </table>
<mat-divider></mat-divider> </div>
</mat-card> </section>
<div [formGroup]="connectorForm" fxLayout="column"> <section [formGroup]="connectorForm" class="tb-form-panel no-border no-padding flex">
<mat-card fxLayout="row" fxLayout.lt-lg="column" fxLayoutGap="15px" fxLayoutGap.lt-lg="5px"> <section class="tb-form-panel input-container section-container">
<mat-form-field class="mat-block tb-value-type"> <section class="tb-form-row tb-standard-fields no-padding no-border column-lt-md input-container">
<mat-form-field class="flex" subscriptSizing="dynamic">
<mat-label>{{ 'gateway.connectors-table-name' | translate }}</mat-label> <mat-label>{{ 'gateway.connectors-table-name' | translate }}</mat-label>
<input matInput formControlName="name" #nameInput/> <input matInput formControlName="name" #nameInput/>
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block tb-value-type"> <mat-form-field class="flex" subscriptSizing="dynamic" hideRequiredMarker>
<mat-label>{{ 'gateway.connectors-table-type' | translate }}</mat-label> <mat-label>{{ 'gateway.connectors-table-type' | translate }}</mat-label>
<mat-select formControlName="type"> <mat-select formControlName="type">
<mat-option style="text-transform: uppercase" <mat-option style="text-transform: uppercase"
*ngFor="let type of gatewayConnectorDefaultTypes | keyvalue" [value]="type.key">{{type.value}}</mat-option> *ngFor="let type of gatewayConnectorDefaultTypes | keyvalue" [value]="type.key">{{ type.value }}</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block tb-value-type" *ngIf="connectorForm.get('type').value === 'grpc'"> <mat-form-field class="flex" *ngIf="connectorForm.get('type').value === 'grpc'" subscriptSizing="dynamic">
<mat-label>{{ 'gateway.connectors-table-key' | translate }}</mat-label> <mat-label>{{ 'gateway.connectors-table-key' | translate }}</mat-label>
<input matInput formControlName="key"/> <input matInput formControlName="key"/>
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block tb-value-type" *ngIf="connectorForm.get('type').value === 'custom'"> <mat-form-field class="flex" *ngIf="connectorForm.get('type').value === 'custom'" subscriptSizing="dynamic">
<mat-label>{{ 'gateway.connectors-table-class' | translate }}</mat-label> <mat-label>{{ 'gateway.connectors-table-class' | translate }}</mat-label>
<input matInput formControlName="class"/> <input matInput formControlName="class"/>
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block tb-value-type"> <mat-form-field class="flex" subscriptSizing="dynamic" hideRequiredMarker>
<mat-label translate>gateway.remote-logging-level</mat-label> <mat-label translate>gateway.remote-logging-level</mat-label>
<mat-select formControlName="logLevel"> <mat-select formControlName="logLevel">
<mat-option *ngFor="let logLevel of gatewayLogLevel" [value]="logLevel">{{logLevel}}</mat-option> <mat-option *ngFor="let logLevel of gatewayLogLevel" [value]="logLevel">{{ logLevel }}</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</mat-card> </section>
<mat-card fxLayout="column" fxFlex> </section>
<tb-json-object-edit <section class="tb-form-panel flex section-container">
fxFlex <tb-json-object-edit
fxLayout="column" fillHeight="true"
jsonRequired class="flex"
label="{{ 'gateway.configuration' | translate }}" fxLayout="column"
formControlName="configurationJson"> jsonRequired
</tb-json-object-edit> label="{{ 'gateway.configuration' | translate }}"
<div fxLayoutAlign="start center"> formControlName="configurationJson">
<button mat-raised-button color="primary" </tb-json-object-edit>
class="action-btns" <div fxLayoutAlign="end center">
type="button" <button mat-raised-button color="primary"
[disabled]="!connectorForm.dirty || connectorForm.invalid" type="button"
(click)="saveConnector()"> [disabled]="!connectorForm.dirty || connectorForm.invalid"
{{ 'action.save' | translate }} (click)="saveConnector()">
</button> {{ 'action.save' | translate }}
</div> </button>
</mat-card> </div>
</div> </section>
</div> </section>
</form> </div>

74
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.scss

@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import '../../../../../../../scss/constants';
:host { :host {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -21,25 +23,32 @@
padding: 0; padding: 0;
.connector-container { .connector-container {
height: 100%;
width: 100%; width: 100%;
flex-direction: row;
@media #{$mat-lt-lg} {
flex-direction: column;
}
.table-section {
min-height: 35vh;
overflow: hidden;
& > mat-card, & > div { .table-container {
min-width: calc(50% - 15px); overflow: auto;
}
} }
mat-card { .flex {
margin: 10px; flex: 1;
padding: 10px;
max-width: 100%;
} }
}
.tb-entity-table { .input-container {
.tb-entity-table-content { height: auto;
width: 100%; }
height: 100%;
background: #fff; .section-container {
overflow: hidden; background-color: #fff;
} }
} }
@ -48,43 +57,24 @@
color: rgba(0, 0, 0, .87) !important; color: rgba(0, 0, 0, .87) !important;
} }
.mat-mdc-form-field {
flex-grow: 1;
}
mat-card {
padding-left: 10px;
background: transparent;
}
.mat-card-selected {
background-color: rgba(48, 86, 128, 0.1);
}
.mat-mdc-slide-toggle { .mat-mdc-slide-toggle {
margin: 15px; margin: 0 8px;
} }
.status-block { .status {
text-align: center; text-align: center;
border-radius: 16px; border-radius: 16px;
font-weight: 500; font-weight: 500;
width: fit-content; width: fit-content;
padding: 5px 15px; padding: 5px 15px;
} &-sync {
background: rgba(25, 128, 56, .06);
.status-sync { color: rgb(25, 128, 56);
background: rgba(25, 128, 56, .06); }
color: rgb(25, 128, 56); &-unsync {
} background: rgba(203, 37, 48, .06);
color: rgb(203, 37, 48);
.status-unsync { }
background: rgba(203, 37, 48, .06);
color: rgb(203, 37, 48);
}
.action-btns {
margin: 10px 10px 0;
} }
mat-row { mat-row {

204
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts

@ -17,14 +17,11 @@
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, NgZone, ViewChild } from '@angular/core'; import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, NgZone, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup, UntypedFormControl, ValidatorFn, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, UntypedFormControl, ValidatorFn, Validators } from '@angular/forms';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { MatDialog } from '@angular/material/dialog';
import { AttributeService } from '@core/http/attribute.service'; import { AttributeService } from '@core/http/attribute.service';
import { DeviceService } from '@core/http/device.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { forkJoin } from 'rxjs'; import { forkJoin, Observable } from 'rxjs';
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
@ -33,47 +30,20 @@ import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { GatewayLogLevel } from '@home/components/widget/lib/gateway/gateway-configuration.component';
import { ActionNotificationShow } from '@core/notification/notification.actions'; import { ActionNotificationShow } from '@core/notification/notification.actions';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetContext } from '@home/models/widget-component.models';
import { deepClone } from '@core/utils'; import { camelCase, deepClone, isString } from '@core/utils';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { DatasourceType, widgetType } from '@shared/models/widget.models'; import { DatasourceType, widgetType } from '@shared/models/widget.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import {
GatewayConnector,
export interface gatewayConnector { GatewayConnectorDefaultTypesTranslates,
name: string; GatewayLogLevel
type: string; } from './gateway-widget.models';
configuration?: string;
configurationJson: string;
logLevel: string;
key?: string;
}
export const GatewayConnectorDefaultTypesTranslates = new Map<string, string>([
['mqtt', 'MQTT'],
['modbus', 'MODBUS'],
['grpc', 'GRPC'],
['opcua', 'OPCUA'],
['opcua_asyncio', 'OPCUA ASYNCIO'],
['ble', 'BLE'],
['request', 'REQUEST'],
['can', 'CAN'],
['bacnet', 'BACNET'],
['odbc', 'ODBC'],
['rest', 'REST'],
['snmp', 'SNMP'],
['ftp', 'FTP'],
['socket', 'SOCKET'],
['xmpp', 'XMPP'],
['ocpp', 'OCCP'],
['custom', 'CUSTOM']
]);
@Component({ @Component({
selector: 'tb-gateway-connector', selector: 'tb-gateway-connector',
@ -84,12 +54,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
pageLink: PageLink; pageLink: PageLink;
attributeDataSource: AttributeDatasource;
inactiveConnectorsDataSource: AttributeDatasource;
serverDataSource: AttributeDatasource;
dataSource: MatTableDataSource<AttributeData>; dataSource: MatTableDataSource<AttributeData>;
displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions']; displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions'];
@ -107,27 +71,29 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
connectorForm: FormGroup; connectorForm: FormGroup;
viewsInited = false;
textSearchMode: boolean; textSearchMode: boolean;
activeConnectors: Array<string>; activeConnectors: Array<string>;
inactiveConnectors: Array<string>; gatewayLogLevel = Object.values(GatewayLogLevel);
InitialActiveConnectors: Array<string>; private inactiveConnectors: Array<string>;
gatewayLogLevel = Object.values(GatewayLogLevel); private attributeDataSource: AttributeDatasource;
private inactiveConnectorsDataSource: AttributeDatasource;
activeData: Array<any> = []; private serverDataSource: AttributeDatasource;
inactiveData: Array<any> = []; private activeData: Array<any> = [];
sharedAttributeData: Array<AttributeData> = []; private inactiveData: Array<any> = [];
initialConnector: gatewayConnector; private sharedAttributeData: Array<AttributeData> = [];
subscriptionOptions: WidgetSubscriptionOptions = { private initialConnector: GatewayConnector;
private subscriptionOptions: WidgetSubscriptionOptions = {
callbacks: { callbacks: {
onDataUpdated: () => this.ctx.ngZone.run(() => { onDataUpdated: () => this.ctx.ngZone.run(() => {
this.onDataUpdated(); this.onDataUpdated();
@ -138,20 +104,17 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
} }
}; };
subscription: IWidgetSubscription; private subscription: IWidgetSubscription;
constructor(protected router: Router, constructor(protected store: Store<AppState>,
protected store: Store<AppState>, private fb: FormBuilder,
protected fb: FormBuilder, private translate: TranslateService,
protected translate: TranslateService, private attributeService: AttributeService,
protected attributeService: AttributeService, private dialogService: DialogService,
protected deviceService: DeviceService,
protected dialogService: DialogService,
private telemetryWsService: TelemetryWebsocketService, private telemetryWsService: TelemetryWebsocketService,
private zone: NgZone, private zone: NgZone,
private utils: UtilsService, private utils: UtilsService,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef) {
public dialog: MatDialog) {
super(store); super(store);
const sortOrder: SortOrder = {property: 'key', direction: Direction.ASC}; const sortOrder: SortOrder = {property: 'key', direction: Direction.ASC};
this.pageLink = new PageLink(1000, 0, null, sortOrder); this.pageLink = new PageLink(1000, 0, null, sortOrder);
@ -186,16 +149,21 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
return data[sortHeaderId] || data.value[sortHeaderId]; return data[sortHeaderId] || data.value[sortHeaderId];
}; };
this.viewsInited = true;
if (this.device) { if (this.device) {
if (this.device.id === NULL_UUID) return; if (this.device.id === NULL_UUID) {
forkJoin(this.attributeService.getEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, ['active_connectors']), return;
this.attributeService.getEntityAttributes(this.device, AttributeScope.SERVER_SCOPE, ['inactive_connectors'])).subscribe(attributes => { }
forkJoin([
this.attributeService.getEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, ['active_connectors']),
this.attributeService.getEntityAttributes(this.device, AttributeScope.SERVER_SCOPE, ['inactive_connectors'])
]).subscribe(attributes => {
if (attributes.length) { if (attributes.length) {
this.activeConnectors = attributes[0].length ? attributes[0][0].value : []; this.activeConnectors = attributes[0].length ? attributes[0][0].value : [];
this.activeConnectors = typeof this.activeConnectors === 'string' ? JSON.parse(this.activeConnectors) : this.activeConnectors; this.activeConnectors = isString(this.activeConnectors) ? JSON.parse(this.activeConnectors as any) : this.activeConnectors;
this.inactiveConnectors = attributes[1].length ? attributes[1][0].value : []; this.inactiveConnectors = attributes[1].length ? attributes[1][0].value : [];
this.inactiveConnectors = typeof this.inactiveConnectors === 'string' ? JSON.parse(this.inactiveConnectors) : this.inactiveConnectors; this.inactiveConnectors = isString(this.inactiveConnectors)
? JSON.parse(this.inactiveConnectors as any)
: this.inactiveConnectors;
this.updateData(true); this.updateData(true);
} else { } else {
this.activeConnectors = []; this.activeConnectors = [];
@ -206,7 +174,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
} }
} }
uniqNameRequired(): ValidatorFn { private uniqNameRequired(): ValidatorFn {
return (c: UntypedFormControl) => { return (c: UntypedFormControl) => {
const newName = c.value.trim().toLowerCase(); const newName = c.value.trim().toLowerCase();
const found = this.dataSource.data.find((connectorAttr) => { const found = this.dataSource.data.find((connectorAttr) => {
@ -229,7 +197,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
saveConnector(): void { saveConnector(): void {
const value = this.connectorForm.value; const value = this.connectorForm.value;
value.configuration = this.camelize(value.name) + '.json'; value.configuration = camelCase(value.name) + '.json';
if (value.type !== 'grpc') { if (value.type !== 'grpc') {
delete value.key; delete value.key;
} }
@ -242,7 +210,9 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
value value
}]; }];
const attributesToDelete = []; const attributesToDelete = [];
const scope = (this.initialConnector && this.activeConnectors.includes(this.initialConnector.name)) ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE; const scope = (this.initialConnector && this.activeConnectors.includes(this.initialConnector.name))
? AttributeScope.SHARED_SCOPE
: AttributeScope.SERVER_SCOPE;
let updateActiveConnectors = false; let updateActiveConnectors = false;
if (this.initialConnector && this.initialConnector.name !== value.name) { if (this.initialConnector && this.initialConnector.name !== value.name) {
attributesToDelete.push({key: this.initialConnector.name}); attributesToDelete.push({key: this.initialConnector.name});
@ -256,19 +226,19 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.inactiveConnectors.splice(inactiveIndex, 1); this.inactiveConnectors.splice(inactiveIndex, 1);
} }
} }
if (!this.activeConnectors.includes(value.name) && scope == AttributeScope.SHARED_SCOPE) { if (!this.activeConnectors.includes(value.name) && scope === AttributeScope.SHARED_SCOPE) {
this.activeConnectors.push(value.name); this.activeConnectors.push(value.name);
updateActiveConnectors = true; updateActiveConnectors = true;
} }
if (!this.inactiveConnectors.includes(value.name) && scope == AttributeScope.SERVER_SCOPE) { if (!this.inactiveConnectors.includes(value.name) && scope === AttributeScope.SERVER_SCOPE) {
this.inactiveConnectors.push(value.name); this.inactiveConnectors.push(value.name);
updateActiveConnectors = true; updateActiveConnectors = true;
} }
const tasks = [this.attributeService.saveEntityAttributes(this.device, scope, attributesToSave)]; const tasks = [this.attributeService.saveEntityAttributes(this.device, scope, attributesToSave)];
if (updateActiveConnectors) { if (updateActiveConnectors) {
tasks.push(this.attributeService.saveEntityAttributes(this.device, scope, [{ tasks.push(this.attributeService.saveEntityAttributes(this.device, scope, [{
key: scope == AttributeScope.SHARED_SCOPE ? 'active_connectors' : 'inactive_connectors', key: scope === AttributeScope.SHARED_SCOPE ? 'active_connectors' : 'inactive_connectors',
value: scope == AttributeScope.SHARED_SCOPE ? this.activeConnectors : this.inactiveConnectors value: scope === AttributeScope.SHARED_SCOPE ? this.activeConnectors : this.inactiveConnectors
}])); }]));
} }
@ -282,7 +252,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}); });
} }
updateData(reload: boolean = false) { private updateData(reload: boolean = false) {
this.pageLink.sortOrder.property = this.sort.active; this.pageLink.sortOrder.property = this.sort.active;
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
this.attributeDataSource.loadAttributes(this.device, AttributeScope.CLIENT_SCOPE, this.pageLink, reload).subscribe(data => { this.attributeDataSource.loadAttributes(this.device, AttributeScope.CLIENT_SCOPE, this.pageLink, reload).subscribe(data => {
@ -301,26 +271,30 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
} }
isConnectorSynced(attribute: AttributeData) { isConnectorSynced(attribute: AttributeData) {
const connectorData = attribute.value; const connectorData = attribute.value;
if (!connectorData.ts) return false; if (!connectorData.ts) {
return false;
}
const clientIndex = this.activeData.findIndex(data => { const clientIndex = this.activeData.findIndex(data => {
const sharedData = data.value; const sharedData = data.value;
return sharedData.name === connectorData.name; return sharedData.name === connectorData.name;
}) });
if (clientIndex == -1) return false; if (clientIndex === -1) {
return false;
}
const sharedIndex = this.sharedAttributeData.findIndex(data => { const sharedIndex = this.sharedAttributeData.findIndex(data => {
const sharedData = data.value; const sharedData = data.value;
return sharedData.name === connectorData.name && sharedData.ts && sharedData.ts <= connectorData.ts; return sharedData.name === connectorData.name && sharedData.ts && sharedData.ts <= connectorData.ts;
}) });
return sharedIndex !== -1; return sharedIndex !== -1;
} }
combineData() { private combineData() {
this.dataSource.data = [...this.activeData, ...this.inactiveData, ...this.sharedAttributeData].filter((item, index, self) => this.dataSource.data = [...this.activeData, ...this.inactiveData, ...this.sharedAttributeData].filter((item, index, self) =>
index === self.findIndex((t) => t.key === item.key) index === self.findIndex((t) => t.key === item.key)
).map(attribute=>{ ).map(attribute => {
attribute.value = typeof attribute.value === 'string' ? JSON.parse(attribute.value) : attribute.value; attribute.value = typeof attribute.value === 'string' ? JSON.parse(attribute.value) : attribute.value;
return attribute return attribute;
}); });
} }
@ -330,14 +304,13 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
} }
this.nameInput.nativeElement.focus(); this.nameInput.nativeElement.focus();
this.clearOutConnectorForm(); this.clearOutConnectorForm();
} }
clearOutConnectorForm(): void { private clearOutConnectorForm(): void {
this.connectorForm.setValue({ this.connectorForm.setValue({
name: '', name: '',
type: 'mqtt', type: 'mqtt',
logLevel: GatewayLogLevel.info, logLevel: GatewayLogLevel.INFO,
key: 'auto', key: 'auto',
class: '', class: '',
configuration: '', configuration: '',
@ -347,7 +320,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.connectorForm.markAsPristine(); this.connectorForm.markAsPristine();
} }
selectConnector(attribute): void { selectConnector(attribute: AttributeData): void {
if (this.connectorForm.disabled) { if (this.connectorForm.disabled) {
this.connectorForm.enable(); this.connectorForm.enable();
} }
@ -363,8 +336,10 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.connectorForm.markAsPristine(); this.connectorForm.markAsPristine();
} }
isSameConnector(attribute): boolean { isSameConnector(attribute: AttributeData): boolean {
if (!this.initialConnector) return false; if (!this.initialConnector) {
return false;
}
const connector = attribute.value; const connector = attribute.value;
return this.initialConnector.name === connector.name; return this.initialConnector.name === connector.name;
} }
@ -378,12 +353,11 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
verticalPosition: 'top', verticalPosition: 'top',
horizontalPosition: 'right', horizontalPosition: 'right',
target: 'dashboardRoot', target: 'dashboardRoot',
// panelClass: this.widgetNamespace,
forceDismiss: true forceDismiss: true
})); }));
} }
returnType(attribute) { returnType(attribute: AttributeData): string {
const value = attribute.value; const value = attribute.value;
return this.gatewayConnectorDefaultTypes.get(value.type); return this.gatewayConnectorDefaultTypes.get(value.type);
} }
@ -396,8 +370,10 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
const content = `All connector data will be deleted.`; const content = `All connector data will be deleted.`;
this.dialogService.confirm(title, content, 'Cancel', 'Delete').subscribe(result => { this.dialogService.confirm(title, content, 'Cancel', 'Delete').subscribe(result => {
if (result) { if (result) {
const tasks = []; const tasks: Array<Observable<any>> = [];
const scope = (this.initialConnector && this.activeConnectors.includes(this.initialConnector.name)) ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE; const scope = (this.initialConnector && this.activeConnectors.includes(this.initialConnector.name))
? AttributeScope.SHARED_SCOPE
: AttributeScope.SERVER_SCOPE;
tasks.push(this.attributeService.deleteEntityAttributes(this.device, scope, [attribute])); tasks.push(this.attributeService.deleteEntityAttributes(this.device, scope, [attribute]));
const activeIndex = this.activeConnectors.indexOf(attribute.key); const activeIndex = this.activeConnectors.indexOf(attribute.key);
const inactiveIndex = this.inactiveConnectors.indexOf(attribute.key); const inactiveIndex = this.inactiveConnectors.indexOf(attribute.key);
@ -408,10 +384,10 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.inactiveConnectors.splice(inactiveIndex, 1); this.inactiveConnectors.splice(inactiveIndex, 1);
} }
tasks.push(this.attributeService.saveEntityAttributes(this.device, scope, [{ tasks.push(this.attributeService.saveEntityAttributes(this.device, scope, [{
key: scope == AttributeScope.SHARED_SCOPE ? 'active_connectors' : 'inactive_connectors', key: scope === AttributeScope.SHARED_SCOPE ? 'active_connectors' : 'inactive_connectors',
value: scope == AttributeScope.SHARED_SCOPE ? this.activeConnectors : this.inactiveConnectors value: scope === AttributeScope.SHARED_SCOPE ? this.activeConnectors : this.inactiveConnectors
}])); }]));
forkJoin(tasks).subscribe(_ => { forkJoin(tasks).subscribe(() => {
if (this.initialConnector ? this.initialConnector.name === attribute.key : true) { if (this.initialConnector ? this.initialConnector.name === attribute.key : true) {
this.clearOutConnectorForm(); this.clearOutConnectorForm();
this.cd.detectChanges(); this.cd.detectChanges();
@ -423,10 +399,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}); });
} }
camelize(str): string {
return str.toLowerCase().replace(/\s+/g, '_');
}
connectorLogs(attribute: AttributeData, $event: Event): void { connectorLogs(attribute: AttributeData, $event: Event): void {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
@ -448,7 +420,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
} }
enableConnector(attribute): void { enableConnector(attribute: AttributeData): void {
const wasEnabled = this.activeConnectors.includes(attribute.key); const wasEnabled = this.activeConnectors.includes(attribute.key);
const scopeOld = wasEnabled ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE; const scopeOld = wasEnabled ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE;
const scopeNew = !wasEnabled ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE; const scopeNew = !wasEnabled ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE;
@ -475,7 +447,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}); });
} }
onDataUpdateError(e: any) { private onDataUpdateError(e: any) {
const exceptionData = this.utils.parseException(e); const exceptionData = this.utils.parseException(e);
let errorText = exceptionData.name; let errorText = exceptionData.name;
if (exceptionData.message) { if (exceptionData.message) {
@ -484,11 +456,11 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
console.error(errorText); console.error(errorText);
} }
onDataUpdated() { private onDataUpdated() {
this.cd.detectChanges(); this.cd.detectChanges();
} }
generateSubscription() { private generateSubscription() {
if (this.subscription) { if (this.subscription) {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
@ -497,21 +469,23 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
type: DatasourceType.entity, type: DatasourceType.entity,
entityType: EntityType.DEVICE, entityType: EntityType.DEVICE,
entityId: this.device.id, entityId: this.device.id,
entityName: "Gateway", entityName: 'Gateway',
timeseries: [] timeseries: []
}]; }];
this.dataSource.data.forEach(value => { this.dataSource.data.forEach(value => {
subscriptionInfo[0].timeseries.push({name: `${value.key}_ERRORS_COUNT`, label: `${value.key}_ERRORS_COUNT`}) subscriptionInfo[0].timeseries.push({name: `${value.key}_ERRORS_COUNT`, label: `${value.key}_ERRORS_COUNT`});
}) });
this.ctx.subscriptionApi.createSubscriptionFromInfo(widgetType.latest, subscriptionInfo,this.subscriptionOptions, false, true).subscribe(subscription => { this.ctx.subscriptionApi.createSubscriptionFromInfo(widgetType.latest, subscriptionInfo, this.subscriptionOptions,
false, true).subscribe(subscription => {
this.subscription = subscription; this.subscription = subscription;
}); });
} }
} }
getErrorsCount(attribute) { getErrorsCount(attribute: AttributeData): string {
const connectorName = attribute.key; const connectorName = attribute.key;
const connector = this.subscription && this.subscription.data.find(data=>data && data.dataKey.name === `${connectorName}_ERRORS_COUNT`); const connector = this.subscription && this.subscription.data
return (connector && this.activeConnectors.includes(connectorName))? connector.data[0][1]: 'Inactive'; .find(data => data && data.dataKey.name === `${connectorName}_ERRORS_COUNT`);
return (connector && this.activeConnectors.includes(connectorName)) ? connector.data[0][1] : 'Inactive';
} }
} }

28
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.html

@ -18,43 +18,39 @@
<nav mat-tab-nav-bar [tabPanel]="tabPanel"> <nav mat-tab-nav-bar [tabPanel]="tabPanel">
<a mat-tab-link *ngFor="let link of logLinks" <a mat-tab-link *ngFor="let link of logLinks"
(click)="onTabChanged(link)" (click)="onTabChanged(link)"
[active]="activeLink.name == link.name"> {{link.name}} </a> [active]="activeLink.name === link.name"> {{ link.name }} </a>
</nav> </nav>
<mat-tab-nav-panel #tabPanel></mat-tab-nav-panel> <mat-tab-nav-panel #tabPanel></mat-tab-nav-panel>
<table mat-table [dataSource]="dataSource" <table mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()"
matSortDisableClear> matSortDisableClear>
<ng-container matColumnDef="ts"> <ng-container matColumnDef="ts">
<mat-header-cell *matHeaderCellDef mat-sort-header> Created time</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header style="width: 20%">{{ 'widgets.gateway.created-time' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let attribute"> <mat-cell *matCellDef="let attribute">
{{ attribute.ts | date:'yyyy-MM-dd HH:mm:ss'}} {{ attribute.ts | date:'yyyy-MM-dd HH:mm:ss' }}
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="status"> <ng-container matColumnDef="status">
<mat-header-cell *matHeaderCellDef mat-sort-header> Status</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header style="width: 10%">{{ 'widgets.gateway.status' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let attribute"> <mat-cell *matCellDef="let attribute">
<span [ngClass]="statusClass(attribute.status )">{{ attribute.status }}</span> <span [class]="statusClass(attribute.status)">{{ attribute.status }}</span>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="message"> <ng-container matColumnDef="message">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 70%">Message</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header style="width: 70%">{{ 'widgets.gateway.message' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let attribute" [ngClass]="statusClassMsg(attribute.status )"> <mat-cell *matCellDef="let attribute" [class]="statusClassMsg(attribute.status)">
{{ attribute.message }} {{ attribute.message }}
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}" <mat-header-row class="mat-row-select" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
*matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> <mat-row class="mat-row-select" *matRowDef="let attribute; columns: displayedColumns;"></mat-row>
<mat-row [ngClass]="{'mat-row-select': true}"
*matRowDef="let attribute; columns: displayedColumns;"></mat-row>
</table> </table>
<span [fxShow]="dataSource.data.length === 0" <span [fxShow]="dataSource.data.length === 0"
fxFlex fxFlex fxLayoutAlign="center center"
fxLayoutAlign="center center" class="no-data-found">{{ 'attribute.no-telemetry-text' | translate }}</span>
class="no-data-found">{{'attribute.no-telemetry-text' | translate}}</span>
<span fxFlex [fxShow]="dataSource.data.length !== 0"></span> <span fxFlex [fxShow]="dataSource.data.length !== 0"></span>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<mat-paginator #paginator <mat-paginator [length]="dataSource.data.length"
[length]="dataSource.data.length"
[pageIndex]="pageLink.page" [pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize" [pageSize]="pageLink.pageSize"
[pageSizeOptions]="[10, 20, 30]"></mat-paginator> [pageSizeOptions]="[10, 20, 30]"></mat-paginator>

155
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.ts

@ -14,55 +14,27 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core'; import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store'; import { MatDialogRef } from '@angular/material/dialog';
import { AppState } from '@core/core.state'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup } from '@angular/forms';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { AttributeService } from '@core/http/attribute.service';
import { DeviceService } from '@core/http/device.service';
import { TranslateService } from '@ngx-translate/core';
import { AttributeData, DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { PageComponent } from '@shared/components/page.component';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
import { AttributeDatasource } from "@home/models/datasource/attribute-datasource"; import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { Direction, SortOrder } from "@shared/models/page/sort-order";
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { GatewayLogLevel } from '@home/components/widget/lib/gateway/gateway-configuration.component';
import { DialogService } from '@core/services/dialog.service';
import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetContext } from '@home/models/widget-component.models';
import { MatPaginator } from '@angular/material/paginator'; import { MatPaginator } from '@angular/material/paginator';
import { GatewayLogData, GatewayStatus, LogLink } from './gateway-widget.models';
export interface GatewayConnector {
name: string;
type: string;
configuration?: string;
configurationJson: string;
logLevel: string;
key?: string;
}
export interface LogLink {
name: string;
key: string;
filterFn?: Function;
}
@Component({ @Component({
selector: 'tb-gateway-logs', selector: 'tb-gateway-logs',
templateUrl: './gateway-logs.component.html', templateUrl: './gateway-logs.component.html',
styleUrls: ['./gateway-logs.component.scss'] styleUrls: ['./gateway-logs.component.scss']
}) })
export class GatewayLogsComponent extends PageComponent implements AfterViewInit { export class GatewayLogsComponent implements AfterViewInit {
pageLink: PageLink; pageLink: PageLink;
attributeDataSource: AttributeDatasource; dataSource: MatTableDataSource<GatewayLogData>;
dataSource: MatTableDataSource<any>
displayedColumns = ['ts', 'status', 'message']; displayedColumns = ['ts', 'status', 'message'];
@ -76,63 +48,39 @@ export class GatewayLogsComponent extends PageComponent implements AfterViewInit
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
@ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatPaginator) paginator: MatPaginator;
connectorForm: FormGroup;
viewsInited = false;
textSearchMode: boolean; textSearchMode: boolean;
activeConnectors: Array<string>;
inactiveConnectors: Array<string>;
InitialActiveConnectors: Array<string>;
gatewayLogLevel = Object.values(GatewayLogLevel);
logLinks: Array<LogLink>; logLinks: Array<LogLink>;
initialConnector: GatewayConnector;
activeLink: LogLink; activeLink: LogLink;
gatewayLogLinks: Array<LogLink> = [ gatewayLogLinks: Array<LogLink> = [
{ {
name: "General", name: 'General',
key: "LOGS" key: 'LOGS'
}, { }, {
name: "Service", name: 'Service',
key: "SERVICE_LOGS" key: 'SERVICE_LOGS'
}, },
{ {
name: "Connection", name: 'Connection',
key: "CONNECTION_LOGS" key: 'CONNECTION_LOGS'
}, { }, {
name: "Storage", name: 'Storage',
key: "STORAGE_LOGS" key: 'STORAGE_LOGS'
}, },
{ {
key: 'EXTENSIONS_LOGS', key: 'EXTENSIONS_LOGS',
name: "Extension" name: 'Extension'
}] }];
constructor(protected router: Router, constructor() {
protected store: Store<AppState>,
protected fb: FormBuilder,
protected translate: TranslateService,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
protected dialogService: DialogService,
public dialog: MatDialog) {
super(store);
const sortOrder: SortOrder = {property: 'ts', direction: Direction.DESC}; const sortOrder: SortOrder = {property: 'ts', direction: Direction.DESC};
this.pageLink = new PageLink(10, 0, null, sortOrder); this.pageLink = new PageLink(10, 0, null, sortOrder);
this.dataSource = new MatTableDataSource<AttributeData>([]); this.dataSource = new MatTableDataSource<GatewayLogData>([]);
} }
ngAfterViewInit() { ngAfterViewInit() {
this.dataSource.sort = this.sort; this.dataSource.sort = this.sort;
this.dataSource.paginator = this.paginator; this.dataSource.paginator = this.paginator;
@ -140,22 +88,18 @@ export class GatewayLogsComponent extends PageComponent implements AfterViewInit
this.ctx.defaultSubscription.options.timeWindowConfig = timewindow; this.ctx.defaultSubscription.options.timeWindowConfig = timewindow;
this.ctx.defaultSubscription.updateDataSubscriptions(); this.ctx.defaultSubscription.updateDataSubscriptions();
return timewindow; return timewindow;
} };
if (this.ctx.settings.isConnectorLog && this.ctx.settings.connectorLogState) { if (this.ctx.settings.isConnectorLog && this.ctx.settings.connectorLogState) {
const connector = this.ctx.stateController.getStateParams()[this.ctx.settings.connectorLogState]; const connector = this.ctx.stateController.getStateParams()[this.ctx.settings.connectorLogState];
this.logLinks = [{ this.logLinks = [{
key: `${connector.key}_LOGS`, key: `${connector.key}_LOGS`,
name: "Connector", name: 'Connector',
filterFn: (attrData)=>{ filterFn: (attrData) => !attrData.message.includes(`_converter.py`)
return !attrData.message.includes(`_converter.py`) }, {
}
},{
key: `${connector.key}_LOGS`, key: `${connector.key}_LOGS`,
name: "Converter", name: 'Converter',
filterFn: (attrData)=>{ filterFn: (attrData) => attrData.message.includes(`_converter.py`)
return attrData.message.includes(`_converter.py`) }];
}
}]
} else { } else {
this.logLinks = this.gatewayLogLinks; this.logLinks = this.gatewayLogLinks;
} }
@ -164,20 +108,20 @@ export class GatewayLogsComponent extends PageComponent implements AfterViewInit
} }
updateData(sort?) { private updateData() {
if (this.ctx.defaultSubscription.data.length && this.ctx.defaultSubscription.data[0]) { if (this.ctx.defaultSubscription.data.length && this.ctx.defaultSubscription.data[0]) {
let attrData = this.ctx.defaultSubscription.data[0].data.map(data => { let attrData = this.ctx.defaultSubscription.data[0].data.map(data => {
let result = { const result = {
ts: data[0], ts: data[0],
key: this.activeLink.key, key: this.activeLink.key,
message: /\[(.*)/.exec(data[1])[0], message: /\[(.*)/.exec(data[1])[0],
status: 'INVALID LOG FORMAT' status: 'INVALID LOG FORMAT' as GatewayStatus
}; };
try { try {
result.status= data[1].match(/\|(\w+)\|/)[1]; result.status = data[1].match(/\|(\w+)\|/)[1];
} catch (e) { } catch (e) {
result.status = 'INVALID LOG FORMAT' result.status = 'INVALID LOG FORMAT' as GatewayStatus;
} }
return result; return result;
@ -186,39 +130,35 @@ export class GatewayLogsComponent extends PageComponent implements AfterViewInit
attrData = attrData.filter(data => this.activeLink.filterFn(data)); attrData = attrData.filter(data => this.activeLink.filterFn(data));
} }
this.dataSource.data = attrData; this.dataSource.data = attrData;
if (sort) {
this.dataSource.sortData(this.dataSource.data, this.sort);
}
} }
} }
onTabChanged(link) { onTabChanged(link: LogLink) {
this.activeLink = link; this.activeLink = link;
this.changeSubscription(); this.changeSubscription();
} }
statusClass(status) { statusClass(status: GatewayStatus): string {
switch (status) { switch (status) {
case GatewayLogLevel.debug: case GatewayStatus.DEBUG:
return "status status-debug"; return 'status status-debug';
case GatewayLogLevel.warning: case GatewayStatus.WARNING:
return "status status-warning"; return 'status status-warning';
case GatewayLogLevel.error: case GatewayStatus.ERROR:
case "EXCEPTION": case GatewayStatus.EXCEPTION:
return "status status-error"; return 'status status-error';
case GatewayLogLevel.info:
default: default:
return "status status-info"; return 'status status-info';
} }
} }
statusClassMsg(status) { statusClassMsg(status?: GatewayStatus): string {
if (status === "EXCEPTION") { if (status === GatewayStatus.EXCEPTION) {
return 'msg-status-exception'; return 'msg-status-exception';
} }
} }
changeSubscription() { private changeSubscription() {
if (this.ctx.datasources && this.ctx.datasources[0].entity && this.ctx.defaultSubscription.options.datasources) { if (this.ctx.datasources && this.ctx.datasources[0].entity && this.ctx.defaultSubscription.options.datasources) {
this.ctx.defaultSubscription.options.datasources[0].dataKeys = [{ this.ctx.defaultSubscription.options.datasources[0].dataKeys = [{
name: this.activeLink.key, name: this.activeLink.key,
@ -229,8 +169,7 @@ export class GatewayLogsComponent extends PageComponent implements AfterViewInit
this.ctx.defaultSubscription.updateDataSubscriptions(); this.ctx.defaultSubscription.updateDataSubscriptions();
this.ctx.defaultSubscription.callbacks.onDataUpdated = () => { this.ctx.defaultSubscription.callbacks.onDataUpdated = () => {
this.updateData(); this.updateData();
} };
} }
} }

75
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-remote-configuration-dialog.html

@ -15,43 +15,38 @@
limitations under the License. limitations under the License.
--> -->
<form style="width: 600px; position: relative;"> <mat-toolbar color="warn">
<mat-toolbar color="warn"> <mat-icon>warning</mat-icon>
<mat-icon>warning</mat-icon> <h2 translate>gateway.configuration-delete-dialog-header</h2>
<h2 translate>gateway.configuration-delete-dialog-header</h2> <span fxFlex></span>
<span fxFlex></span> <button mat-icon-button
<button mat-icon-button (click)="close()"
(click)="close()" type="button">
type="button"> <mat-icon class="material-icons">close</mat-icon>
<mat-icon class="material-icons">close</mat-icon> </button>
</button> </mat-toolbar>
</mat-toolbar> <div mat-dialog-content style="max-width: 600px" class="mat-content" fxLayout="column">
<div mat-dialog-content tb-toast toastTarget="activationLinkDialogContent"> <span innerHTML="{{ 'gateway.configuration-delete-dialog-body' | translate }} <b>{{ gatewayName }}</b>" ></span>
<div class="mat-content" fxLayout="column"> <mat-form-field class="mat-block tb-value-type" style="flex-grow: 0">
<span innerHTML="{{'gateway.configuration-delete-dialog-body' | translate}} <b>{{gatewayName}}</b>" > </span> <mat-label translate>gateway.configuration-delete-dialog-input</mat-label>
<mat-form-field class="mat-block tb-value-type" style="flex-grow: 0"> <input matInput [formControl]="gatewayControl" required/>
<mat-label translate>gateway.configuration-delete-dialog-input</mat-label> <mat-error
<input matInput [formControl]="gatewayControl" required/> *ngIf="gatewayControl.hasError('required')">
<mat-error {{ 'gateway.configuration-delete-dialog-input-required' | translate }}
*ngIf="gatewayControl.hasError('required')"> </mat-error>
{{'gateway.configuration-delete-dialog-input-required' | translate }} </mat-form-field>
</mat-error> </div>
</mat-form-field> <div mat-dialog-actions fxLayoutAlign="end center">
</div> <button mat-button color="warn"
</div> type="button"
<div mat-dialog-actions fxLayoutAlign="end center"> cdkFocusInitial
<button mat-button color="warn" (click)="close()">
type="button" {{ 'action.cancel' | translate }}
cdkFocusInitial </button>
(click)="close()"> <button mat-button color="warn"
{{ 'action.cancel' | translate }} type="button"
</button> [disabled]="gatewayControl.value !== gatewayName"
<button mat-button color="warn" (click)="turnOff()">
type="button" {{ 'gateway.configuration-delete-dialog-confirm' | translate }}
cdkFocusInitial </button>
[disabled]="gatewayControl.value !== gatewayName" </div>
(click)="turnOff()">
{{ 'gateway.configuration-delete-dialog-confirm' | translate }}
</button>
</div>
</form>

12
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-remote-configuration-dialog.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject, OnInit } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -31,11 +31,10 @@ export interface GatewayRemoteConfigurationDialogData {
templateUrl: './gateway-remote-configuration-dialog.html' templateUrl: './gateway-remote-configuration-dialog.html'
}) })
export class GatewayRemoteConfigurationDialogComponent extends DialogComponent<GatewayRemoteConfigurationDialogComponent, export class GatewayRemoteConfigurationDialogComponent extends
boolean> implements OnInit { DialogComponent<GatewayRemoteConfigurationDialogComponent, boolean>{
gatewayName: string; gatewayName: string;
gatewayControl: FormControl; gatewayControl: FormControl;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
@ -45,10 +44,7 @@ export class GatewayRemoteConfigurationDialogComponent extends DialogComponent<G
private fb: FormBuilder) { private fb: FormBuilder) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.gatewayName = this.data.gatewayName; this.gatewayName = this.data.gatewayName;
this.gatewayControl = this.fb.control(null); this.gatewayControl = this.fb.control('');
}
ngOnInit(): void {
} }
close(): void { close(): void {

74
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html

@ -16,38 +16,48 @@
--> -->
<div fxLayout="row" fxLayout.lt-sm="column" class="command-form" fxLayoutGap="10px" [formGroup]="commandForm"> <div fxLayout="row" fxLayout.lt-sm="column" class="command-form" fxLayoutGap="10px" [formGroup]="commandForm">
<mat-form-field class="mat-block tb-value-type"> <ng-container *ngIf="!isConnector; else connectorForm">
<mat-label>{{'gateway.statistics.command' | translate}}</mat-label> <mat-form-field>
<mat-select formControlName="command" *ngIf="!isConnector"> <mat-label>{{ 'gateway.statistics.command' | translate }}</mat-label>
<mat-option *ngFor="let command of RPCCommands" [value]="command"> <mat-select formControlName="command">
{{command}} <mat-option *ngFor="let command of RPCCommands" [value]="command">
</mat-option> {{ command }}
</mat-select> </mat-option>
<input matInput formControlName="command" *ngIf="isConnector"/> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block tb-value-type" fxFlex *ngIf="!isConnector"> <mat-form-field fxFlex>
<mat-label>{{'gateway.statistics.timeout-ms' | translate}}</mat-label> <mat-label>{{ 'gateway.statistics.timeout-ms' | translate }}</mat-label>
<input matInput formControlName="time" type="number" min="1"/> <input matInput formControlName="time" type="number" min="1"/>
<mat-error <mat-error *ngIf="commandForm.get('time').hasError('min')">
*ngIf="commandForm.get('time').hasError('min')"> {{ 'gateway.statistics.timeout-min' | translate }}
{{'gateway.statistics.timeout-min' | translate }} </mat-error>
</mat-error> </mat-form-field>
</mat-form-field> </ng-container>
<mat-form-field class="mat-block tb-value-type" fxFlex *ngIf="isConnector"> <ng-template #connectorForm>
<mat-label>{{'widget-config.datasource-parameters' | translate}}</mat-label> <mat-form-field>
<input matInput formControlName="params" type="JSON"/> <mat-label>{{ 'gateway.statistics.command' | translate }}</mat-label>
<mat-icon class="material-icons-outlined" aria-hidden="false" aria-label="help-icon" <input matInput formControlName="command" />
matSuffix style="cursor:pointer;" </mat-form-field>
(click)="openEditJSONDialog($event)" <mat-form-field fxFlex>
matTooltip="{{'gateway.rpc-command-edit-params' | translate}}">edit <mat-label>{{ 'widget-config.datasource-parameters' | translate }}</mat-label>
</mat-icon> <input matInput formControlName="params" type="JSON"/>
</mat-form-field> <mat-icon class="material-icons-outlined" aria-hidden="false" aria-label="help-icon"
<button mat-raised-button color="primary" (click)="sendCommand()" matIconSuffix style="cursor:pointer;"
[disabled]="commandForm.invalid">{{'gateway.rpc-command-send' | translate}}</button> (click)="openEditJSONDialog($event)"
matTooltip="{{ 'gateway.rpc-command-edit-params' | translate }}">edit
</mat-icon>
</mat-form-field>
</ng-template>
<button mat-raised-button
color="primary"
(click)="sendCommand()"
[disabled]="commandForm.invalid">
{{ 'gateway.rpc-command-send' | translate }}
</button>
</div> </div>
<mat-card class="result-block" [formGroup]="commandForm" fxFlex> <section class="result-block" [formGroup]="commandForm">
<span>{{'gateway.rpc-command-result' | translate}}</span> <span>{{ 'gateway.rpc-command-result' | translate }}</span>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<tb-json-content [contentType]="contentTypes.JSON" readonly="true" formControlName="result" fxFlex></tb-json-content> <tb-json-content [contentType]="contentTypes.JSON" readonly="true" formControlName="result"></tb-json-content>
</mat-card> </section>

16
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss

@ -22,24 +22,24 @@
padding: 0; padding: 0;
.command-form { .command-form {
width: 100%;
flex-wrap: nowrap; flex-wrap: nowrap;
padding: 0 15px 5px; padding: 0 5px 5px;
margin-bottom: 5px;
& > button { & > button {
margin-top: 10px; margin-top: 10px;
} }
} }
.result-block { .result-block {
padding: 0 15px; padding: 0 5px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1;
& > span { & > span {
font-weight: 600; font-weight: 600;
} }
tb-json-content {
flex: 1;
}
} }
} }
@ -47,9 +47,5 @@
.tb-json-content { .tb-json-content {
height: 100%; height: 100%;
} }
.mat-mdc-form-field-icon-suffix {
z-index: 100;
}
} }

67
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts

@ -14,17 +14,9 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { AfterViewInit, Component, Input } from '@angular/core'; import { AfterViewInit, Component, Input } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { AttributeService } from '@core/http/attribute.service';
import { DeviceService } from '@core/http/device.service';
import { TranslateService } from '@ngx-translate/core';
import { PageComponent } from "@shared/components/page.component";
import { DialogService } from '@core/services/dialog.service';
import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetContext } from '@home/models/widget-component.models';
import { ContentType } from '@shared/models/constants'; import { ContentType } from '@shared/models/constants';
import { import {
@ -33,13 +25,12 @@ import {
} from '@shared/components/dialog/json-object-edit-dialog.component'; } from '@shared/components/dialog/json-object-edit-dialog.component';
import { jsonRequired } from '@shared/components/json-object-edit.component'; import { jsonRequired } from '@shared/components/json-object-edit.component';
@Component({ @Component({
selector: 'tb-gateway-service-rpc', selector: 'tb-gateway-service-rpc',
templateUrl: './gateway-service-rpc.component.html', templateUrl: './gateway-service-rpc.component.html',
styleUrls: ['./gateway-service-rpc.component.scss'] styleUrls: ['./gateway-service-rpc.component.scss']
}) })
export class GatewayServiceRPCComponent extends PageComponent implements AfterViewInit { export class GatewayServiceRPCComponent implements AfterViewInit {
@Input() @Input()
ctx: WidgetContext; ctx: WidgetContext;
@ -53,39 +44,28 @@ export class GatewayServiceRPCComponent extends PageComponent implements AfterVi
isConnector: boolean; isConnector: boolean;
connectorType: string;
RPCCommands: Array<string> = [ RPCCommands: Array<string> = [
"Ping", 'Ping',
"Stats", 'Stats',
"Devices", 'Devices',
"Update", 'Update',
"Version", 'Version',
"Restart", 'Restart',
"Reboot" 'Reboot'
] ];
private connectorType: string;
constructor(protected router: Router,
protected store: Store<AppState>, constructor(private fb: FormBuilder,
protected fb: FormBuilder, private dialog: MatDialog) {
protected translate: TranslateService,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
protected dialogService: DialogService,
public dialog: MatDialog) {
super(store);
this.commandForm = this.fb.group({ this.commandForm = this.fb.group({
command: [null,[Validators.required]], command: [null,[Validators.required]],
time: [60, [Validators.required, Validators.min(1)]], time: [60, [Validators.required, Validators.min(1)]],
params: [{}, [jsonRequired]], params: [{}, [jsonRequired]],
result: [null] result: [null]
}) });
} }
ngAfterViewInit() { ngAfterViewInit() {
this.isConnector = this.ctx.settings.isConnector; this.isConnector = this.ctx.settings.isConnector;
if (!this.isConnector) { if (!this.isConnector) {
@ -95,16 +75,16 @@ export class GatewayServiceRPCComponent extends PageComponent implements AfterVi
} }
} }
sendCommand() { sendCommand() {
const formValues = this.commandForm.value; const formValues = this.commandForm.value;
const commandPrefix = this.isConnector ? `${this.connectorType}_` : 'gateway_'; const commandPrefix = this.isConnector ? `${this.connectorType}_` : 'gateway_';
this.ctx.controlApi.sendTwoWayCommand(commandPrefix+formValues.command.toLowerCase(), {},formValues.time).subscribe(resp=>{ this.ctx.controlApi.sendTwoWayCommand(commandPrefix+formValues.command.toLowerCase(), {},formValues.time).subscribe({
this.commandForm.get('result').setValue(JSON.stringify(resp)); next: resp => this.commandForm.get('result').setValue(JSON.stringify(resp)),
},error => { error: error => {
console.log(error); console.log(error);
this.commandForm.get('result').setValue(JSON.stringify(error.error)); this.commandForm.get('result').setValue(JSON.stringify(error.error));
}) }
});
} }
openEditJSONDialog($event: Event) { openEditJSONDialog($event: Event) {
@ -126,5 +106,4 @@ export class GatewayServiceRPCComponent extends PageComponent implements AfterVi
} }
); );
} }
} }

26
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html

@ -19,13 +19,13 @@
<div class="statistics-container" fxLayout="row" fxLayout.lt-md="column"> <div class="statistics-container" fxLayout="row" fxLayout.lt-md="column">
<mat-card [formGroup]="statisticForm" *ngIf="!general"> <mat-card [formGroup]="statisticForm" *ngIf="!general">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label>{{'gateway.statistics.statistic' | translate}}</mat-label> <mat-label>{{ 'gateway.statistics.statistic' | translate }}</mat-label>
<mat-select formControlName="statisticKey"> <mat-select formControlName="statisticKey">
<mat-option *ngFor="let key of statisticsKeys" [value]="key"> <mat-option *ngFor="let key of statisticsKeys" [value]="key">
{{key}} {{ key }}
</mat-option> </mat-option>
<mat-option *ngFor="let command of commands" [value]="command.attributeOnGateway"> <mat-option *ngFor="let command of commands" [value]="command.attributeOnGateway">
{{command.attributeOnGateway}} {{ command.attributeOnGateway }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
@ -34,44 +34,42 @@
{{'gateway.statistics.statistic-commands-empty' | translate }} {{'gateway.statistics.statistic-commands-empty' | translate }}
</mat-error> </mat-error>
<mat-form-field class="mat-block" *ngIf="commandObj"> <mat-form-field class="mat-block" *ngIf="commandObj">
<mat-label>{{'gateway.statistics.command' | translate}}</mat-label> <mat-label>{{ 'gateway.statistics.command' | translate }}</mat-label>
<input matInput [value]="commandObj.command" disabled> <input matInput [value]="commandObj.command" disabled>
</mat-form-field> </mat-form-field>
</mat-card> </mat-card>
<div class="chart-box" fxLayout="column"> <div class="chart-box" fxLayout="column">
<div class="chart-container" #statisticChart [fxShow]="isNumericData"> <div class="chart-container" #statisticChart [fxShow]="isNumericData"></div>
</div>
<table [fxShow]="!isNumericData" mat-table [dataSource]="dataSource" <table [fxShow]="!isNumericData" mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()"
matSortDisableClear> matSortDisableClear>
<ng-container matColumnDef="0"> <ng-container matColumnDef="0">
<mat-header-cell *matHeaderCellDef mat-sort-header>{{'audit-log.timestamp' | translate}}</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'widgets.gateway.created-time' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let row; let rowIndex = index"> <mat-cell *matCellDef="let row; let rowIndex = index">
{{row[0]| date:'yyyy-MM-dd HH:mm:ss' }} {{row[0]| date:'yyyy-MM-dd HH:mm:ss' }}
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="1"> <ng-container matColumnDef="1">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 70%">{{"event.message" | translate}}</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header style="width: 70%">{{ 'widgets.gateway.message' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let row"> <mat-cell *matCellDef="let row">
{{ row[1] }} {{ row[1] }}
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}" <mat-header-row class="mat-row-select"
*matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row [ngClass]="{'mat-row-select': true}" <mat-row class="mat-row-select"
*matRowDef="let row; columns: displayedColumns;"></mat-row> *matRowDef="let row; columns: displayedColumns;"></mat-row>
</table> </table>
<span [fxShow]="dataSource.data.length === 0 && !isNumericData" <span [fxShow]="dataSource.data.length === 0 && !isNumericData"
fxLayoutAlign="center center" fxLayoutAlign="center center"
class="no-data-found">{{'attribute.no-telemetry-text' | translate}}</span> class="no-data-found">{{ 'attribute.no-telemetry-text' | translate }}</span>
<div fxFlex class="legend" fxLayout="row" fxLayoutAlign="center center" [fxShow]="isNumericData"> <div fxFlex class="legend" fxLayout="row" fxLayoutAlign="center center" [fxShow]="isNumericData">
<div class="legend-keys" *ngFor="let legendKey of legendData?.keys" fxLayout="row" <div class="legend-keys" *ngFor="let legendKey of legendData?.keys" fxLayout="row"
fxLayoutAlign="center center"> fxLayoutAlign="center center">
<span class="legend-line" [ngStyle]="{backgroundColor: legendKey.dataKey.color}"></span> <span class="legend-line" [style.background-color]="legendKey.dataKey.color"></span>
<div class="legend-label" <div class="legend-label"
(click)="onLegendKeyHiddenChange(legendKey.dataIndex)" (click)="onLegendKeyHiddenChange(legendKey.dataIndex)"
[ngClass]="{ 'hidden-label': legendData.keys[legendKey.dataIndex].dataKey.hidden }" [class]="{ 'hidden-label': legendData.keys[legendKey.dataIndex].dataKey.hidden }"
[innerHTML]="legendKey.dataKey.label"> [innerHTML]="legendKey.dataKey.label">
</div> </div>
</div> </div>

127
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.ts

@ -14,18 +14,10 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, ViewChild } from '@angular/core'; import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup } from '@angular/forms'; import { FormBuilder, FormGroup } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { AttributeService } from '@core/http/attribute.service'; import { AttributeService } from '@core/http/attribute.service';
import { DeviceService } from '@core/http/device.service';
import { TranslateService } from '@ngx-translate/core';
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { PageComponent } from '@shared/components/page.component';
import { DialogService } from '@core/services/dialog.service';
import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetContext } from '@home/models/widget-component.models';
import { TbFlot } from '@home/components/widget/lib/flot-widget'; import { TbFlot } from '@home/components/widget/lib/flot-widget';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
@ -41,13 +33,12 @@ import { MatTableDataSource } from '@angular/material/table';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
@Component({ @Component({
selector: 'tb-gateway-statistics', selector: 'tb-gateway-statistics',
templateUrl: './gateway-statistics.component.html', templateUrl: './gateway-statistics.component.html',
styleUrls: ['./gateway-statistics.component.scss'] styleUrls: ['./gateway-statistics.component.scss']
}) })
export class GatewayStatisticsComponent extends PageComponent implements AfterViewInit { export class GatewayStatisticsComponent implements AfterViewInit {
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
@ViewChild('statisticChart') statisticChart: ElementRef; @ViewChild('statisticChart') statisticChart: ElementRef;
@ -58,10 +49,11 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
@Input() @Input()
public general: boolean; public general: boolean;
public isNumericData: boolean = false; public isNumericData = false;
public dataTypeDefined: boolean = false;
public chartInited: boolean; public chartInited: boolean;
private flot: TbFlot; private flot: TbFlot;
private flotCtx; private flotCtx: WidgetContext;
public statisticForm: FormGroup; public statisticForm: FormGroup;
public statisticsKeys = []; public statisticsKeys = [];
public commands = []; public commands = [];
@ -89,70 +81,61 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
}; };
constructor(protected router: Router, constructor(private fb: FormBuilder,
protected store: Store<AppState>, private attributeService: AttributeService,
protected fb: FormBuilder, private utils: UtilsService) {
protected translate: TranslateService,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
protected dialogService: DialogService,
private cd: ChangeDetectorRef,
private utils: UtilsService,
public dialog: MatDialog) {
super(store);
const sortOrder: SortOrder = {property: '0', direction: Direction.DESC}; const sortOrder: SortOrder = {property: '0', direction: Direction.DESC};
this.pageLink = new PageLink(Number.POSITIVE_INFINITY, 0, null, sortOrder); this.pageLink = new PageLink(Number.POSITIVE_INFINITY, 0, null, sortOrder);
this.displayedColumns = ['0', '1']; this.displayedColumns = ['0', '1'];
this.dataSource = new MatTableDataSource<any>([]); this.dataSource = new MatTableDataSource<any>([]);
this.statisticForm = this.fb.group({ this.statisticForm = this.fb.group({
statisticKey: [null, []] statisticKey: [null, []]
}) });
this.statisticForm.get('statisticKey').valueChanges.subscribe(value => { this.statisticForm.get('statisticKey').valueChanges.subscribe(value => {
this.commandObj = null; this.commandObj = null;
if (this.commands.length) { if (this.commands.length) {
this.commandObj = this.commands.find(command => command.attributeOnGateway === value); this.commandObj = this.commands.find(command => command.attributeOnGateway === value);
} }
if (this.subscriptionInfo) this.createChartsSubscription(this.ctx.defaultSubscription.datasources[0].entity, value); if (this.subscriptionInfo) {
}) this.createChartsSubscription(this.ctx.defaultSubscription.datasources[0].entity, value);
}
});
} }
ngAfterViewInit() { ngAfterViewInit() {
this.dataSource.sort = this.sort; this.dataSource.sort = this.sort;
this.sort.sortChange.subscribe(_=>{ this.sort.sortChange.subscribe(() => this.sortData());
this.sortData();
})
this.init(); this.init();
if (this.ctx.defaultSubscription.datasources.length) { if (this.ctx.defaultSubscription.datasources.length) {
const gateway = this.ctx.defaultSubscription.datasources[0].entity; const gateway = this.ctx.defaultSubscription.datasources[0].entity;
if (gateway.id.id === NULL_UUID) return; if (gateway.id.id === NULL_UUID) {
return;
}
if (!this.general) { if (!this.general) {
this.attributeService.getEntityAttributes(gateway.id, AttributeScope.SHARED_SCOPE, ["general_configuration"]).subscribe((resp: AttributeData[]) => { this.attributeService.getEntityAttributes(gateway.id, AttributeScope.SHARED_SCOPE, ['general_configuration'])
if (resp && resp.length) { .subscribe((resp: AttributeData[]) => {
this.commands = resp[0].value.statistics.commands; if (resp && resp.length) {
if (!this.statisticForm.get('statisticKey').value && this.commands && this.commands.length) { this.commands = resp[0].value.statistics.commands;
this.statisticForm.get('statisticKey').setValue(this.commands[0].attributeOnGateway); if (!this.statisticForm.get('statisticKey').value && this.commands && this.commands.length) {
this.createChartsSubscription(gateway, this.commands[0].attributeOnGateway); this.statisticForm.get('statisticKey').setValue(this.commands[0].attributeOnGateway);
this.createChartsSubscription(gateway, this.commands[0].attributeOnGateway);
}
} }
} });
})
} else { } else {
let connectorsTs;
this.attributeService.getEntityTimeseriesLatest(gateway.id).subscribe( this.attributeService.getEntityTimeseriesLatest(gateway.id).subscribe(
data => { data => {
connectorsTs = Object.keys(data) const connectorsTs = Object.keys(data)
.filter(el => el.includes( .filter(el => el.includes('ConnectorEventsProduced') || el.includes('ConnectorEventsSent'));
'ConnectorEventsProduced'
) || el.includes(
'ConnectorEventsSent'))
this.createGeneralChartsSubscription(gateway, connectorsTs); this.createGeneralChartsSubscription(gateway, connectorsTs);
}) });
} }
} }
} }
public sortData () { public sortData() {
this.dataSource.sortData(this.dataSource.data, this.sort); this.dataSource.sortData(this.dataSource.data, this.sort);
} }
@ -162,7 +145,7 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
} }
private createChartsSubscription(gateway: BaseData<EntityId>, attr: string) { private createChartsSubscription(gateway: BaseData<EntityId>, attr: string) {
let subscriptionInfo = [{ const subscriptionInfo = [{
type: DatasourceType.entity, type: DatasourceType.entity,
entityType: EntityType.DEVICE, entityType: EntityType.DEVICE,
entityId: gateway.id.id, entityId: gateway.id.id,
@ -173,10 +156,11 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
subscriptionInfo[0].timeseries = [{name: attr, label: attr}]; subscriptionInfo[0].timeseries = [{name: attr, label: attr}];
this.subscriptionInfo = subscriptionInfo; this.subscriptionInfo = subscriptionInfo;
this.changeSubscription(subscriptionInfo); this.changeSubscription(subscriptionInfo);
this.ctx.defaultSubscription.unsubscribe();
} }
private createGeneralChartsSubscription(gateway: BaseData<EntityId>, attrData: [string]) { private createGeneralChartsSubscription(gateway: BaseData<EntityId>, attrData: string[]) {
let subscriptionInfo = [{ const subscriptionInfo = [{
type: DatasourceType.entity, type: DatasourceType.entity,
entityType: EntityType.DEVICE, entityType: EntityType.DEVICE,
entityId: gateway.id.id, entityId: gateway.id.id,
@ -184,20 +168,20 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
timeseries: [] timeseries: []
}]; }];
subscriptionInfo[0].timeseries = []; subscriptionInfo[0].timeseries = [];
if (attrData && attrData.length) { if (attrData?.length) {
attrData.forEach(attr => { attrData.forEach(attr => {
subscriptionInfo[0].timeseries.push({name: attr, label: attr}) subscriptionInfo[0].timeseries.push({name: attr, label: attr});
}) });
} }
this.ctx.defaultSubscription.datasources[0].dataKeys.forEach(dataKey => { this.ctx.defaultSubscription.datasources[0].dataKeys.forEach(dataKey => {
subscriptionInfo[0].timeseries.push({name: dataKey.name, label: dataKey.label}) subscriptionInfo[0].timeseries.push({name: dataKey.name, label: dataKey.label});
}) });
this.subscriptionInfo = subscriptionInfo;
this.changeSubscription(subscriptionInfo); this.changeSubscription(subscriptionInfo);
this.ctx.defaultSubscription.unsubscribe();
} }
init = () => { private init = () => {
this.flotCtx = { this.flotCtx = {
$scope: this.ctx.$scope, $scope: this.ctx.$scope,
$injector: this.ctx.$injector, $injector: this.ctx.$injector,
@ -207,20 +191,20 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
subscriptionApi: this.ctx.subscriptionApi, subscriptionApi: this.ctx.subscriptionApi,
detectChanges: this.ctx.detectChanges, detectChanges: this.ctx.detectChanges,
settings: this.ctx.settings settings: this.ctx.settings
}; } as WidgetContext;
} };
updateChart = () => { private updateChart = () => {
if (this.flot && this.ctx.defaultSubscription.data.length) { if (this.flot && this.ctx.defaultSubscription.data.length) {
this.flot.update(); this.flot.update();
} }
} };
resize = () => { private resize = () => {
if (this.flot) { if (this.flot) {
this.flot.resize(); this.flot.resize();
} }
} };
private reset() { private reset() {
if (this.resize$) { if (this.resize$) {
@ -260,7 +244,7 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
this.chartInited = true; this.chartInited = true;
this.flotCtx.$container = $(this.statisticChart.nativeElement); this.flotCtx.$container = $(this.statisticChart.nativeElement);
this.resize$.observe(this.statisticChart.nativeElement); this.resize$.observe(this.statisticChart.nativeElement);
this.flot = new TbFlot(this.flotCtx as WidgetContext, "line"); this.flot = new TbFlot(this.flotCtx as WidgetContext, 'line');
this.flot.update(); this.flot.update();
} }
@ -270,16 +254,21 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
return; return;
} }
this.dataSource.data = this.subscription.data.length ? this.subscription.data[0].data : []; this.dataSource.data = this.subscription.data.length ? this.subscription.data[0].data : [];
this.isNumericData = this.dataSource.data.every(data => !isNaN(+data[1]) ); if (this.dataSource.data.length && !this.dataTypeDefined) {
this.dataTypeDefined = true;
this.isNumericData = this.dataSource.data.every(data => !isNaN(+data[1]));
}
} }
changeSubscription(subscriptionInfo: SubscriptionInfo[]) { private changeSubscription(subscriptionInfo: SubscriptionInfo[]) {
if (this.subscription) { if (this.subscription) {
this.reset(); this.reset();
} }
if (this.ctx.datasources[0].entity) { if (this.ctx.datasources[0].entity) {
this.ctx.subscriptionApi.createSubscriptionFromInfo(widgetType.timeseries, subscriptionInfo, this.subscriptionOptions, false, true).subscribe(subscription => { this.ctx.subscriptionApi.createSubscriptionFromInfo(widgetType.timeseries, subscriptionInfo, this.subscriptionOptions,
false, true).subscribe(subscription => {
this.dataTypeDefined = false;
this.subscription = subscription; this.subscription = subscription;
this.isDataOnlyNumbers(); this.isDataOnlyNumbers();
this.legendData = this.subscription.legendData; this.legendData = this.subscription.legendData;
@ -291,9 +280,7 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
if (this.isNumericData) { if (this.isNumericData) {
this.initChart(); this.initChart();
} }
}) });
} }
} }
} }

142
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts

@ -0,0 +1,142 @@
///
/// 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.
///
export enum StorageTypes {
MEMORY = 'memory',
FILE = 'file',
SQLITE = 'sqlite'
}
export enum DeviceGatewayStatus {
EXCEPTION = 'EXCEPTION'
}
export enum GatewayLogLevel {
NONE = 'NONE',
CRITICAL = 'CRITICAL',
ERROR = 'ERROR',
WARNING = 'WARNING',
INFO = 'INFO',
DEBUG = 'DEBUG'
}
export const GatewayStatus = {
...GatewayLogLevel,
...DeviceGatewayStatus
};
export type GatewayStatus = DeviceGatewayStatus | GatewayLogLevel;
export enum LogSavingPeriod {
days = 'D',
hours = 'H',
minutes = 'M',
seconds = 'S'
}
export enum LocalLogsConfigs {
service = 'service',
connector = 'connector',
converter = 'converter',
tb_connection = 'tb_connection',
storage = 'storage',
extension = 'extension'
}
export const LocalLogsConfigTranslateMap = new Map<LocalLogsConfigs, string>([
[LocalLogsConfigs.service, 'Service'],
[LocalLogsConfigs.connector, 'Connector'],
[LocalLogsConfigs.converter, 'Converter'],
[LocalLogsConfigs.tb_connection, 'TB Connection'],
[LocalLogsConfigs.storage, 'Storage'],
[LocalLogsConfigs.extension, 'Extension']
]);
export const LogSavingPeriodTranslations = new Map<LogSavingPeriod, string>(
[
[LogSavingPeriod.days, 'gateway.logs.days'],
[LogSavingPeriod.hours, 'gateway.logs.hours'],
[LogSavingPeriod.minutes, 'gateway.logs.minutes'],
[LogSavingPeriod.seconds, 'gateway.logs.seconds']
]
);
export const StorageTypesTranslationMap = new Map<StorageTypes, string>(
[
[StorageTypes.MEMORY, 'gateway.storage-types.memory-storage'],
[StorageTypes.FILE, 'gateway.storage-types.file-storage'],
[StorageTypes.SQLITE, 'gateway.storage-types.sqlite']
]
);
export enum SecurityTypes {
ACCESS_TOKEN = 'accessToken',
USERNAME_PASSWORD = 'usernamePassword',
TLS_ACCESS_TOKEN = 'tlsAccessToken',
TLS_PRIVATE_KEY = 'tlsPrivateKey'
}
export const GecurityTypesTranslationsMap = new Map<SecurityTypes, string>(
[
[SecurityTypes.ACCESS_TOKEN, 'gateway.security-types.access-token'],
[SecurityTypes.USERNAME_PASSWORD, 'gateway.security-types.username-password'],
[SecurityTypes.TLS_ACCESS_TOKEN, 'gateway.security-types.tls-access-token'],
// [SecurityTypes.TLS_PRIVATE_KEY, 'gateway.security-types.tls-private-key'],
]
);
export interface GatewayConnector {
name: string;
type: string;
configuration?: string;
configurationJson: string;
logLevel: string;
key?: string;
}
export const GatewayConnectorDefaultTypesTranslates = new Map<string, string>([
['mqtt', 'MQTT'],
['modbus', 'MODBUS'],
['grpc', 'GRPC'],
['opcua', 'OPCUA'],
['opcua_asyncio', 'OPCUA ASYNCIO'],
['ble', 'BLE'],
['request', 'REQUEST'],
['can', 'CAN'],
['bacnet', 'BACNET'],
['odbc', 'ODBC'],
['rest', 'REST'],
['snmp', 'SNMP'],
['ftp', 'FTP'],
['socket', 'SOCKET'],
['xmpp', 'XMPP'],
['ocpp', 'OCCP'],
['custom', 'CUSTOM']
]);
export interface LogLink {
name: string;
key: string;
filterFn?: (arg: any) => boolean;
}
export interface GatewayLogData {
ts: number;
key: string;
message: string;
status: GatewayStatus;
}

19
ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-logs-settings.component.html

@ -15,12 +15,13 @@
limitations under the License. limitations under the License.
--> -->
<section class="tb-widget-settings" [formGroup]="GatewayLogSettingForm" fxLayout="column"> <div class="tb-form-panel" [formGroup]="gatewayLogSettingForm">
<mat-slide-toggle class="mat-slide" formControlName="isConnectorLog"> <div class="tb-form-row column-xs">
{{"widgets.gateway.is-connector" | translate}} <mat-slide-toggle class="mat-slide fixed-title-width" formControlName="isConnectorLog">
</mat-slide-toggle> {{ "widgets.gateway.show-connector" | translate }}
<mat-form-field fxFlex class="mat-block" *ngIf="GatewayLogSettingForm.get('isConnectorLog').value"> </mat-slide-toggle>
<mat-label>{{"widgets.gateway.state-param-name" | translate}}</mat-label> <mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="connectorLogState"> <input matInput formControlName="connectorLogState" placeholder="{{ 'widgets.gateway.connector-state-param-key' | translate }}">
</mat-form-field> </mat-form-field>
</section> </div>
</div>

29
ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-logs-settings.component.ts

@ -16,7 +16,7 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -27,28 +27,41 @@ import { AppState } from '@core/core.state';
}) })
export class GatewayLogsSettingsComponent extends WidgetSettingsComponent { export class GatewayLogsSettingsComponent extends WidgetSettingsComponent {
GatewayLogSettingForm: UntypedFormGroup; gatewayLogSettingForm: FormGroup;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) { private fb: FormBuilder) {
super(store); super(store);
} }
protected settingsForm(): UntypedFormGroup { protected settingsForm(): FormGroup {
return this.GatewayLogSettingForm; return this.gatewayLogSettingForm;
} }
protected defaultSettings(): WidgetSettings { protected defaultSettings(): WidgetSettings {
return { return {
isConnectorLog: false, isConnectorLog: false,
connectorLogState: 'default' connectorLogState: ''
}; };
} }
protected onSettingsSet(settings: WidgetSettings) { protected onSettingsSet(settings: WidgetSettings) {
this.GatewayLogSettingForm = this.fb.group({ this.gatewayLogSettingForm = this.fb.group({
isConnectorLog: [false, []], isConnectorLog: [false, []],
connectorLogState: ['default', []] connectorLogState: ['', Validators.required]
}); });
} }
protected validatorTriggers(): string[] {
return ['isConnectorLog'];
}
protected updateValidators(emitEvent: boolean) {
const isConnectorLog: boolean = this.gatewayLogSettingForm.get('isConnectorLog').value;
if (isConnectorLog) {
this.gatewayLogSettingForm.get('connectorLogState').enable({emitEvent});
} else {
this.gatewayLogSettingForm.get('connectorLogState').disable({emitEvent});
}
}
} }

12
ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component.html

@ -15,8 +15,10 @@
limitations under the License. limitations under the License.
--> -->
<section class="tb-widget-settings" [formGroup]="GatewayLogSettingForm" fxLayout="column"> <div class="tb-form-panel" [formGroup]="gatewayServiceRPCSettingForm">
<mat-slide-toggle class="mat-slide" formControlName="isConnector"> <div class="tb-form-row column-xs">
{{"widgets.gateway.is-connector" | translate}} <mat-slide-toggle class="mat-slide margin" formControlName="isConnector">
</mat-slide-toggle> {{ "widgets.gateway.show-connector" | translate }}
</section> </mat-slide-toggle>
</div>
</div>

12
ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component.ts

@ -16,7 +16,7 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { FormBuilder, FormGroup } from '@angular/forms';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -27,15 +27,15 @@ import { AppState } from '@core/core.state';
}) })
export class GatewayServiceRPCSettingsComponent extends WidgetSettingsComponent { export class GatewayServiceRPCSettingsComponent extends WidgetSettingsComponent {
GatewayLogSettingForm: UntypedFormGroup; gatewayServiceRPCSettingForm: FormGroup;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) { private fb: FormBuilder) {
super(store); super(store);
} }
protected settingsForm(): UntypedFormGroup { protected settingsForm(): FormGroup {
return this.GatewayLogSettingForm; return this.gatewayServiceRPCSettingForm;
} }
protected defaultSettings(): WidgetSettings { protected defaultSettings(): WidgetSettings {
@ -45,7 +45,7 @@ export class GatewayServiceRPCSettingsComponent extends WidgetSettingsComponent
} }
protected onSettingsSet(settings: WidgetSettings) { protected onSettingsSet(settings: WidgetSettings) {
this.GatewayLogSettingForm = this.fb.group({ this.gatewayServiceRPCSettingForm = this.fb.group({
isConnector: [false, []] isConnector: [false, []]
}); });
} }

95
ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.html

@ -15,29 +15,74 @@
limitations under the License. limitations under the License.
--> -->
<div style="min-width: 800px;"> <mat-toolbar color="primary">
<mat-toolbar color="primary"> <h2 translate>{{ dialogTitle }}</h2>
<h2><mat-icon>info_outline</mat-icon> <span fxFlex></span>
{{ 'edge.install-connect-instructions' | translate }}</h2> <button mat-icon-button
<span fxFlex></span> (click)="close()"
<button mat-icon-button type="button">
(click)="cancel()" <mat-icon class="material-icons">close</mat-icon>
type="button"> </button>
<mat-icon class="material-icons">close</mat-icon> </mat-toolbar>
</button> <div mat-dialog-content>
</mat-toolbar> <section *ngIf="loadedInstructions; else loadingInstructions" class="tb-form-panel no-padding no-border">
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> <div class="tb-form-panel no-padding no-border">
</mat-progress-bar> <mat-tab-group [(selectedIndex)]="tabIndex" (selectedTabChange)="selectedTabChange(tabIndex)">
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> <mat-tab>
<div mat-dialog-content> <ng-template mat-tab-label>
<tb-markdown [data]="instructions" lineNumbers fallbackToPlainMarkdown></tb-markdown> <mat-icon class="tabs-icon" svgIcon="mdi:ubuntu"></mat-icon>
</div> Ubuntu
<div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center"> </ng-template>
<button mat-button color="primary" <ng-template matTabContent>
type="button" <div class="tb-form-panel no-padding no-border">
[disabled]="(isLoading$ | async)" <div class="tb-form-panel no-padding stroked">
(click)="cancel()" cdkFocusInitial> <tb-markdown [data]='contentData.ubuntu'></tb-markdown>
{{ 'action.close' | translate }} </div>
</button> </div>
</div> </ng-template>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<mat-icon class="tabs-icon" svgIcon="mdi:centos"></mat-icon>
CentOS/RHEL
</ng-template>
<ng-template matTabContent>
<div class="tb-form-panel no-padding no-border">
<div class="tb-form-panel no-padding stroked">
<tb-markdown [data]='contentData.centos'></tb-markdown>
</div>
</div>
</ng-template>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<mat-icon class="tabs-icon" svgIcon="docker"></mat-icon>
Docker
</ng-template>
<ng-template matTabContent>
<div class="tb-form-panel no-padding no-border">
<div class="tb-form-panel no-padding stroked">
<tb-markdown [data]='contentData.docker'></tb-markdown>
</div>
</div>
</ng-template>
</mat-tab>
</mat-tab-group>
</div>
</section>
</div> </div>
<div mat-dialog-actions class="tb-dialog-actions">
<mat-slide-toggle fxShow="{{ showDontShowAgain }}" [(ngModel)]="notShowAgain">{{ 'action.dont-show-again' | translate}}</mat-slide-toggle>
<span fxFlex></span>
<button mat-button
[disabled]="(isLoading$ | async)"
(click)="close()">{{ 'action.close' | translate }}</button>
</div>
<ng-template #loadingInstructions>
<div class="tb-loader">
<mat-spinner color="accent" diameter="65" strokeWidth="4"></mat-spinner>
<span class="mat-subtitle-1 label">
{{ 'edge.loading-edge-instructions' | translate }}
</span>
</div>
</ng-template>

123
ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.scss

@ -0,0 +1,123 @@
/**
* 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.
*/
@import "../../../../../scss/constants";
:host {
height: 100%;
max-height: 100vh;
display: grid;
grid-template-rows: min-content minmax(auto, 1fr) min-content;
.tb-loader {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
height: 300px;
max-height: 100%;
.label {
margin-bottom: 0;
text-align: center;
}
}
.tb-font-14 {
font-size: 14px;
}
.tb-flex-1 {
flex: 1;
}
@media #{$mat-sm} {
width: 705px;
}
@media #{$mat-gt-sm} {
width: 1080px;
}
}
:host-context(.mat-mdc-dialog-container) {
.tb-dialog-actions {
display: flex;
gap: 8px;
padding: 8px 16px;
}
.mat-mdc-dialog-content {
max-height: 80vh;
padding: 16px;
}
}
:host ::ng-deep {
.tb-markdown-view {
padding: 16px 16px 32px 16px;
div.code-wrapper button.clipboard-btn {
right: -2px !important;
p {
color: #305680;
}
p, div {
background-color: #F3F6FA;
}
div {
img {
display: none;
}
&:after {
content: "";
position: initial;
display: block;
width: 18px;
height: 18px;
background: #305680;
mask-image: url(/assets/copy-code-icon.svg);
mask-repeat: no-repeat;
}
}
&.multiline {
right: -2px !important;
}
}
& > *:not(ul) {
padding-right: unset !important;
padding-left: unset !important;
}
}
.mdc-button__label > span {
.mat-icon {
vertical-align: text-bottom;
box-sizing: initial;
}
}
.tabs-icon {
margin-right: 8px;
}
.tb-form-panel.tb-tab-body {
padding: 16px 0 0;
}
.mat-mdc-tab-body {
padding: 16px 0;
}
}

84
ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.ts

@ -14,33 +14,83 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject } from '@angular/core'; import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; import { DialogComponent } from '@shared/components/dialog.component';
import { DialogComponent } from "@shared/components/dialog.component"; import { Store } from '@ngrx/store';
import { Store } from "@ngrx/store"; import { AppState } from '@core/core.state';
import { AppState } from "@core/core.state"; import { Router } from '@angular/router';
import { Router } from "@angular/router"; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { ActionPreferencesPutUserSettings } from '@core/auth/auth.actions';
export interface EdgeInstructionsData { import { EdgeInfo, EdgeInstructionsMethod } from '@shared/models/edge.models';
instructions: string; import { EdgeService } from '@core/http/edge.service';
export interface EdgeInstructionsDialogData {
edge: EdgeInfo;
afterAdd: boolean;
} }
@Component({ @Component({
selector: 'tb-edge-instructions', selector: 'tb-edge-installation-dialog',
templateUrl: './edge-instructions-dialog.component.html' templateUrl: './edge-instructions-dialog.component.html',
styleUrls: ['./edge-instructions-dialog.component.scss']
}) })
export class EdgeInstructionsDialogComponent extends DialogComponent<EdgeInstructionsDialogComponent, EdgeInstructionsData> { export class EdgeInstructionsDialogComponent extends DialogComponent<EdgeInstructionsDialogComponent> implements OnInit, OnDestroy {
dialogTitle: string;
showDontShowAgain: boolean;
instructions: string = this.data.instructions; loadedInstructions = false;
notShowAgain = false;
tabIndex = 0;
instructionsMethod = EdgeInstructionsMethod;
contentData: any = {};
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
public dialogRef: MatDialogRef<EdgeInstructionsDialogComponent, EdgeInstructionsData>, @Inject(MAT_DIALOG_DATA) private data: EdgeInstructionsDialogData,
@Inject(MAT_DIALOG_DATA) public data: EdgeInstructionsData) { public dialogRef: MatDialogRef<EdgeInstructionsDialogComponent>,
private edgeService: EdgeService) {
super(store, router, dialogRef); super(store, router, dialogRef);
if (this.data.afterAdd) {
this.dialogTitle = 'edge.install-connect-instructions-edge-created';
this.showDontShowAgain = true;
} else {
this.dialogTitle = 'edge.install-connect-instructions';
this.showDontShowAgain = false;
}
}
ngOnInit() {
this.getInstructions(this.instructionsMethod[this.tabIndex]);
}
ngOnDestroy() {
super.ngOnDestroy();
}
close(): void {
if (this.notShowAgain && this.showDontShowAgain) {
this.store.dispatch(new ActionPreferencesPutUserSettings({notDisplayInstructionsAfterAddEdge: true}));
this.dialogRef.close(null);
} else {
this.dialogRef.close(null);
}
}
selectedTabChange(index: number) {
this.getInstructions(this.instructionsMethod[index]);
} }
cancel(): void { getInstructions(method: string) {
this.dialogRef.close(null); 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;
}
);
}
} }
} }

65
ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts

@ -29,10 +29,10 @@ import {
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
import { EntityAction } from '@home/models/entity/entity-component.models'; import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models';
import { forkJoin, Observable, of } from 'rxjs'; import { forkJoin, Observable, of } from 'rxjs';
import { select, Store } from '@ngrx/store'; import { select, Store } from '@ngrx/store';
import { selectAuthUser } from '@core/auth/auth.selectors'; import { selectAuthUser, selectUserSettingsProperty } from '@core/auth/auth.selectors';
import { map, mergeMap, take, tap } from 'rxjs/operators'; import { map, mergeMap, take, tap } from 'rxjs/operators';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { Authority } from '@app/shared/models/authority.enum'; import { Authority } from '@app/shared/models/authority.enum';
@ -51,7 +51,7 @@ import {
AddEntitiesToCustomerDialogData AddEntitiesToCustomerDialogData
} from '../../dialogs/add-entities-to-customer-dialog.component'; } from '../../dialogs/add-entities-to-customer-dialog.component';
import { HomeDialogsService } from '@home/dialogs/home-dialogs.service'; import { HomeDialogsService } from '@home/dialogs/home-dialogs.service';
import { Edge, EdgeInfo, EdgeInstallInstructions } from '@shared/models/edge.models'; import { Edge, EdgeInfo } from '@shared/models/edge.models';
import { EdgeService } from '@core/http/edge.service'; import { EdgeService } from '@core/http/edge.service';
import { EdgeComponent } from '@home/pages/edge/edge.component'; import { EdgeComponent } from '@home/pages/edge/edge.component';
import { EdgeTableHeaderComponent } from '@home/pages/edge/edge-table-header.component'; import { EdgeTableHeaderComponent } from '@home/pages/edge/edge-table-header.component';
@ -59,9 +59,10 @@ import { EdgeId } from '@shared/models/id/edge-id';
import { EdgeTabsComponent } from '@home/pages/edge/edge-tabs.component'; import { EdgeTabsComponent } from '@home/pages/edge/edge-tabs.component';
import { ActionNotificationShow } from '@core/notification/notification.actions'; import { ActionNotificationShow } from '@core/notification/notification.actions';
import { import {
EdgeInstructionsData, EdgeInstructionsDialogComponent,
EdgeInstructionsDialogComponent EdgeInstructionsDialogData
} from "@home/pages/edge/edge-instructions-dialog.component"; } from '@home/pages/edge/edge-instructions-dialog.component';
import { AddEntityDialogComponent } from '@home/components/entity/add-entity-dialog.component';
@Injectable() @Injectable()
export class EdgesTableConfigResolver implements Resolve<EntityTableConfig<EdgeInfo>> { export class EdgesTableConfigResolver implements Resolve<EntityTableConfig<EdgeInfo>> {
@ -140,6 +141,7 @@ export class EdgesTableConfigResolver implements Resolve<EntityTableConfig<EdgeI
this.config.addEnabled = this.config.componentsData.edgeScope !== 'customer_user'; this.config.addEnabled = this.config.componentsData.edgeScope !== 'customer_user';
this.config.entitiesDeleteEnabled = this.config.componentsData.edgeScope === 'tenant'; this.config.entitiesDeleteEnabled = this.config.componentsData.edgeScope === 'tenant';
this.config.deleteEnabled = () => this.config.componentsData.edgeScope === 'tenant'; this.config.deleteEnabled = () => this.config.componentsData.edgeScope === 'tenant';
this.config.addEntity = () => { this.addEdge(); return of(null); };
return this.config; return this.config;
}) })
); );
@ -530,21 +532,50 @@ export class EdgesTableConfigResolver implements Resolve<EntityTableConfig<EdgeI
); );
} }
openInstructions($event, edge) { addEdge() {
this.dialog.open<AddEntityDialogComponent, AddEntityDialogData<EdgeInfo>,
EdgeInfo>(AddEntityDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
entitiesTableConfig: this.config
}
}).afterClosed().subscribe(
(entity) => {
if (entity) {
this.store.pipe(select(selectUserSettingsProperty('notDisplayInstructionsAfterAddEdge'))).pipe(
take(1)
).subscribe((settings: boolean) => {
if (!settings) {
this.openInstructions(null, entity, true);
} else {
this.config.updateData();
this.config.entityAdded(entity);
}
});
}
}
);
}
openInstructions($event, edge: EdgeInfo, afterAdd = false) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
this.edgeService.getEdgeDockerInstallInstructions(edge.id.id).subscribe( this.dialog.open<EdgeInstructionsDialogComponent, EdgeInstructionsDialogData>
(edgeInstructionsTemplate: EdgeInstallInstructions) => { (EdgeInstructionsDialogComponent, {
this.dialog.open<EdgeInstructionsDialogComponent, EdgeInstructionsData>(EdgeInstructionsDialogComponent, { disableClose: true,
disableClose: false, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: {
data: { edge,
instructions: edgeInstructionsTemplate.dockerInstallInstructions afterAdd
}
});
} }
) }).afterClosed().subscribe(() => {
if (afterAdd) {
this.config.updateData();
}
}
);
} }
onEdgeAction(action: EntityAction<EdgeInfo>, config: EntityTableConfig<EdgeInfo>): boolean { onEdgeAction(action: EntityAction<EdgeInfo>, config: EntityTableConfig<EdgeInfo>): boolean {

9
ui-ngx/src/app/shared/components/file-input.component.html

@ -16,11 +16,10 @@
--> -->
<div class="tb-container"> <div class="tb-container">
<label class="tb-title" [ngClass]="{'tb-required': !disabled && required}">{{ label }} <label class="tb-title"
<mat-icon *ngIf="hint" class="material-icons-outlined pointer-event" [class.tb-required]="!disabled && required"
style="cursor:pointer;" [class.pointer-event]="hint"
matTooltip="{{hint}}">info_outlined tb-hint-tooltip-icon="{{ hint }}">{{ label }}
</mat-icon>
</label> </label>
<ng-container #flow="flow" <ng-container #flow="flow"
[flowConfig]="{allowDuplicateUploads: true}"> [flowConfig]="{allowDuplicateUploads: true}">

2
ui-ngx/src/app/shared/components/file-input.component.scss

@ -22,7 +22,7 @@ $previewSize: 100px !default;
.tb-container { .tb-container {
margin-top: 0; margin-top: 0;
label.tb-title { label.tb-title {
display: block; display: flex;
padding-bottom: 8px; padding-bottom: 8px;
} }
} }

5
ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html

@ -17,5 +17,6 @@
--> -->
<ng-content></ng-content> <ng-content></ng-content>
<tb-icon class="tb-hint-tooltip-icon tb-mat-18" <tb-icon class="tb-hint-tooltip-icon tb-mat-18"
matTooltip="{{ tooltipText }}" *ngIf="tooltipText"
[matTooltipPosition]="tooltipPosition">{{ hintIcon }}</tb-icon> matTooltip="{{ tooltipText }}"
[matTooltipPosition]="tooltipPosition">{{ hintIcon }}</tb-icon>

4
ui-ngx/src/app/shared/components/hint-tooltip-icon.component.scss

@ -13,12 +13,14 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
:host { :host(.tb-hint-tooltip) {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
}
:host {
.tb-hint-tooltip-icon { .tb-hint-tooltip-icon {
color: #E0E0E0; color: #E0E0E0;
overflow: visible; overflow: visible;

3
ui-ngx/src/app/shared/components/hint-tooltip-icon.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Input } from '@angular/core'; import { Component, HostBinding, Input } from '@angular/core';
import { TooltipPosition } from '@angular/material/tooltip'; import { TooltipPosition } from '@angular/material/tooltip';
@Component({ @Component({
@ -24,6 +24,7 @@ import { TooltipPosition } from '@angular/material/tooltip';
}) })
export class HintTooltipIconComponent { export class HintTooltipIconComponent {
@HostBinding('class.tb-hint-tooltip')
@Input('tb-hint-tooltip-icon') tooltipText: string; @Input('tb-hint-tooltip-icon') tooltipText: string;
@Input() @Input()

8
ui-ngx/src/app/shared/models/edge.models.ts

@ -179,5 +179,11 @@ export interface EdgeEvent extends BaseData<EventId> {
} }
export interface EdgeInstallInstructions { export interface EdgeInstallInstructions {
dockerInstallInstructions: string; installInstructions: string;
}
export enum EdgeInstructionsMethod {
ubuntu,
centos,
docker
} }

1
ui-ngx/src/app/shared/models/user-settings.models.ts

@ -17,6 +17,7 @@
export interface UserSettings { export interface UserSettings {
openedMenuSections?: string[]; openedMenuSections?: string[];
notDisplayConnectivityAfterAddDevice?: boolean; notDisplayConnectivityAfterAddDevice?: boolean;
notDisplayInstructionsAfterAddEdge?: boolean;
} }
export const initialUserSettings: UserSettings = { export const initialUserSettings: UserSettings = {

71
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1293,7 +1293,6 @@
"device-details": "Device details", "device-details": "Device details",
"add-device-text": "Add new device", "add-device-text": "Add new device",
"credentials": "Credentials", "credentials": "Credentials",
"commands": "Commands",
"manage-credentials": "Manage credentials", "manage-credentials": "Manage credentials",
"delete": "Delete device", "delete": "Delete device",
"assign-devices": "Assign devices", "assign-devices": "Assign devices",
@ -1395,11 +1394,6 @@
"copyId": "Copy device Id", "copyId": "Copy device Id",
"copyAccessToken": "Copy access token", "copyAccessToken": "Copy access token",
"copy-mqtt-authentication": "Copy MQTT credentials", "copy-mqtt-authentication": "Copy MQTT credentials",
"transport-command-copied-message": "Transport Command has been copied to clipboard",
"telemetry-commands-help-link": "In order to publish telemetry data to (<a href='{{helpLink}}' target='_blank'>ThingsBoard device</a>) with credentials of the current device you can use the following commands.",
"telemetry-command-setup-step": "1. Setup:",
"telemetry-command-send-step": "2. Send command:",
"telemetry-command-send-step-coap": "2. Send command: (based on CoAP cli)",
"idCopiedMessage": "Device Id has been copied to clipboard", "idCopiedMessage": "Device Id has been copied to clipboard",
"accessTokenCopiedMessage": "Device access token has been copied to clipboard", "accessTokenCopiedMessage": "Device access token has been copied to clipboard",
"mqtt-authentication-copied-message": "Device MQTT authentication has been copied to clipboard", "mqtt-authentication-copied-message": "Device MQTT authentication has been copied to clipboard",
@ -1964,6 +1958,8 @@
"make-private-edge-text": "After the confirmation the edge and all its data will be made private and won't be accessible by others.", "make-private-edge-text": "After the confirmation the edge and all its data will be made private and won't be accessible by others.",
"import": "Import edge", "import": "Import edge",
"install-connect-instructions": "Install & Connect Instructions", "install-connect-instructions": "Install & Connect Instructions",
"install-connect-instructions-edge-created": "Edge created! Check Install & Connect Instructions",
"loading-edge-instructions": "Loading edge instructions...",
"label": "Label", "label": "Label",
"load-entity-error": "Failed to load data. Entity has been deleted.", "load-entity-error": "Failed to load data. Entity has been deleted.",
"assign-new-edge": "Assign new edge", "assign-new-edge": "Assign new edge",
@ -2643,43 +2639,18 @@
"gateway": { "gateway": {
"add-entry": "Add configuration", "add-entry": "Add configuration",
"advanced": "Advanced", "advanced": "Advanced",
"checking-device-activity": "Checking Device Activity:", "checking-device-activity": "Checking device activity",
"command": "Docker commands", "command": "Docker commands",
"command-copied-message": "Docker command has been copied to clipboard", "command-copied-message": "Docker command has been copied to clipboard",
"configuration": "Configuration", "configuration": "Configuration",
"connector-json": "Connector JSON",
"connector-add": "Add new connector", "connector-add": "Add new connector",
"connector-enabled": "Enable connector", "connector-enabled": "Enable connector",
"connector-name": "Connector name", "connector-name": "Connector name",
"connector-name-required": "Connector name is required.", "connector-name-required": "Connector name is required.",
"connector-key": "Connector key",
"connector-key-required": "Connector key is required.",
"connector-configuration": "Configuration file name",
"connector-configuration-required": "Configuration file name is required.",
"connector-type": "Connector type", "connector-type": "Connector type",
"connector-type-required": "Connector type is required.", "connector-type-required": "Connector type is required.",
"connector-types": {
"mqtt": "MQTT Broker Connector",
"modbus": "Modbus Connector",
"modbus_serial": "Modbus Connector (serial)",
"opcua": "OPC-UA Connector",
"opcua_asyncio": "OPC-UA Connector (asyncio)",
"ble": "BLE Connector",
"request": "REQUEST Connector",
"can": "CAN Connector",
"bacnet": "BACnet Connector",
"odbc": "ODBC Connector",
"rest": "REST Connector",
"snmp": "SNMP Connector",
"ftp": "FTP Connector",
"socket": "Socket TCP/UDP Connector",
"xmpp": "XMPP Connector",
"ocpp": "OCPP Connector"
},
"connectors": "Connectors", "connectors": "Connectors",
"connectors-config": "Connectors configuration", "connectors-config": "Connectors configuration",
"connectors-active": "Connector active",
"connectors-inactive": "Connector inactive",
"connectors-table-enabled": "Enabled", "connectors-table-enabled": "Enabled",
"connectors-table-name": "Name", "connectors-table-name": "Name",
"connectors-table-type": "Type", "connectors-table-type": "Type",
@ -2690,10 +2661,8 @@
"rpc-command-send": "Send", "rpc-command-send": "Send",
"rpc-command-result": "Result", "rpc-command-result": "Result",
"rpc-command-edit-params": "Edit parameters", "rpc-command-edit-params": "Edit parameters",
"select-connector": "Select connector", "gateway-configuration": "General Configuration",
"gateway-configuration": "Gateway Configuration",
"docker-label": "In order to run ThingsBoard IoT gateway in docker with credentials for this device you can use the following commands.", "docker-label": "In order to run ThingsBoard IoT gateway in docker with credentials for this device you can use the following commands.",
"copy-command": "Copy docker command",
"create-new-gateway": "Create a new gateway", "create-new-gateway": "Create a new gateway",
"create-new-gateway-text": "Are you sure you want create a new gateway with name: '{{gatewayName}}'?", "create-new-gateway-text": "Are you sure you want create a new gateway with name: '{{gatewayName}}'?",
"created-time": "Created time", "created-time": "Created time",
@ -2710,7 +2679,6 @@
"gateway-name": "Gateway name", "gateway-name": "Gateway name",
"gateway-name-required": "Gateway name is required.", "gateway-name-required": "Gateway name is required.",
"gateway-saved": "Gateway configuration successfully saved.", "gateway-saved": "Gateway configuration successfully saved.",
"gateway-search": "Gateway search",
"grpc": "GRPC", "grpc": "GRPC",
"grpc-keep-alive-timeout": "Keep alive timeout (in ms)", "grpc-keep-alive-timeout": "Keep alive timeout (in ms)",
"grpc-keep-alive-timeout-required": "Keep alive timeout is required", "grpc-keep-alive-timeout-required": "Keep alive timeout is required",
@ -2732,7 +2700,6 @@
"grpc-max-pings-without-data-required": "Max pings without data is required", "grpc-max-pings-without-data-required": "Max pings without data is required",
"grpc-max-pings-without-data-min": "Max pings without data can not be less then 1", "grpc-max-pings-without-data-min": "Max pings without data can not be less then 1",
"grpc-max-pings-without-data-pattern": "Max pings without data is not valid", "grpc-max-pings-without-data-pattern": "Max pings without data is not valid",
"handle-device-renaming": "Handle device renaming",
"inactivity-check-period-seconds": "Inactivity check period (in sec)", "inactivity-check-period-seconds": "Inactivity check period (in sec)",
"inactivity-check-period-seconds-required": "Inactivity check period is required", "inactivity-check-period-seconds-required": "Inactivity check period is required",
"inactivity-check-period-seconds-min": "Inactivity check period can not be less then 1", "inactivity-check-period-seconds-min": "Inactivity check period can not be less then 1",
@ -2741,7 +2708,6 @@
"inactivity-timeout-seconds-min": "Inactivity timeout can not be less then 1", "inactivity-timeout-seconds-min": "Inactivity timeout can not be less then 1",
"json-parse": "Not valid JSON.", "json-parse": "Not valid JSON.",
"json-required": "Field cannot be empty.", "json-required": "Field cannot be empty.",
"linux-macos": "Linux/MacOS",
"logs": { "logs": {
"logs": "Logs", "logs": "Logs",
"days": "days", "days": "days",
@ -2792,7 +2758,6 @@
"tls-private-key": "TLS + Private Key" "tls-private-key": "TLS + Private Key"
}, },
"server-port": "Server port", "server-port": "Server port",
"stats-send-period-in-sec": "Stats send period in seconds",
"statistics": { "statistics": {
"statistic": "Statistic", "statistic": "Statistic",
"statistics": "Statistics", "statistics": "Statistics",
@ -2851,7 +2816,7 @@
"sqlite": "SQLITE" "sqlite": "SQLITE"
}, },
"thingsboard": "ThingsBoard", "thingsboard": "ThingsBoard",
"thingsboard-general": "General", "general": "General",
"thingsboard-host": "ThingsBoard host", "thingsboard-host": "ThingsBoard host",
"thingsboard-host-required": "Host is required.", "thingsboard-host-required": "Host is required.",
"thingsboard-port": "ThingsBoard port", "thingsboard-port": "ThingsBoard port",
@ -2879,7 +2844,6 @@
"toggle-fullscreen": "Toggle fullscreen", "toggle-fullscreen": "Toggle fullscreen",
"transformer-json-config": "Configuration JSON*", "transformer-json-config": "Configuration JSON*",
"update-config": "Add/update configuration JSON", "update-config": "Add/update configuration JSON",
"windows": "Windows",
"hints": { "hints": {
"remote-configuration": "Enables remote configuration and management of the gateway", "remote-configuration": "Enables remote configuration and management of the gateway",
"remote-shell": "Enables remote control of the operating system with the gateway from the Remote Shell widget", "remote-shell": "Enables remote control of the operating system with the gateway from the Remote Shell widget",
@ -2890,17 +2854,11 @@
"username": "MQTT username for the gateway form ThingsBoard server", "username": "MQTT username for the gateway form ThingsBoard server",
"password": "MQTT password for the gateway form ThingsBoard server", "password": "MQTT password for the gateway form ThingsBoard server",
"ca-cert": "Path to CA certificate file", "ca-cert": "Path to CA certificate file",
"cert": "Path to certificate file",
"private-key": "Path to private key file",
"date-form": "Date format in log message", "date-form": "Date format in log message",
"log-format": "Log message format", "log-format": "Log message format",
"remote-log": "Enables remote logging and logs reading from the gateway", "remote-log": "Enables remote logging and logs reading from the gateway",
"backup-count": "If backup count is > 0, when a rollover is done, no more than backup count files are kept - the oldest ones are deleted", "backup-count": "If backup count is > 0, when a rollover is done, no more than backup count files are kept - the oldest ones are deleted",
"storage": "Provides configuration for saving incoming data before it is sent to the ThingsBoard platform", "storage": "Provides configuration for saving incoming data before it is sent to the platform",
"file": "Received data saving to the hard drive",
"memory": "Received data saving to the RAM memory",
"sqlite": "Received data saving to the .db file",
"data-folder": "Path to folder, that will contains data (Relative or Absolute)",
"max-file-count": "Maximum count of file that will be created", "max-file-count": "Maximum count of file that will be created",
"max-read-count": "Count of messages to get from storage and send to ThingsBoard", "max-read-count": "Count of messages to get from storage and send to ThingsBoard",
"max-records": "Maximum count of records that will be stored in one file", "max-records": "Maximum count of records that will be stored in one file",
@ -3505,14 +3463,7 @@
"clientId-required": "Client ID is required", "clientId-required": "Client ID is required",
"username": "Username", "username": "Username",
"username-required": "Username is required", "username-required": "Username is required",
"password": "Password",
"password-required": "Password is required",
"ca-cert": "CA certificate", "ca-cert": "CA certificate",
"ca-cert-required": "CA certificate is required",
"cert": "Certificate",
"cert-required": "Certificate is required",
"private-key": "Private Key",
"private-key-required": "Private Key is required",
"2fa": { "2fa": {
"2fa": "Two-factor authentication", "2fa": "Two-factor authentication",
"2fa-description": "Two-factor authentication protects your account from unauthorized access. All you have to do is enter a security code when you log in.", "2fa-description": "Two-factor authentication protects your account from unauthorized access. All you have to do is enter a security code when you log in.",
@ -5343,8 +5294,11 @@
"events-title": "Gateway events form title", "events-title": "Gateway events form title",
"events-filter": "Events filter", "events-filter": "Events filter",
"event-key-contains": "Event key contains...", "event-key-contains": "Event key contains...",
"is-connector": "Is Connector", "show-connector": "Show for the connector",
"state-param-name": "State parameter connector name" "connector-state-param-key": "Connector state parameter key",
"status": "Status",
"message": "Message",
"created-time": "Created time"
}, },
"gauge": { "gauge": {
"default-color": "Default color", "default-color": "Default color",
@ -6402,8 +6356,7 @@
"node-selected": "On node selected", "node-selected": "On node selected",
"element-click": "On HTML element click", "element-click": "On HTML element click",
"pie-slice-click": "On slice click", "pie-slice-click": "On slice click",
"row-double-click": "On row double click", "row-double-click": "On row double click"
"action-button-click": "Action button click"
} }
}, },
"paginator" : { "paginator" : {

5
ui-ngx/src/form.scss

@ -65,6 +65,9 @@
box-shadow: none; box-shadow: none;
border-radius: 0; border-radius: 0;
} }
&.no-gap {
gap: 0;
}
&.tb-slide-toggle { &.tb-slide-toggle {
padding: 0; padding: 0;
gap: 0; gap: 0;
@ -218,7 +221,7 @@
} }
} }
.tb-form-row .mat-mdc-form-field, .mat-mdc-form-field.tb-inline-field { .tb-form-row:not(.tb-standard-fields) .mat-mdc-form-field:not(.tb-not-inline-field), .mat-mdc-form-field.tb-inline-field {
&.mat-form-field-appearance-fill { &.mat-form-field-appearance-fill {
.mdc-text-field--filled:not(.mdc-text-field--disabled) { .mdc-text-field--filled:not(.mdc-text-field--disabled) {
&:before { &:before {

Loading…
Cancel
Save