Browse Source

Fix conflicts

pull/9264/head
Igor Kulikov 3 years ago
parent
commit
ece8b804e6
  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. 69
      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>.
### 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.
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}
```
### Running ThingsBoard Edge as docker service
#### Running ThingsBoard Edge as docker service
${LOCALHOST_WARNING}
@ -64,12 +62,12 @@ services:
{: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.
Please update next lines of `docker-compose.yml` file:
```bash
```text
ports:
- "18080:8080"
- "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": [
"gateway_widgets.gateway_configuration",
"gateway_widgets.attributes_card",
"gateway_widgets.gateway_configuration2",
"gateway_widgets.gateway_general_configuration",
"gateway_widgets.config_form_latest",
"gateway_widgets.gateway_events",
"gateway_widgets.gateway_connector",
"gateway_widgets.gateway_connectors",
"gateway_widgets.gateway_logs",
"gateway_widgets.gateway_statistics",
"gateway_widgets.gateway_general_statistics",
"gateway_widgets.gateway_custom_statistics",
"gateway_widgets.gateway_general_chart_statistics",
"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,
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/instructions/{edgeId}", method = RequestMethod.GET)
@RequestMapping(value = "/edge/instructions/{edgeId}/{method}", method = RequestMethod.GET)
@ResponseBody
public EdgeInstallInstructions getEdgeDockerInstallInstructions(
@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("edgeId") String strEdgeId,
@ApiParam(value = "Installation method ('docker', 'ubuntu' or 'centos')")
@PathVariable("method") String installationMethod,
HttpServletRequest request) throws ThingsboardException {
if (isEdgesEnabled() && edgeInstallServiceOpt.isPresent()) {
EdgeId edgeId = new EdgeId(toUUID(strEdgeId));
edgeId = checkNotNull(edgeId);
Edge edge = checkEdgeId(edgeId, Operation.READ);
return checkNotNull(edgeInstallServiceOpt.get().getDockerInstallInstructions(getTenantId(), edge, request));
return checkNotNull(edgeInstallServiceOpt.get().getInstallInstructions(getTenantId(), edge, installationMethod, request));
} else {
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 java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@ -56,7 +55,20 @@ public class DefaultEdgeInstallService implements EdgeInstallService {
private String appVersion;
@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 baseUrl = request.getServerName();
if (baseUrl.contains("localhost") || baseUrl.contains("127.0.0.1")) {
@ -70,16 +82,40 @@ public class DefaultEdgeInstallService implements EdgeInstallService {
String edgeVersion = appVersion + "EDGE";
edgeVersion = edgeVersion.replace("-SNAPSHOT", "");
dockerInstallInstructions = dockerInstallInstructions.replace("${TB_EDGE_VERSION}", edgeVersion);
dockerInstallInstructions = dockerInstallInstructions.replace("${CLOUD_ROUTING_KEY}", edge.getRoutingKey());
dockerInstallInstructions = dockerInstallInstructions.replace("${CLOUD_ROUTING_SECRET}", edge.getSecret());
dockerInstallInstructions = dockerInstallInstructions.replace("${CLOUD_RPC_PORT}", Integer.toString(rpcPort));
dockerInstallInstructions = dockerInstallInstructions.replace("${CLOUD_RPC_SSL_ENABLED}", Boolean.toString(sslEnabled));
dockerInstallInstructions = replacePlaceholders(dockerInstallInstructions, edge);
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) {
try {
return new String(Files.readAllBytes(file), StandardCharsets.UTF_8);
return Files.readString(file);
} catch (IOException e) {
log.warn("Failed to read file: {}", file, 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 {
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())
.setConfiguration(JacksonUtil.OBJECT_MAPPER.writeValueAsString(node.getConfiguration()))
.setAdditionalInfo(JacksonUtil.OBJECT_MAPPER.writeValueAsString(node.getAdditionalInfo()))
.setSingletonMode(node.isSingletonMode())
.setConfigurationVersion(node.getConfigurationVersion())
.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) {

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 {
Edge edge = constructEdge(tenantId, "Edge for Test Docker Install Instructions", "default", "7390c3a6-69b0-9910-d155-b90aca4b772e", "l7q4zsjplzwhk16geqxy");
Edge savedEdge = doPost("/api/edge", edge, Edge.class);
String installInstructions = doGet("/api/edge/instructions/" + savedEdge.getId().getId().toString(), String.class);
String installInstructions = doGet("/api/edge/instructions/" + savedEdge.getId().getId().toString() + "/docker", String.class);
Assert.assertTrue(installInstructions.contains("l7q4zsjplzwhk16geqxy"));
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.RuleChainMetadataUpdateMsg;
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.UplinkMsg;
@ -46,6 +47,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@DaoSqlTest
public class RuleChainEdgeTest extends AbstractEdgeTest {
private static final int CONFIGURATION_VERSION = 5;
@Test
public void testRuleChains() throws Exception {
// create rule chain
@ -138,6 +141,10 @@ public class RuleChainEdgeTest extends AbstractEdgeTest {
RuleChainId receivedRuleChainId =
new RuleChainId(new UUID(ruleChainMetadataUpdateMsg.getRuleChainIdMSB(), ruleChainMetadataUpdateMsg.getRuleChainIdLSB()));
Assert.assertEquals(ruleChainId, receivedRuleChainId);
for (RuleNodeProto ruleNodeProto : ruleChainMetadataUpdateMsg.getNodesList()) {
Assert.assertEquals(CONFIGURATION_VERSION, ruleNodeProto.getConfigurationVersion());
}
}
private void createRuleChainMetadata(RuleChain ruleChain) {
@ -147,7 +154,7 @@ public class RuleChainEdgeTest extends AbstractEdgeTest {
RuleNode ruleNode1 = new RuleNode();
ruleNode1.setName("name1");
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();
configuration.setFetchTo(TbMsgSource.METADATA);
configuration.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
@ -156,13 +163,13 @@ public class RuleChainEdgeTest extends AbstractEdgeTest {
RuleNode ruleNode2 = new RuleNode();
ruleNode2.setName("name2");
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));
RuleNode ruleNode3 = new RuleNode();
ruleNode3.setName("name3");
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));
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.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.context.ApplicationEventPublisher;
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.msg.queue.ServiceType;
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.queue.discovery.event.PartitionChangeEvent;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@ -49,12 +52,17 @@ import java.util.Random;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
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.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@Slf4j
@ -78,15 +86,7 @@ public class HashPartitionServiceTest {
applicationEventPublisher = mock(ApplicationEventPublisher.class);
routingInfoService = mock(TenantRoutingInfoService.class);
queueRoutingInfoService = mock(QueueRoutingInfoService.class);
clusterRoutingService = new HashPartitionService(discoveryService,
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);
clusterRoutingService = createPartitionService();
ServiceInfo currentServer = ServiceInfo.newBuilder()
.setServiceId("tb-core-0")
.addAllServiceTypes(Collections.singletonList(ServiceType.TB_CORE.name()))
@ -101,8 +101,6 @@ public class HashPartitionServiceTest {
.build());
}
clusterRoutingService.init();
clusterRoutingService.partitionsInit();
clusterRoutingService.recalculatePartitions(currentServer, otherServers);
}
@ -194,25 +192,12 @@ public class HashPartitionServiceTest {
}
List<Queue> queues = new ArrayList<>();
Queue systemQueue = new Queue();
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);
queues.add(createQueue(TenantId.SYS_TENANT_ID, 10));
tenants.forEach((tenantId, profileId) -> {
Queue isolatedQueue = new Queue();
isolatedQueue.setTenantId(tenantId);
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));
queues.add(createQueue(tenantId, 2));
mockRoutingInfo(tenantId, profileId, true);
});
when(queueRoutingInfoService.getAllQueuesRoutingInfo()).thenReturn(queues.stream()
.map(QueueRoutingInfo::new).collect(Collectors.toList()));
mockQueues(queues);
List<ServiceInfo> ruleEngines = new ArrayList<>();
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
public void testIsManagedByCurrentServiceCheck() {
TenantProfileId isolatedProfileId = new TenantProfileId(UUID.randomUUID());
@ -282,9 +358,9 @@ public class HashPartitionServiceTest {
TenantProfileId regularProfileId = new TenantProfileId(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());
when(routingInfoService.getRoutingInfo(eq(regularTenantId))).thenReturn(new TenantRoutingInfo(regularTenantId, regularProfileId, false));
mockRoutingInfo(regularTenantId, regularProfileId, false);
assertThat(clusterRoutingService.isManagedByCurrentService(isolatedTenantId)).isTrue();
assertThat(clusterRoutingService.isManagedByCurrentService(regularTenantId)).isFalse();
@ -296,4 +372,43 @@ public class HashPartitionServiceTest {
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();
AlarmSeverity getSeverity();
long getStartTs();
long getEndTs();

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

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

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

@ -163,6 +163,8 @@ message RuleNodeProto {
bool debugMode = 5;
string configuration = 6;
string additionalInfo = 7;
bool singletonMode = 8;
int32 configurationVersion = 9;
}
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) {
TenantId tenantId = new TenantId(new UUID(queueDeleteMsg.getTenantIdMSB(), queueDeleteMsg.getTenantIdLSB()));
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueDeleteMsg.getQueueName(), tenantId);
myPartitions.remove(queueKey);
partitionTopicsMap.remove(queueKey);
partitionSizesMap.remove(queueKey);
//TODO: remove after merging tb entity services
@ -272,12 +271,23 @@ public class HashPartitionService implements PartitionService {
final ConcurrentMap<QueueKey, List<Integer>> oldPartitions = myPartitions;
myPartitions = newPartitions;
Set<QueueKey> removed = new HashSet<>();
oldPartitions.forEach((queueKey, partitions) -> {
if (!myPartitions.containsKey(queueKey)) {
log.info("[{}] NO MORE PARTITIONS FOR CURRENT KEY", queueKey);
applicationEventPublisher.publishEvent(new PartitionChangeEvent(this, queueKey, Collections.emptySet()));
if (!newPartitions.containsKey(queueKey)) {
removed.add(queueKey);
}
});
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) -> {
if (!partitions.equals(oldPartitions.get(queueKey))) {
@ -306,7 +316,11 @@ public class HashPartitionService implements PartitionService {
if (!changes.isEmpty()) {
applicationEventPublisher.publishEvent(new ClusterTopologyChangeEvent(this, changes));
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) {
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.AlarmApiCallResult;
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.device.profile.AlarmCondition;
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
public void testConstantKeyFilterSimple() throws Exception {
init();
@ -1605,9 +1694,9 @@ public class TbDeviceProfileNodeTest {
private void registerCreateAlarmMock(AlarmApiCallResult a, boolean created) {
when(a).thenAnswer(invocationOnMock -> {
// AlarmCreateOrUpdateActiveRequest request = invocationOnMock.getArgument(0);
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()
.successful(true)
.created(created)

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

@ -27,7 +27,8 @@ import {
DeviceCredentials,
DeviceInfo,
DeviceInfoQuery,
DeviceSearchQuery, PublishLaunchCommand,
DeviceSearchQuery,
PublishLaunchCommand,
PublishTelemetryCommand
} from '@app/shared/models/device.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));
}
public getEdgeDockerInstallInstructions(edgeId: string, config?: RequestConfig): Observable<EdgeInstallInstructions> {
return this.http.get<EdgeInstallInstructions>(`/api/edge/instructions/${edgeId}`, defaultHttpOptionsFromConfig(config));
public getEdgeInstallInstructions(edgeId: string, method: string = 'ubuntu', config?: RequestConfig): Observable<EdgeInstallInstructions> {
return this.http.get<EdgeInstallInstructions>(`/api/edge/instructions/${edgeId}/${method}`, defaultHttpOptionsFromConfig(config));
}
}

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

@ -812,3 +812,8 @@ export const getOS = (): string => {
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 { Observable } from 'rxjs';
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 { CommonModule } from '@angular/common';
import { mergeMap, tap } from 'rxjs/operators';
@ -28,7 +26,11 @@ import {
CustomDialogContainerData
} from '@home/components/widget/dialog/custom-dialog-container.component';
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()
export class CustomDialogService {
@ -36,12 +38,11 @@ export class CustomDialogService {
private customModules: Array<Type<any>>;
constructor(
private translate: TranslateService,
private authService: AuthService,
private dynamicComponentFactoryService: DynamicComponentFactoryService,
@Inject(SHARED_MODULE_TOKEN) private sharedModule: Type<any>,
@Inject(SHARED_HOME_COMPONENTS_MODULE_TOKEN) private sharedHomeComponentsModule: Type<any>,
@Inject(HOME_COMPONENTS_MODULE_TOKEN) private homeComponentsModule: Type<any>,
@Inject(WIDGET_COMPONENTS_MODULE_TOKEN) private widgetComponentsModule: Type<any>,
public dialog: MatDialog
) {
}
@ -52,7 +53,8 @@ export class CustomDialogService {
customDialog(template: string, controller: (instance: CustomDialogComponent) => void, data?: 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)) {
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.
-->
<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">
<span class="tb-no-data-text">{{ 'gateway.docker-label' | translate }}</span>
<div fxFlexAlign="end" class="tb-help" [tb-help]="helpLink"></div>
@ -29,14 +29,9 @@
Windows
</ng-template>
<ng-template matTabContent>
<div class="tb-form-panel no-padding no-border tb-tab-body">
<tb-markdown usePlainMarkdown containerClass="start-code" data="
```bash
{{windowsCode}}
{:copy-code}
```
"></tb-markdown>
</div>
<ng-container
*ngTemplateOutlet="commandsExample; context: {command: commands?.mqtt?.windows }">
</ng-container>
</ng-template>
</mat-tab>
<mat-tab>
@ -45,14 +40,9 @@
Linux
</ng-template>
<ng-template matTabContent>
<div class="tb-form-panel no-padding no-border tb-tab-body">
<tb-markdown usePlainMarkdown containerClass="start-code" data="
```bash
{{linuxCode}}
{:copy-code}
```
"></tb-markdown>
</div>
<ng-container
*ngTemplateOutlet="commandsExample; context: {command: commands?.mqtt?.linux }">
</ng-container>
</ng-template>
</mat-tab>
<mat-tab>
@ -61,15 +51,23 @@
MacOS
</ng-template>
<ng-template matTabContent>
<div class="tb-form-panel no-padding no-border tb-tab-body">
<tb-markdown usePlainMarkdown containerClass="start-code" data="
```bash
{{linuxCode}}
{:copy-code}
```
"></tb-markdown>
</div>
<ng-container
*ngTemplateOutlet="commandsExample; context: {command: commands?.mqtt?.linux }">
</ng-container>
</ng-template>
</mat-tab>
</mat-tab-group>
</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 {
.tb-markdown-view {
.start-code {
code[class*="language-"], pre[class*="language-"] {
overflow: hidden;
code[class*="language-"] {
white-space: break-spaces;
word-break: break-all;
}
pre[class*="language-"] {
overflow: hidden;
background: #F3F6FA;
border-color: #305680;
}
.code-wrapper {
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 { 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 { helpBaseUrl } from '@shared/models/constants';
import { getOS } from '@core/utils';
import { PublishLaunchCommand } from '@shared/models/device.models';
@Component({
selector: 'tb-gateway-command',
@ -39,17 +34,13 @@ export class DeviceGatewayCommandComponent implements OnInit {
@Input()
deviceId: string;
linuxCode: string;
windowsCode: string;
commands: PublishLaunchCommand;
helpLink: string = helpBaseUrl + '/docs/iot-gateway/install/docker-installation/';
tabIndex = 0;
constructor(protected router: Router,
protected store: Store<AppState>,
private translate: TranslateService,
private cd: ChangeDetectorRef,
constructor(private cd: ChangeDetectorRef,
private deviceService: DeviceService) {
}
@ -57,7 +48,7 @@ export class DeviceGatewayCommandComponent implements OnInit {
ngOnInit(): void {
if (this.deviceId) {
this.deviceService.getDevicePublishLaunchCommands(this.deviceId).subscribe(commands => {
this.createRunCode(commands.mqtt);
this.commands = commands;
this.cd.detectChanges();
});
}
@ -78,21 +69,4 @@ export class DeviceGatewayCommandComponent implements OnInit {
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 {
width: 100%;
height: 100%;
display: block;
display: grid;
grid-template-rows: min-content minmax(auto, 1fr) min-content;
.mat-icon {
color: rgba(0, 0, 0, .12);
}
.tb-form-panel {
margin-bottom: 20px;
.configuration-block {
display: flex;
flex-direction: column;
gap: 16px;
}
.mat-toolbar {
grid-row: 1;
background: transparent;
color: rgba(0, 0, 0, .87) !important;
}
.mat-content {
.expansion-panel-header {
font-weight: 600;
color: rgba(0, 0, 0, .87) !important;
}
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;
}
.tab-group-block {
min-width: 0;
height: 100%;
min-height: 0;
grid-row: 2;
}
.security-toggle-group {
margin-bottom: 15px;
.toggle-group {
margin-right: auto;
}
.logs-label {
font-weight: 500;
margin-bottom: 10px;
}
.statistics-block {
margin-bottom: 15px;
padding-left: 15px;
padding-top: 15px;
}
.first-capital {
text-transform: capitalize;
}
mat-panel-title {
display: block;
padding-top: 20px;
textarea {
resize: none;
}
mat-panel-title span {
display: block;
padding-left: 0;
padding-top: 5px;
.saving-period {
flex: 1;
}
.tb-hint {
font-size: 13px;
color: rgba(0, 0, 0, .54);
width: fit-content;
cursor: pointer;
text-transform: none !important;
}
.statistics-container {
width: 50%;
.line-break {
width: 100%;
.command-container {
width: 100%;
}
}
textarea {
resize: none;
.actions {
grid-row: 3;
padding: 8px;
display: flex;
gap: 8px;
justify-content: flex-end;
flex: 1;
}
}
: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-events: all;
}
.mat-mdc-form-field-icon-suffix {
z-index: 100;
.toggle-group span {
padding: 0 25px;
}
.security-toggle-group span {
padding: 0 25px;
.mat-mdc-form-field-icon-suffix {
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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import {
FormArray,
FormBuilder,
FormControl,
FormGroup,
UntypedFormGroup,
ValidationErrors,
ValidatorFn,
Validators
@ -41,79 +37,18 @@ import { Observable, of } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
import { DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models';
import { NULL_UUID } from '@shared/models/id/has-uuid';
export enum StorageTypes {
MEMORY = 'memory',
FILE = 'file',
SQLITE = 'sqlite'
}
export enum GatewayLogLevel {
none = 'NONE',
critical = 'CRITICAL',
error = 'ERROR',
warning = 'WARNING',
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'],
]
);
import {
GatewayLogLevel,
GecurityTypesTranslationsMap,
LocalLogsConfigTranslateMap,
LocalLogsConfigs,
LogSavingPeriod,
LogSavingPeriodTranslations,
SecurityTypes,
StorageTypes,
StorageTypesTranslationMap
} from './gateway-widget.models';
import { deepTrim } from '@core/utils';
@Component({
selector: 'tb-gateway-configuration',
@ -124,13 +59,16 @@ export class GatewayConfigurationComponent implements OnInit {
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);
@ -142,24 +80,19 @@ export class GatewayConfigurationComponent implements OnInit {
logSelector: FormControl;
securityType: SecurityTypes;
private initialCredentials: DeviceCredentials;
initialCredentials: DeviceCredentials;
constructor(protected router: Router,
protected store: Store<AppState>,
protected fb: FormBuilder,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
constructor(private fb: FormBuilder,
private attributeService: AttributeService,
private deviceService: DeviceService,
private cd: ChangeDetectorRef,
public dialog: MatDialog) {
private dialog: MatDialog) {
}
ngOnInit() {
this.gatewayConfigGroup = 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]+$/)]],
remoteShell: [false, []],
remoteConfiguration: [true, []],
@ -175,30 +108,30 @@ export class GatewayConfigurationComponent implements OnInit {
handleDeviceRenaming: [true, []],
checkingDeviceActivity: this.fb.group({
checkDeviceInactivity: [false, []],
inactivityTimeoutSeconds: [200, [Validators.min(1)]],
inactivityCheckPeriodSeconds: [500, [Validators.min(1)]]
inactivityTimeoutSeconds: [200, [Validators.min(1), Validators.pattern(/^[^.\s]+$/)]],
inactivityCheckPeriodSeconds: [500, [Validators.min(1), Validators.pattern(/^[^.\s]+$/)]]
}),
security: this.fb.group({
type: [SecurityTypes.ACCESS_TOKEN, [Validators.required]],
accessToken: [null, [Validators.required]],
clientId: [null, []],
username: [null, []],
password: [null, []],
accessToken: [null, [Validators.required, Validators.pattern(/^[^.\s]+$/)]],
clientId: [null, [Validators.pattern(/^[^.\s]+$/)]],
username: [null, [Validators.pattern(/^[^.\s]+$/)]],
password: [null, [Validators.pattern(/^[^.\s]+$/)]],
caCert: [null, []],
cert: [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({
type: [StorageTypes.MEMORY, [Validators.required]],
read_records_count: [100, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]],
max_records_count: [100000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]],
data_folder_path: ['./data/', []],
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, Validators.pattern(/^[^.\s]+$/)]],
data_folder_path: ['./data/', [Validators.pattern(/^[^\s]+$/)]],
max_file_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]+$/)]],
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_in_days: [7, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
@ -215,13 +148,13 @@ export class GatewayConfigurationComponent implements OnInit {
}),
connectors: this.fb.array([]),
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',
[Validators.required]],
[Validators.required, Validators.pattern(/^[^\s].*[^\s]$/)]],
type: ['remote', [Validators.required]],
remote: this.fb.group({
enabled: [false],
logLevel: [GatewayLogLevel.info, [Validators.required]],
logLevel: [GatewayLogLevel.INFO, [Validators.required]],
}),
local: this.fb.group({})
})
@ -231,7 +164,7 @@ export class GatewayConfigurationComponent implements OnInit {
if (password && password !== '') {
this.gatewayConfigGroup.get('thingsboard.security.username').setValidators([Validators.required]);
} else {
this.gatewayConfigGroup.get('thingsboard.security.username').setValidators([]);
this.gatewayConfigGroup.get('thingsboard.security.username').clearValidators();
}
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]);
} else {
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').clearValidators();
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').setErrors(null);
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 => {
@ -272,7 +205,7 @@ export class GatewayConfigurationComponent implements OnInit {
securityGroup.get('type').valueChanges.subscribe(type => {
this.removeAllSecurityValidators();
if (type === SecurityTypes.ACCESS_TOKEN) {
securityGroup.get('accessToken').addValidators([Validators.required]);
securityGroup.get('accessToken').addValidators([Validators.required, Validators.pattern(/^[^.\s]+$/)]);
securityGroup.get('accessToken').updateValueAndValidity();
} else if (type === SecurityTypes.TLS_PRIVATE_KEY) {
securityGroup.get('caCert').addValidators([Validators.required]);
@ -282,21 +215,19 @@ export class GatewayConfigurationComponent implements OnInit {
securityGroup.get('cert').addValidators([Validators.required]);
securityGroup.get('cert').updateValueAndValidity();
} 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('caCert').addValidators([Validators.required]);
securityGroup.get('caCert').updateValueAndValidity();
} else if (type === SecurityTypes.USERNAME_PASSWORD) {
securityGroup.addValidators([this.atLeastOneRequired(Validators.required, ['clientId', 'username'])])
// securityGroup.get('password').addValidators([Validators.required]);
// securityGroup.get('password').updateValueAndValidity();
securityGroup.addValidators([this.atLeastOneRequired(Validators.required, ['clientId', 'username'])]);
}
securityGroup.updateValueAndValidity();
});
securityGroup.get('caCert').valueChanges.subscribe(_ => this.cd.detectChanges());
securityGroup.get('privateKey').valueChanges.subscribe(_ => this.cd.detectChanges());
securityGroup.get('cert').valueChanges.subscribe(_ => this.cd.detectChanges());
securityGroup.get('caCert').valueChanges.subscribe(() => this.cd.detectChanges());
securityGroup.get('privateKey').valueChanges.subscribe(() => this.cd.detectChanges());
securityGroup.get('cert').valueChanges.subscribe(() => this.cd.detectChanges());
const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup;
storageGroup.get('type').valueChanges.subscribe(type => {
@ -306,28 +237,37 @@ export class GatewayConfigurationComponent implements OnInit {
[Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]);
storageGroup.get('max_records_count').addValidators(
[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) {
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(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('max_read_records_count').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('max_records_per_file').addValidators(
[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) {
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(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('messages_ttl_in_days').addValidators(
[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);
}
atLeastOneRequired(validator: ValidatorFn, controls: string[] = null) {
return (group: UntypedFormGroup): ValidationErrors | null => {
private atLeastOneRequired(validator: ValidatorFn, controls: string[] = null) {
return (group: FormGroup): ValidationErrors | null => {
if (!controls) {
controls = Object.keys(group.controls);
}
@ -337,22 +277,10 @@ export class GatewayConfigurationComponent implements OnInit {
};
}
updateSecurityValidators(value: SecurityTypes) {
this.gatewayConfigGroup.get('thingsboard.security.type').setValue(value, {emitEvent: true});
this.gatewayConfigGroup.get('thingsboard.security.type').markAsDirty();
}
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;
private fetchConfigAttribute(entityId: EntityId) {
if (entityId.id === NULL_UUID) {
return;
}
this.attributeService.getEntityAttributes(entityId, AttributeScope.CLIENT_SCOPE,
['general_configuration', 'grpc_configuration', 'logs_configuration', 'storage_configuration', 'RemoteLoggingLevel']).pipe(
mergeMap(attributes => attributes.length ? of(attributes) : this.attributeService.getEntityAttributes(
@ -397,24 +325,26 @@ export class GatewayConfigurationComponent implements OnInit {
if (remoteLoggingLevel) {
const remoteLogsFormGroup = this.gatewayConfigGroup.get('logs.remote');
remoteLogsFormGroup.patchValue({
enabled: remoteLoggingLevel !== GatewayLogLevel.none,
enabled: remoteLoggingLevel !== GatewayLogLevel.NONE,
logLevel: remoteLoggingLevel
}, {emitEvent: false});
remoteLogsFormGroup.markAsPristine();
}
this.cd.detectChanges();
} else {
this.checkAndFetchCredentials({});
this.checkAndFetchCredentials();
}
});
}
checkAndFetchCredentials(security): void {
private checkAndFetchCredentials(security: any = {}): void {
if (security.type !== SecurityTypes.TLS_PRIVATE_KEY) {
this.deviceService.getDeviceCredentials(this.device.id).subscribe(credentials => {
this.initialCredentials = credentials;
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);
if(security.type === SecurityTypes.TLS_ACCESS_TOKEN) {
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 = {
local: {}
};
@ -453,43 +383,42 @@ export class GatewayConfigurationComponent implements OnInit {
return {local: logsObject, logFormat, dateFormat};
}
toggleRpcFields(enable: boolean) {
private toggleRpcFields(enable: boolean) {
const grpcGroup = this.gatewayConfigGroup.get('grpc') as FormGroup;
if (enable) {
grpcGroup.get('serverPort').enable();
grpcGroup.get('keepAliveTimeMs').enable();
grpcGroup.get('keepAliveTimeoutMs').enable();
grpcGroup.get('keepalivePermitWithoutCalls').enable();
grpcGroup.get('maxPingsWithoutData').enable();
grpcGroup.get('minTimeBetweenPingsMs').enable();
grpcGroup.get('minPingIntervalWithoutDataMs').enable();
grpcGroup.get('serverPort').enable({emitEvent: false});
grpcGroup.get('keepAliveTimeMs').enable({emitEvent: false});
grpcGroup.get('keepAliveTimeoutMs').enable({emitEvent: false});
grpcGroup.get('keepalivePermitWithoutCalls').enable({emitEvent: false});
grpcGroup.get('maxPingsWithoutData').enable({emitEvent: false});
grpcGroup.get('minTimeBetweenPingsMs').enable({emitEvent: false});
grpcGroup.get('minPingIntervalWithoutDataMs').enable({emitEvent: false});
} else {
grpcGroup.get('serverPort').disable();
grpcGroup.get('keepAliveTimeMs').disable();
grpcGroup.get('keepAliveTimeoutMs').disable();
grpcGroup.get('keepalivePermitWithoutCalls').disable();
grpcGroup.get('maxPingsWithoutData').disable();
grpcGroup.get('minTimeBetweenPingsMs').disable();
grpcGroup.get('minPingIntervalWithoutDataMs').disable();
grpcGroup.get('serverPort').disable({emitEvent: false});
grpcGroup.get('keepAliveTimeMs').disable({emitEvent: false});
grpcGroup.get('keepAliveTimeoutMs').disable({emitEvent: false});
grpcGroup.get('keepalivePermitWithoutCalls').disable({emitEvent: false});
grpcGroup.get('maxPingsWithoutData').disable({emitEvent: false});
grpcGroup.get('minTimeBetweenPingsMs').disable({emitEvent: false});
grpcGroup.get('minPingIntervalWithoutDataMs').disable({emitEvent: false});
}
}
addCommand(command?): void {
const data = command || {};
addCommand(command: any = {}): void {
const commandsFormArray = this.commandFormArray();
const commandFormGroup = this.fb.group({
attributeOnGateway: [data.attributeOnGateway || null, [Validators.required]],
command: [data.command || null, [Validators.required]],
timeout: [data.timeout || null, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
attributeOnGateway: [command.attributeOnGateway || null, [Validators.required, Validators.pattern(/^[^.\s]+$/)]],
command: [command.command || null, [Validators.required, Validators.pattern(/^[^.\s]+$/)]],
timeout: [command.timeout || null, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.pattern(/^[^.\s]+$/)]],
});
commandsFormArray.push(commandFormGroup);
}
addLocalLogConfig(name, config): void {
private addLocalLogConfig(name: string, config: any): void {
const localLogsFormGroup = this.gatewayConfigGroup.get('logs.local') as FormGroup;
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]],
backupCount: [config.backupCount || 7, [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 {
if (event.pointerType === '') return;
if (event.pointerType === '') {
return;
}
this.commandFormArray().removeAt(index);
this.gatewayConfigGroup.markAsDirty();
}
removeAllSecurityValidators(): void {
private removeAllSecurityValidators(): void {
const securityGroup = this.gatewayConfigGroup.get('thingsboard.security') as FormGroup;
securityGroup.clearValidators();
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;
for (const storageKey in storageGroup.controls) {
if (storageKey !== 'type') {
@ -535,7 +466,7 @@ export class GatewayConfigurationComponent implements OnInit {
}
}
removeEmpty(obj) {
private removeEmpty(obj: any) {
return Object.fromEntries(
Object.entries(obj)
.filter(([_, v]) => v != null)
@ -543,7 +474,7 @@ export class GatewayConfigurationComponent implements OnInit {
);
}
generateLogsFile(logsObj) {
private generateLogsFile(logsObj: any) {
const logAttrObj = {
version: 1,
disable_existing_loggers: false,
@ -591,7 +522,7 @@ export class GatewayConfigurationComponent implements OnInit {
return logAttrObj;
}
createHandlerObj(logObj, key) {
private createHandlerObj(logObj: any, key: string) {
return {
class: 'thingsboard_gateway.tb_utility.tb_handler.TimedRotatingFileHandler',
formatter: 'LogFormatter',
@ -603,7 +534,7 @@ export class GatewayConfigurationComponent implements OnInit {
};
}
createLoggerObj(logObj, key) {
private createLoggerObj(logObj: any, key: string) {
return {
handlers: [`${key}Handler`, 'consoleHandler'],
level: logObj.logLevel,
@ -612,12 +543,12 @@ export class GatewayConfigurationComponent implements OnInit {
}
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);
const attributes = [];
attributes.push({
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;
attributes.push({
@ -642,18 +573,18 @@ export class GatewayConfigurationComponent implements OnInit {
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) {
this.dialogRef.close();
} else {
this.gatewayConfigGroup.markAsPristine();
this.cd.detectChanges();
}
})
});
});
}
updateCredentials(securityConfig): Observable<any> {
private updateCredentials(securityConfig: any): Observable<any> {
let updateCredentials = false;
let newCredentials = {};
if (securityConfig.type === SecurityTypes.USERNAME_PASSWORD) {
@ -661,17 +592,26 @@ export class GatewayConfigurationComponent implements OnInit {
updateCredentials = true;
} else {
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) {
let credentialsValue: { clientId?, userName?, password? } = {};
const credentialsValue: { clientId?: string; userName?: string; password?: string } = {};
const credentialsType = DeviceCredentialsType.MQTT_BASIC;
if (securityConfig.clientId) credentialsValue.clientId = securityConfig.clientId;
if (securityConfig.username) credentialsValue.userName = securityConfig.username;
if (securityConfig.password) credentialsValue.password = securityConfig.password;
if (securityConfig.clientId) {
credentialsValue.clientId = securityConfig.clientId;
}
if (securityConfig.username) {
credentialsValue.userName = securityConfig.username;
}
if (securityConfig.password) {
credentialsValue.password = securityConfig.password;
}
newCredentials = {
credentialsType,
credentialsValue: JSON.stringify(credentialsValue)
credentialsType,
credentialsValue: JSON.stringify(credentialsValue)
};
}
} else if (securityConfig.type === SecurityTypes.ACCESS_TOKEN || securityConfig.type === SecurityTypes.TLS_ACCESS_TOKEN) {
@ -684,12 +624,12 @@ export class GatewayConfigurationComponent implements OnInit {
newCredentials = {
credentialsType: DeviceCredentialsType.ACCESS_TOKEN,
credentialsId: securityConfig.accessToken
}
};
}
}
if (updateCredentials) {
return this.deviceService.saveDeviceCredentials({...this.initialCredentials,...newCredentials})
return this.deviceService.saveDeviceCredentials({...this.initialCredentials,...newCredentials});
}
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.
-->
<form style="height: calc(100% - 7px)" fxLayout="column">
<div fxLayout="row" fxLayout.lt-lg="column" fxLayoutGap="15px" fxFlex class="connector-container">
<mat-card fxLayout="column" fxFlex.lt-lg style="overflow: auto; min-height: 35vh">
<mat-toolbar class="mat-mdc-table-toolbar">
<h2>{{ 'gateway.connectors' | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
[disabled]="isLoading$ | async"
(click)="addAttribute()"
matTooltip="{{ 'action.add' | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
</button>
</mat-toolbar>
<div class="connector-container tb-form-panel no-border">
<section class="table-section tb-form-panel no-padding flex section-container">
<mat-toolbar class="mat-mdc-table-toolbar">
<h2>{{ 'gateway.connectors' | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
[disabled]="isLoading$ | async"
(click)="addAttribute()"
matTooltip="{{ 'action.add' | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
</button>
</mat-toolbar>
<div class="table-container">
<table mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()"
matSortDisableClear>
<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 }}
</mat-header-cell>
<mat-cell *matCellDef="let attribute">
@ -61,12 +61,9 @@
{{ 'gateway.configuration' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let attribute" style="text-transform: uppercase">
<div
[ngClass]="{
'status-block': true,
'status-sync':isConnectorSynced(attribute),
'status-unsync':!isConnectorSynced(attribute)
}">{{isConnectorSynced(attribute)?'sync' : 'out of sync'}}</div>
<div class="status" [class]="isConnectorSynced(attribute) ? 'status-sync' : 'status-unsync'">
{{ isConnectorSynced(attribute) ? 'sync' : 'out of sync' }}
</div>
</mat-cell>
</ng-container>
<ng-container matColumnDef="errors">
@ -77,10 +74,8 @@
<span class="dot"
matTooltip="{{ 'Errors: '+ getErrorsCount(attribute)}}"
matTooltipPosition="above"
[ngClass]="{
'hasErrors': +getErrorsCount(attribute) > 0,
'noErrors': +getErrorsCount(attribute) == 0 || getErrorsCount(attribute) == ''
}"></span>
[class]="{'hasErrors': +getErrorsCount(attribute) > 0,
'noErrors': +getErrorsCount(attribute) === 0 || getErrorsCount(attribute) === ''}"></span>
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
@ -139,60 +134,60 @@
</div>
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}"
<mat-header-row class="mat-row-select"
*matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row [ngClass]="{'mat-row-select': true,
'tb-current-entity': isSameConnector(attribute)}"
<mat-row class="mat-row-select" [class]="{'tb-current-entity': isSameConnector(attribute)}"
*matRowDef="let attribute; columns: displayedColumns;" (click)="selectConnector(attribute)"></mat-row>
</table>
<mat-divider></mat-divider>
</mat-card>
<div [formGroup]="connectorForm" fxLayout="column">
<mat-card fxLayout="row" fxLayout.lt-lg="column" fxLayoutGap="15px" fxLayoutGap.lt-lg="5px">
<mat-form-field class="mat-block tb-value-type">
</div>
</section>
<section [formGroup]="connectorForm" class="tb-form-panel no-border no-padding flex">
<section class="tb-form-panel input-container section-container">
<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>
<input matInput formControlName="name" #nameInput/>
</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-select formControlName="type">
<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-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>
<input matInput formControlName="key"/>
</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>
<input matInput formControlName="class"/>
</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-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-form-field>
</mat-card>
<mat-card fxLayout="column" fxFlex>
<tb-json-object-edit
fxFlex
fxLayout="column"
jsonRequired
label="{{ 'gateway.configuration' | translate }}"
formControlName="configurationJson">
</tb-json-object-edit>
<div fxLayoutAlign="start center">
<button mat-raised-button color="primary"
class="action-btns"
type="button"
[disabled]="!connectorForm.dirty || connectorForm.invalid"
(click)="saveConnector()">
{{ 'action.save' | translate }}
</button>
</div>
</mat-card>
</div>
</div>
</form>
</section>
</section>
<section class="tb-form-panel flex section-container">
<tb-json-object-edit
fillHeight="true"
class="flex"
fxLayout="column"
jsonRequired
label="{{ 'gateway.configuration' | translate }}"
formControlName="configurationJson">
</tb-json-object-edit>
<div fxLayoutAlign="end center">
<button mat-raised-button color="primary"
type="button"
[disabled]="!connectorForm.dirty || connectorForm.invalid"
(click)="saveConnector()">
{{ 'action.save' | translate }}
</button>
</div>
</section>
</section>
</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
* limitations under the License.
*/
@import '../../../../../../../scss/constants';
:host {
width: 100%;
height: 100%;
@ -21,25 +23,32 @@
padding: 0;
.connector-container {
height: 100%;
width: 100%;
flex-direction: row;
@media #{$mat-lt-lg} {
flex-direction: column;
}
.table-section {
min-height: 35vh;
overflow: hidden;
& > mat-card, & > div {
min-width: calc(50% - 15px);
.table-container {
overflow: auto;
}
}
mat-card {
margin: 10px;
padding: 10px;
max-width: 100%;
.flex {
flex: 1;
}
}
.tb-entity-table {
.tb-entity-table-content {
width: 100%;
height: 100%;
background: #fff;
overflow: hidden;
.input-container {
height: auto;
}
.section-container {
background-color: #fff;
}
}
@ -48,43 +57,24 @@
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 {
margin: 15px;
margin: 0 8px;
}
.status-block {
.status {
text-align: center;
border-radius: 16px;
font-weight: 500;
width: fit-content;
padding: 5px 15px;
}
.status-sync {
background: rgba(25, 128, 56, .06);
color: rgb(25, 128, 56);
}
.status-unsync {
background: rgba(203, 37, 48, .06);
color: rgb(203, 37, 48);
}
.action-btns {
margin: 10px 10px 0;
&-sync {
background: rgba(25, 128, 56, .06);
color: rgb(25, 128, 56);
}
&-unsync {
background: rgba(203, 37, 48, .06);
color: rgb(203, 37, 48);
}
}
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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup, UntypedFormControl, ValidatorFn, Validators } from '@angular/forms';
import { EntityId } from '@shared/models/id/entity-id';
import { MatDialog } 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 { forkJoin } from 'rxjs';
import { forkJoin, Observable } from 'rxjs';
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { PageComponent } from '@shared/components/page.component';
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 { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
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 { DialogService } from '@core/services/dialog.service';
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 { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { DatasourceType, widgetType } from '@shared/models/widget.models';
import { UtilsService } from '@core/services/utils.service';
import { EntityType } from '@shared/models/entity-type.models';
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']
]);
import {
GatewayConnector,
GatewayConnectorDefaultTypesTranslates,
GatewayLogLevel
} from './gateway-widget.models';
@Component({
selector: 'tb-gateway-connector',
@ -84,12 +54,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
pageLink: PageLink;
attributeDataSource: AttributeDatasource;
inactiveConnectorsDataSource: AttributeDatasource;
serverDataSource: AttributeDatasource;
dataSource: MatTableDataSource<AttributeData>;
displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions'];
@ -107,27 +71,29 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
connectorForm: FormGroup;
viewsInited = false;
textSearchMode: boolean;
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: {
onDataUpdated: () => this.ctx.ngZone.run(() => {
this.onDataUpdated();
@ -138,20 +104,17 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}
};
subscription: IWidgetSubscription;
private subscription: IWidgetSubscription;
constructor(protected router: Router,
protected store: Store<AppState>,
protected fb: FormBuilder,
protected translate: TranslateService,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
protected dialogService: DialogService,
constructor(protected store: Store<AppState>,
private fb: FormBuilder,
private translate: TranslateService,
private attributeService: AttributeService,
private dialogService: DialogService,
private telemetryWsService: TelemetryWebsocketService,
private zone: NgZone,
private utils: UtilsService,
private cd: ChangeDetectorRef,
public dialog: MatDialog) {
private cd: ChangeDetectorRef) {
super(store);
const sortOrder: SortOrder = {property: 'key', direction: Direction.ASC};
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];
};
this.viewsInited = true;
if (this.device) {
if (this.device.id === NULL_UUID) return;
forkJoin(this.attributeService.getEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, ['active_connectors']),
this.attributeService.getEntityAttributes(this.device, AttributeScope.SERVER_SCOPE, ['inactive_connectors'])).subscribe(attributes => {
if (this.device.id === NULL_UUID) {
return;
}
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) {
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 = 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);
} else {
this.activeConnectors = [];
@ -206,7 +174,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}
}
uniqNameRequired(): ValidatorFn {
private uniqNameRequired(): ValidatorFn {
return (c: UntypedFormControl) => {
const newName = c.value.trim().toLowerCase();
const found = this.dataSource.data.find((connectorAttr) => {
@ -229,7 +197,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
saveConnector(): void {
const value = this.connectorForm.value;
value.configuration = this.camelize(value.name) + '.json';
value.configuration = camelCase(value.name) + '.json';
if (value.type !== 'grpc') {
delete value.key;
}
@ -242,7 +210,9 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
value
}];
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;
if (this.initialConnector && this.initialConnector.name !== value.name) {
attributesToDelete.push({key: this.initialConnector.name});
@ -256,19 +226,19 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
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);
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);
updateActiveConnectors = true;
}
const tasks = [this.attributeService.saveEntityAttributes(this.device, scope, attributesToSave)];
if (updateActiveConnectors) {
tasks.push(this.attributeService.saveEntityAttributes(this.device, scope, [{
key: scope == AttributeScope.SHARED_SCOPE ? 'active_connectors' : 'inactive_connectors',
value: scope == AttributeScope.SHARED_SCOPE ? this.activeConnectors : this.inactiveConnectors
key: scope === AttributeScope.SHARED_SCOPE ? 'active_connectors' : 'inactive_connectors',
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.direction = Direction[this.sort.direction.toUpperCase()];
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) {
const connectorData = attribute.value;
if (!connectorData.ts) return false;
const connectorData = attribute.value;
if (!connectorData.ts) {
return false;
}
const clientIndex = this.activeData.findIndex(data => {
const sharedData = data.value;
const sharedData = data.value;
return sharedData.name === connectorData.name;
})
if (clientIndex == -1) return false;
});
if (clientIndex === -1) {
return false;
}
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 sharedIndex !== -1;
}
combineData() {
private combineData() {
this.dataSource.data = [...this.activeData, ...this.inactiveData, ...this.sharedAttributeData].filter((item, index, self) =>
index === self.findIndex((t) => t.key === item.key)
).map(attribute=>{
).map(attribute => {
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.clearOutConnectorForm();
}
clearOutConnectorForm(): void {
private clearOutConnectorForm(): void {
this.connectorForm.setValue({
name: '',
type: 'mqtt',
logLevel: GatewayLogLevel.info,
logLevel: GatewayLogLevel.INFO,
key: 'auto',
class: '',
configuration: '',
@ -347,7 +320,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.connectorForm.markAsPristine();
}
selectConnector(attribute): void {
selectConnector(attribute: AttributeData): void {
if (this.connectorForm.disabled) {
this.connectorForm.enable();
}
@ -363,8 +336,10 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.connectorForm.markAsPristine();
}
isSameConnector(attribute): boolean {
if (!this.initialConnector) return false;
isSameConnector(attribute: AttributeData): boolean {
if (!this.initialConnector) {
return false;
}
const connector = attribute.value;
return this.initialConnector.name === connector.name;
}
@ -378,12 +353,11 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
verticalPosition: 'top',
horizontalPosition: 'right',
target: 'dashboardRoot',
// panelClass: this.widgetNamespace,
forceDismiss: true
}));
}
returnType(attribute) {
returnType(attribute: AttributeData): string {
const value = attribute.value;
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.`;
this.dialogService.confirm(title, content, 'Cancel', 'Delete').subscribe(result => {
if (result) {
const tasks = [];
const scope = (this.initialConnector && this.activeConnectors.includes(this.initialConnector.name)) ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE;
const tasks: Array<Observable<any>> = [];
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]));
const activeIndex = this.activeConnectors.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);
}
tasks.push(this.attributeService.saveEntityAttributes(this.device, scope, [{
key: scope == AttributeScope.SHARED_SCOPE ? 'active_connectors' : 'inactive_connectors',
value: scope == AttributeScope.SHARED_SCOPE ? this.activeConnectors : this.inactiveConnectors
key: scope === AttributeScope.SHARED_SCOPE ? 'active_connectors' : 'inactive_connectors',
value: scope === AttributeScope.SHARED_SCOPE ? this.activeConnectors : this.inactiveConnectors
}]));
forkJoin(tasks).subscribe(_ => {
forkJoin(tasks).subscribe(() => {
if (this.initialConnector ? this.initialConnector.name === attribute.key : true) {
this.clearOutConnectorForm();
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 {
if ($event) {
$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 scopeOld = 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);
let errorText = exceptionData.name;
if (exceptionData.message) {
@ -484,11 +456,11 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
console.error(errorText);
}
onDataUpdated() {
private onDataUpdated() {
this.cd.detectChanges();
}
generateSubscription() {
private generateSubscription() {
if (this.subscription) {
this.subscription.unsubscribe();
}
@ -497,21 +469,23 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
type: DatasourceType.entity,
entityType: EntityType.DEVICE,
entityId: this.device.id,
entityName: "Gateway",
entityName: 'Gateway',
timeseries: []
}];
this.dataSource.data.forEach(value => {
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 => {
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.subscription = subscription;
});
}
}
getErrorsCount(attribute) {
getErrorsCount(attribute: AttributeData): string {
const connectorName = attribute.key;
const connector = this.subscription && this.subscription.data.find(data=>data && data.dataKey.name === `${connectorName}_ERRORS_COUNT`);
return (connector && this.activeConnectors.includes(connectorName))? connector.data[0][1]: 'Inactive';
const connector = this.subscription && this.subscription.data
.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">
<a mat-tab-link *ngFor="let link of logLinks"
(click)="onTabChanged(link)"
[active]="activeLink.name == link.name"> {{link.name}} </a>
[active]="activeLink.name === link.name"> {{ link.name }} </a>
</nav>
<mat-tab-nav-panel #tabPanel></mat-tab-nav-panel>
<table mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()"
matSortDisableClear>
<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">
{{ attribute.ts | date:'yyyy-MM-dd HH:mm:ss'}}
{{ attribute.ts | date:'yyyy-MM-dd HH:mm:ss' }}
</mat-cell>
</ng-container>
<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">
<span [ngClass]="statusClass(attribute.status )">{{ attribute.status }}</span>
<span [class]="statusClass(attribute.status)">{{ attribute.status }}</span>
</mat-cell>
</ng-container>
<ng-container matColumnDef="message">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 70%">Message</mat-header-cell>
<mat-cell *matCellDef="let attribute" [ngClass]="statusClassMsg(attribute.status )">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 70%">{{ 'widgets.gateway.message' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let attribute" [class]="statusClassMsg(attribute.status)">
{{ attribute.message }}
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}"
*matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row [ngClass]="{'mat-row-select': true}"
*matRowDef="let attribute; columns: displayedColumns;"></mat-row>
<mat-header-row class="mat-row-select" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row class="mat-row-select" *matRowDef="let attribute; columns: displayedColumns;"></mat-row>
</table>
<span [fxShow]="dataSource.data.length === 0"
fxFlex
fxLayoutAlign="center center"
class="no-data-found">{{'attribute.no-telemetry-text' | translate}}</span>
fxFlex fxLayoutAlign="center center"
class="no-data-found">{{ 'attribute.no-telemetry-text' | translate }}</span>
<span fxFlex [fxShow]="dataSource.data.length !== 0"></span>
<mat-divider></mat-divider>
<mat-paginator #paginator
[length]="dataSource.data.length"
<mat-paginator [length]="dataSource.data.length"
[pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize"
[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.
///
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 { 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 { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
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 { 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 { MatPaginator } from '@angular/material/paginator';
export interface GatewayConnector {
name: string;
type: string;
configuration?: string;
configurationJson: string;
logLevel: string;
key?: string;
}
export interface LogLink {
name: string;
key: string;
filterFn?: Function;
}
import { GatewayLogData, GatewayStatus, LogLink } from './gateway-widget.models';
@Component({
selector: 'tb-gateway-logs',
templateUrl: './gateway-logs.component.html',
styleUrls: ['./gateway-logs.component.scss']
})
export class GatewayLogsComponent extends PageComponent implements AfterViewInit {
export class GatewayLogsComponent implements AfterViewInit {
pageLink: PageLink;
attributeDataSource: AttributeDatasource;
dataSource: MatTableDataSource<any>
dataSource: MatTableDataSource<GatewayLogData>;
displayedColumns = ['ts', 'status', 'message'];
@ -76,63 +48,39 @@ export class GatewayLogsComponent extends PageComponent implements AfterViewInit
@ViewChild(MatSort) sort: MatSort;
@ViewChild(MatPaginator) paginator: MatPaginator;
connectorForm: FormGroup;
viewsInited = false;
textSearchMode: boolean;
activeConnectors: Array<string>;
inactiveConnectors: Array<string>;
InitialActiveConnectors: Array<string>;
gatewayLogLevel = Object.values(GatewayLogLevel);
logLinks: Array<LogLink>;
initialConnector: GatewayConnector;
activeLink: LogLink;
gatewayLogLinks: Array<LogLink> = [
{
name: "General",
key: "LOGS"
name: 'General',
key: 'LOGS'
}, {
name: "Service",
key: "SERVICE_LOGS"
name: 'Service',
key: 'SERVICE_LOGS'
},
{
name: "Connection",
key: "CONNECTION_LOGS"
name: 'Connection',
key: 'CONNECTION_LOGS'
}, {
name: "Storage",
key: "STORAGE_LOGS"
name: 'Storage',
key: 'STORAGE_LOGS'
},
{
key: 'EXTENSIONS_LOGS',
name: "Extension"
}]
constructor(protected router: Router,
protected store: Store<AppState>,
protected fb: FormBuilder,
protected translate: TranslateService,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
protected dialogService: DialogService,
public dialog: MatDialog) {
super(store);
name: 'Extension'
}];
constructor() {
const sortOrder: SortOrder = {property: 'ts', direction: Direction.DESC};
this.pageLink = new PageLink(10, 0, null, sortOrder);
this.dataSource = new MatTableDataSource<AttributeData>([]);
this.dataSource = new MatTableDataSource<GatewayLogData>([]);
}
ngAfterViewInit() {
this.dataSource.sort = this.sort;
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.updateDataSubscriptions();
return timewindow;
}
};
if (this.ctx.settings.isConnectorLog && this.ctx.settings.connectorLogState) {
const connector = this.ctx.stateController.getStateParams()[this.ctx.settings.connectorLogState];
this.logLinks = [{
key: `${connector.key}_LOGS`,
name: "Connector",
filterFn: (attrData)=>{
return !attrData.message.includes(`_converter.py`)
}
},{
name: 'Connector',
filterFn: (attrData) => !attrData.message.includes(`_converter.py`)
}, {
key: `${connector.key}_LOGS`,
name: "Converter",
filterFn: (attrData)=>{
return attrData.message.includes(`_converter.py`)
}
}]
name: 'Converter',
filterFn: (attrData) => attrData.message.includes(`_converter.py`)
}];
} else {
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]) {
let attrData = this.ctx.defaultSubscription.data[0].data.map(data => {
let result = {
const result = {
ts: data[0],
key: this.activeLink.key,
message: /\[(.*)/.exec(data[1])[0],
status: 'INVALID LOG FORMAT'
status: 'INVALID LOG FORMAT' as GatewayStatus
};
try {
result.status= data[1].match(/\|(\w+)\|/)[1];
result.status = data[1].match(/\|(\w+)\|/)[1];
} catch (e) {
result.status = 'INVALID LOG FORMAT'
result.status = 'INVALID LOG FORMAT' as GatewayStatus;
}
return result;
@ -186,39 +130,35 @@ export class GatewayLogsComponent extends PageComponent implements AfterViewInit
attrData = attrData.filter(data => this.activeLink.filterFn(data));
}
this.dataSource.data = attrData;
if (sort) {
this.dataSource.sortData(this.dataSource.data, this.sort);
}
}
}
onTabChanged(link) {
onTabChanged(link: LogLink) {
this.activeLink = link;
this.changeSubscription();
}
statusClass(status) {
statusClass(status: GatewayStatus): string {
switch (status) {
case GatewayLogLevel.debug:
return "status status-debug";
case GatewayLogLevel.warning:
return "status status-warning";
case GatewayLogLevel.error:
case "EXCEPTION":
return "status status-error";
case GatewayLogLevel.info:
case GatewayStatus.DEBUG:
return 'status status-debug';
case GatewayStatus.WARNING:
return 'status status-warning';
case GatewayStatus.ERROR:
case GatewayStatus.EXCEPTION:
return 'status status-error';
default:
return "status status-info";
return 'status status-info';
}
}
statusClassMsg(status) {
if (status === "EXCEPTION") {
statusClassMsg(status?: GatewayStatus): string {
if (status === GatewayStatus.EXCEPTION) {
return 'msg-status-exception';
}
}
changeSubscription() {
private changeSubscription() {
if (this.ctx.datasources && this.ctx.datasources[0].entity && this.ctx.defaultSubscription.options.datasources) {
this.ctx.defaultSubscription.options.datasources[0].dataKeys = [{
name: this.activeLink.key,
@ -229,8 +169,7 @@ export class GatewayLogsComponent extends PageComponent implements AfterViewInit
this.ctx.defaultSubscription.updateDataSubscriptions();
this.ctx.defaultSubscription.callbacks.onDataUpdated = () => {
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.
-->
<form style="width: 600px; position: relative;">
<mat-toolbar color="warn">
<mat-icon>warning</mat-icon>
<h2 translate>gateway.configuration-delete-dialog-header</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="close()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content tb-toast toastTarget="activationLinkDialogContent">
<div class="mat-content" fxLayout="column">
<span innerHTML="{{'gateway.configuration-delete-dialog-body' | translate}} <b>{{gatewayName}}</b>" > </span>
<mat-form-field class="mat-block tb-value-type" style="flex-grow: 0">
<mat-label translate>gateway.configuration-delete-dialog-input</mat-label>
<input matInput [formControl]="gatewayControl" required/>
<mat-error
*ngIf="gatewayControl.hasError('required')">
{{'gateway.configuration-delete-dialog-input-required' | translate }}
</mat-error>
</mat-form-field>
</div>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="warn"
type="button"
cdkFocusInitial
(click)="close()">
{{ 'action.cancel' | translate }}
</button>
<button mat-button color="warn"
type="button"
cdkFocusInitial
[disabled]="gatewayControl.value !== gatewayName"
(click)="turnOff()">
{{ 'gateway.configuration-delete-dialog-confirm' | translate }}
</button>
</div>
</form>
<mat-toolbar color="warn">
<mat-icon>warning</mat-icon>
<h2 translate>gateway.configuration-delete-dialog-header</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="close()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content style="max-width: 600px" class="mat-content" fxLayout="column">
<span innerHTML="{{ 'gateway.configuration-delete-dialog-body' | translate }} <b>{{ gatewayName }}</b>" ></span>
<mat-form-field class="mat-block tb-value-type" style="flex-grow: 0">
<mat-label translate>gateway.configuration-delete-dialog-input</mat-label>
<input matInput [formControl]="gatewayControl" required/>
<mat-error
*ngIf="gatewayControl.hasError('required')">
{{ 'gateway.configuration-delete-dialog-input-required' | translate }}
</mat-error>
</mat-form-field>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="warn"
type="button"
cdkFocusInitial
(click)="close()">
{{ 'action.cancel' | translate }}
</button>
<button mat-button color="warn"
type="button"
[disabled]="gatewayControl.value !== gatewayName"
(click)="turnOff()">
{{ 'gateway.configuration-delete-dialog-confirm' | translate }}
</button>
</div>

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

@ -14,7 +14,7 @@
/// 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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -31,11 +31,10 @@ export interface GatewayRemoteConfigurationDialogData {
templateUrl: './gateway-remote-configuration-dialog.html'
})
export class GatewayRemoteConfigurationDialogComponent extends DialogComponent<GatewayRemoteConfigurationDialogComponent,
boolean> implements OnInit {
export class GatewayRemoteConfigurationDialogComponent extends
DialogComponent<GatewayRemoteConfigurationDialogComponent, boolean>{
gatewayName: string;
gatewayControl: FormControl;
constructor(protected store: Store<AppState>,
@ -45,10 +44,7 @@ export class GatewayRemoteConfigurationDialogComponent extends DialogComponent<G
private fb: FormBuilder) {
super(store, router, dialogRef);
this.gatewayName = this.data.gatewayName;
this.gatewayControl = this.fb.control(null);
}
ngOnInit(): void {
this.gatewayControl = this.fb.control('');
}
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">
<mat-form-field class="mat-block tb-value-type">
<mat-label>{{'gateway.statistics.command' | translate}}</mat-label>
<mat-select formControlName="command" *ngIf="!isConnector">
<mat-option *ngFor="let command of RPCCommands" [value]="command">
{{command}}
</mat-option>
</mat-select>
<input matInput formControlName="command" *ngIf="isConnector"/>
</mat-form-field>
<mat-form-field class="mat-block tb-value-type" fxFlex *ngIf="!isConnector">
<mat-label>{{'gateway.statistics.timeout-ms' | translate}}</mat-label>
<input matInput formControlName="time" type="number" min="1"/>
<mat-error
*ngIf="commandForm.get('time').hasError('min')">
{{'gateway.statistics.timeout-min' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block tb-value-type" fxFlex *ngIf="isConnector">
<mat-label>{{'widget-config.datasource-parameters' | translate}}</mat-label>
<input matInput formControlName="params" type="JSON"/>
<mat-icon class="material-icons-outlined" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
(click)="openEditJSONDialog($event)"
matTooltip="{{'gateway.rpc-command-edit-params' | translate}}">edit
</mat-icon>
</mat-form-field>
<button mat-raised-button color="primary" (click)="sendCommand()"
[disabled]="commandForm.invalid">{{'gateway.rpc-command-send' | translate}}</button>
<ng-container *ngIf="!isConnector; else connectorForm">
<mat-form-field>
<mat-label>{{ 'gateway.statistics.command' | translate }}</mat-label>
<mat-select formControlName="command">
<mat-option *ngFor="let command of RPCCommands" [value]="command">
{{ command }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ 'gateway.statistics.timeout-ms' | translate }}</mat-label>
<input matInput formControlName="time" type="number" min="1"/>
<mat-error *ngIf="commandForm.get('time').hasError('min')">
{{ 'gateway.statistics.timeout-min' | translate }}
</mat-error>
</mat-form-field>
</ng-container>
<ng-template #connectorForm>
<mat-form-field>
<mat-label>{{ 'gateway.statistics.command' | translate }}</mat-label>
<input matInput formControlName="command" />
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ 'widget-config.datasource-parameters' | translate }}</mat-label>
<input matInput formControlName="params" type="JSON"/>
<mat-icon class="material-icons-outlined" aria-hidden="false" aria-label="help-icon"
matIconSuffix style="cursor:pointer;"
(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>
<mat-card class="result-block" [formGroup]="commandForm" fxFlex>
<span>{{'gateway.rpc-command-result' | translate}}</span>
<section class="result-block" [formGroup]="commandForm">
<span>{{ 'gateway.rpc-command-result' | translate }}</span>
<mat-divider></mat-divider>
<tb-json-content [contentType]="contentTypes.JSON" readonly="true" formControlName="result" fxFlex></tb-json-content>
</mat-card>
<tb-json-content [contentType]="contentTypes.JSON" readonly="true" formControlName="result"></tb-json-content>
</section>

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

@ -22,24 +22,24 @@
padding: 0;
.command-form {
width: 100%;
flex-wrap: nowrap;
padding: 0 15px 5px;
margin-bottom: 5px;
padding: 0 5px 5px;
& > button {
margin-top: 10px;
}
}
.result-block {
padding: 0 15px;
padding: 0 5px;
display: flex;
flex-direction: column;
flex: 1;
& > span {
font-weight: 600;
}
tb-json-content {
flex: 1;
}
}
}
@ -47,9 +47,5 @@
.tb-json-content {
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.
///
import { AfterViewInit, Component, Input } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { AfterViewInit, Component, Input } from '@angular/core';
import { FormBuilder, FormGroup, Validators } 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 { PageComponent } from "@shared/components/page.component";
import { DialogService } from '@core/services/dialog.service';
import { WidgetContext } from '@home/models/widget-component.models';
import { ContentType } from '@shared/models/constants';
import {
@ -33,13 +25,12 @@ import {
} from '@shared/components/dialog/json-object-edit-dialog.component';
import { jsonRequired } from '@shared/components/json-object-edit.component';
@Component({
selector: 'tb-gateway-service-rpc',
templateUrl: './gateway-service-rpc.component.html',
styleUrls: ['./gateway-service-rpc.component.scss']
})
export class GatewayServiceRPCComponent extends PageComponent implements AfterViewInit {
export class GatewayServiceRPCComponent implements AfterViewInit {
@Input()
ctx: WidgetContext;
@ -53,39 +44,28 @@ export class GatewayServiceRPCComponent extends PageComponent implements AfterVi
isConnector: boolean;
connectorType: string;
RPCCommands: Array<string> = [
"Ping",
"Stats",
"Devices",
"Update",
"Version",
"Restart",
"Reboot"
]
constructor(protected router: Router,
protected store: Store<AppState>,
protected fb: FormBuilder,
protected translate: TranslateService,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
protected dialogService: DialogService,
public dialog: MatDialog) {
super(store);
'Ping',
'Stats',
'Devices',
'Update',
'Version',
'Restart',
'Reboot'
];
private connectorType: string;
constructor(private fb: FormBuilder,
private dialog: MatDialog) {
this.commandForm = this.fb.group({
command: [null,[Validators.required]],
time: [60, [Validators.required, Validators.min(1)]],
params: [{}, [jsonRequired]],
result: [null]
})
});
}
ngAfterViewInit() {
this.isConnector = this.ctx.settings.isConnector;
if (!this.isConnector) {
@ -95,16 +75,16 @@ export class GatewayServiceRPCComponent extends PageComponent implements AfterVi
}
}
sendCommand() {
const formValues = this.commandForm.value;
const commandPrefix = this.isConnector ? `${this.connectorType}_` : 'gateway_';
this.ctx.controlApi.sendTwoWayCommand(commandPrefix+formValues.command.toLowerCase(), {},formValues.time).subscribe(resp=>{
this.commandForm.get('result').setValue(JSON.stringify(resp));
},error => {
console.log(error);
this.commandForm.get('result').setValue(JSON.stringify(error.error));
})
this.ctx.controlApi.sendTwoWayCommand(commandPrefix+formValues.command.toLowerCase(), {},formValues.time).subscribe({
next: resp => this.commandForm.get('result').setValue(JSON.stringify(resp)),
error: error => {
console.log(error);
this.commandForm.get('result').setValue(JSON.stringify(error.error));
}
});
}
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">
<mat-card [formGroup]="statisticForm" *ngIf="!general">
<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-option *ngFor="let key of statisticsKeys" [value]="key">
{{key}}
{{ key }}
</mat-option>
<mat-option *ngFor="let command of commands" [value]="command.attributeOnGateway">
{{command.attributeOnGateway}}
{{ command.attributeOnGateway }}
</mat-option>
</mat-select>
</mat-form-field>
@ -34,44 +34,42 @@
{{'gateway.statistics.statistic-commands-empty' | translate }}
</mat-error>
<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>
</mat-form-field>
</mat-card>
<div class="chart-box" fxLayout="column">
<div class="chart-container" #statisticChart [fxShow]="isNumericData">
</div>
<div class="chart-container" #statisticChart [fxShow]="isNumericData"></div>
<table [fxShow]="!isNumericData" mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()"
matSortDisableClear>
<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">
{{row[0]| date:'yyyy-MM-dd HH:mm:ss' }}
</mat-cell>
</ng-container>
<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">
{{ row[1] }}
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}"
<mat-header-row class="mat-row-select"
*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>
</table>
<span [fxShow]="dataSource.data.length === 0 && !isNumericData"
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 class="legend-keys" *ngFor="let legendKey of legendData?.keys" fxLayout="row"
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"
(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">
</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.
///
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { MatDialog } 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, 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 { TbFlot } from '@home/components/widget/lib/flot-widget';
import { ResizeObserver } from '@juggle/resize-observer';
@ -41,13 +33,12 @@ import { MatTableDataSource } from '@angular/material/table';
import { MatSort } from '@angular/material/sort';
import { NULL_UUID } from '@shared/models/id/has-uuid';
@Component({
selector: 'tb-gateway-statistics',
templateUrl: './gateway-statistics.component.html',
styleUrls: ['./gateway-statistics.component.scss']
})
export class GatewayStatisticsComponent extends PageComponent implements AfterViewInit {
export class GatewayStatisticsComponent implements AfterViewInit {
@ViewChild(MatSort) sort: MatSort;
@ViewChild('statisticChart') statisticChart: ElementRef;
@ -58,10 +49,11 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
@Input()
public general: boolean;
public isNumericData: boolean = false;
public isNumericData = false;
public dataTypeDefined: boolean = false;
public chartInited: boolean;
private flot: TbFlot;
private flotCtx;
private flotCtx: WidgetContext;
public statisticForm: FormGroup;
public statisticsKeys = [];
public commands = [];
@ -89,70 +81,61 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
};
constructor(protected router: Router,
protected store: Store<AppState>,
protected fb: FormBuilder,
protected translate: TranslateService,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
protected dialogService: DialogService,
private cd: ChangeDetectorRef,
private utils: UtilsService,
public dialog: MatDialog) {
super(store);
constructor(private fb: FormBuilder,
private attributeService: AttributeService,
private utils: UtilsService) {
const sortOrder: SortOrder = {property: '0', direction: Direction.DESC};
this.pageLink = new PageLink(Number.POSITIVE_INFINITY, 0, null, sortOrder);
this.displayedColumns = ['0', '1'];
this.dataSource = new MatTableDataSource<any>([]);
this.statisticForm = this.fb.group({
statisticKey: [null, []]
})
});
this.statisticForm.get('statisticKey').valueChanges.subscribe(value => {
this.commandObj = null;
if (this.commands.length) {
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() {
this.dataSource.sort = this.sort;
this.sort.sortChange.subscribe(_=>{
this.sortData();
})
this.sort.sortChange.subscribe(() => this.sortData());
this.init();
if (this.ctx.defaultSubscription.datasources.length) {
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) {
this.attributeService.getEntityAttributes(gateway.id, AttributeScope.SHARED_SCOPE, ["general_configuration"]).subscribe((resp: AttributeData[]) => {
if (resp && resp.length) {
this.commands = resp[0].value.statistics.commands;
if (!this.statisticForm.get('statisticKey').value && this.commands && this.commands.length) {
this.statisticForm.get('statisticKey').setValue(this.commands[0].attributeOnGateway);
this.createChartsSubscription(gateway, this.commands[0].attributeOnGateway);
this.attributeService.getEntityAttributes(gateway.id, AttributeScope.SHARED_SCOPE, ['general_configuration'])
.subscribe((resp: AttributeData[]) => {
if (resp && resp.length) {
this.commands = resp[0].value.statistics.commands;
if (!this.statisticForm.get('statisticKey').value && this.commands && this.commands.length) {
this.statisticForm.get('statisticKey').setValue(this.commands[0].attributeOnGateway);
this.createChartsSubscription(gateway, this.commands[0].attributeOnGateway);
}
}
}
})
});
} else {
let connectorsTs;
this.attributeService.getEntityTimeseriesLatest(gateway.id).subscribe(
data => {
connectorsTs = Object.keys(data)
.filter(el => el.includes(
'ConnectorEventsProduced'
) || el.includes(
'ConnectorEventsSent'))
const connectorsTs = Object.keys(data)
.filter(el => el.includes('ConnectorEventsProduced') || el.includes('ConnectorEventsSent'));
this.createGeneralChartsSubscription(gateway, connectorsTs);
})
});
}
}
}
public sortData () {
public sortData() {
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) {
let subscriptionInfo = [{
const subscriptionInfo = [{
type: DatasourceType.entity,
entityType: EntityType.DEVICE,
entityId: gateway.id.id,
@ -173,10 +156,11 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
subscriptionInfo[0].timeseries = [{name: attr, label: attr}];
this.subscriptionInfo = subscriptionInfo;
this.changeSubscription(subscriptionInfo);
this.ctx.defaultSubscription.unsubscribe();
}
private createGeneralChartsSubscription(gateway: BaseData<EntityId>, attrData: [string]) {
let subscriptionInfo = [{
private createGeneralChartsSubscription(gateway: BaseData<EntityId>, attrData: string[]) {
const subscriptionInfo = [{
type: DatasourceType.entity,
entityType: EntityType.DEVICE,
entityId: gateway.id.id,
@ -184,20 +168,20 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
timeseries: []
}];
subscriptionInfo[0].timeseries = [];
if (attrData && attrData.length) {
if (attrData?.length) {
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 => {
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.ctx.defaultSubscription.unsubscribe();
}
init = () => {
private init = () => {
this.flotCtx = {
$scope: this.ctx.$scope,
$injector: this.ctx.$injector,
@ -207,20 +191,20 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
subscriptionApi: this.ctx.subscriptionApi,
detectChanges: this.ctx.detectChanges,
settings: this.ctx.settings
};
}
} as WidgetContext;
};
updateChart = () => {
private updateChart = () => {
if (this.flot && this.ctx.defaultSubscription.data.length) {
this.flot.update();
}
}
};
resize = () => {
private resize = () => {
if (this.flot) {
this.flot.resize();
}
}
};
private reset() {
if (this.resize$) {
@ -260,7 +244,7 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
this.chartInited = true;
this.flotCtx.$container = $(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();
}
@ -270,16 +254,21 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
return;
}
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) {
this.reset();
}
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.isDataOnlyNumbers();
this.legendData = this.subscription.legendData;
@ -291,9 +280,7 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
if (this.isNumericData) {
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.
-->
<section class="tb-widget-settings" [formGroup]="GatewayLogSettingForm" fxLayout="column">
<mat-slide-toggle class="mat-slide" formControlName="isConnectorLog">
{{"widgets.gateway.is-connector" | translate}}
</mat-slide-toggle>
<mat-form-field fxFlex class="mat-block" *ngIf="GatewayLogSettingForm.get('isConnectorLog').value">
<mat-label>{{"widgets.gateway.state-param-name" | translate}}</mat-label>
<input matInput formControlName="connectorLogState">
</mat-form-field>
</section>
<div class="tb-form-panel" [formGroup]="gatewayLogSettingForm">
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="isConnectorLog">
{{ "widgets.gateway.show-connector" | translate }}
</mat-slide-toggle>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="connectorLogState" placeholder="{{ 'widgets.gateway.connector-state-param-key' | translate }}">
</mat-form-field>
</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 { 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 { AppState } from '@core/core.state';
@ -27,28 +27,41 @@ import { AppState } from '@core/core.state';
})
export class GatewayLogsSettingsComponent extends WidgetSettingsComponent {
GatewayLogSettingForm: UntypedFormGroup;
gatewayLogSettingForm: FormGroup;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
private fb: FormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.GatewayLogSettingForm;
protected settingsForm(): FormGroup {
return this.gatewayLogSettingForm;
}
protected defaultSettings(): WidgetSettings {
return {
isConnectorLog: false,
connectorLogState: 'default'
connectorLogState: ''
};
}
protected onSettingsSet(settings: WidgetSettings) {
this.GatewayLogSettingForm = this.fb.group({
this.gatewayLogSettingForm = this.fb.group({
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.
-->
<section class="tb-widget-settings" [formGroup]="GatewayLogSettingForm" fxLayout="column">
<mat-slide-toggle class="mat-slide" formControlName="isConnector">
{{"widgets.gateway.is-connector" | translate}}
</mat-slide-toggle>
</section>
<div class="tb-form-panel" [formGroup]="gatewayServiceRPCSettingForm">
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide margin" formControlName="isConnector">
{{ "widgets.gateway.show-connector" | translate }}
</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 { 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 { AppState } from '@core/core.state';
@ -27,15 +27,15 @@ import { AppState } from '@core/core.state';
})
export class GatewayServiceRPCSettingsComponent extends WidgetSettingsComponent {
GatewayLogSettingForm: UntypedFormGroup;
gatewayServiceRPCSettingForm: FormGroup;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
private fb: FormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.GatewayLogSettingForm;
protected settingsForm(): FormGroup {
return this.gatewayServiceRPCSettingForm;
}
protected defaultSettings(): WidgetSettings {
@ -45,7 +45,7 @@ export class GatewayServiceRPCSettingsComponent extends WidgetSettingsComponent
}
protected onSettingsSet(settings: WidgetSettings) {
this.GatewayLogSettingForm = this.fb.group({
this.gatewayServiceRPCSettingForm = this.fb.group({
isConnector: [false, []]
});
}

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

@ -15,29 +15,74 @@
limitations under the License.
-->
<div style="min-width: 800px;">
<mat-toolbar color="primary">
<h2><mat-icon>info_outline</mat-icon>
{{ 'edge.install-connect-instructions' | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<tb-markdown [data]="instructions" lineNumbers fallbackToPlainMarkdown></tb-markdown>
</div>
<div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.close' | translate }}
</button>
</div>
<mat-toolbar color="primary">
<h2 translate>{{ dialogTitle }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="close()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content>
<section *ngIf="loadedInstructions; else loadingInstructions" class="tb-form-panel no-padding no-border">
<div class="tb-form-panel no-padding no-border">
<mat-tab-group [(selectedIndex)]="tabIndex" (selectedTabChange)="selectedTabChange(tabIndex)">
<mat-tab>
<ng-template mat-tab-label>
<mat-icon class="tabs-icon" svgIcon="mdi:ubuntu"></mat-icon>
Ubuntu
</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.ubuntu'></tb-markdown>
</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 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.
///
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
import { DialogComponent } from "@shared/components/dialog.component";
import { Store } from "@ngrx/store";
import { AppState } from "@core/core.state";
import { Router } from "@angular/router";
export interface EdgeInstructionsData {
instructions: string;
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { ActionPreferencesPutUserSettings } from '@core/auth/auth.actions';
import { EdgeInfo, EdgeInstructionsMethod } from '@shared/models/edge.models';
import { EdgeService } from '@core/http/edge.service';
export interface EdgeInstructionsDialogData {
edge: EdgeInfo;
afterAdd: boolean;
}
@Component({
selector: 'tb-edge-instructions',
templateUrl: './edge-instructions-dialog.component.html'
selector: 'tb-edge-installation-dialog',
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>,
protected router: Router,
public dialogRef: MatDialogRef<EdgeInstructionsDialogComponent, EdgeInstructionsData>,
@Inject(MAT_DIALOG_DATA) public data: EdgeInstructionsData) {
@Inject(MAT_DIALOG_DATA) private data: EdgeInstructionsDialogData,
public dialogRef: MatDialogRef<EdgeInstructionsDialogComponent>,
private edgeService: EdgeService) {
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 {
this.dialogRef.close(null);
getInstructions(method: string) {
if (!this.contentData[method]) {
this.loadedInstructions = false;
this.edgeService.getEdgeInstallInstructions(this.data.edge.id.id, method).subscribe(
res => {
this.contentData[method] = res.installInstructions;
this.loadedInstructions = true;
}
);
}
}
}

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 { DatePipe } from '@angular/common';
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 { 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 { AppState } from '@core/core.state';
import { Authority } from '@app/shared/models/authority.enum';
@ -51,7 +51,7 @@ import {
AddEntitiesToCustomerDialogData
} from '../../dialogs/add-entities-to-customer-dialog.component';
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 { EdgeComponent } from '@home/pages/edge/edge.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 { ActionNotificationShow } from '@core/notification/notification.actions';
import {
EdgeInstructionsData,
EdgeInstructionsDialogComponent
} from "@home/pages/edge/edge-instructions-dialog.component";
EdgeInstructionsDialogComponent,
EdgeInstructionsDialogData
} from '@home/pages/edge/edge-instructions-dialog.component';
import { AddEntityDialogComponent } from '@home/components/entity/add-entity-dialog.component';
@Injectable()
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.entitiesDeleteEnabled = this.config.componentsData.edgeScope === 'tenant';
this.config.deleteEnabled = () => this.config.componentsData.edgeScope === 'tenant';
this.config.addEntity = () => { this.addEdge(); return of(null); };
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) {
$event.stopPropagation();
}
this.edgeService.getEdgeDockerInstallInstructions(edge.id.id).subscribe(
(edgeInstructionsTemplate: EdgeInstallInstructions) => {
this.dialog.open<EdgeInstructionsDialogComponent, EdgeInstructionsData>(EdgeInstructionsDialogComponent, {
disableClose: false,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
instructions: edgeInstructionsTemplate.dockerInstallInstructions
}
});
this.dialog.open<EdgeInstructionsDialogComponent, EdgeInstructionsDialogData>
(EdgeInstructionsDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
edge,
afterAdd
}
)
}).afterClosed().subscribe(() => {
if (afterAdd) {
this.config.updateData();
}
}
);
}
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">
<label class="tb-title" [ngClass]="{'tb-required': !disabled && required}">{{ label }}
<mat-icon *ngIf="hint" class="material-icons-outlined pointer-event"
style="cursor:pointer;"
matTooltip="{{hint}}">info_outlined
</mat-icon>
<label class="tb-title"
[class.tb-required]="!disabled && required"
[class.pointer-event]="hint"
tb-hint-tooltip-icon="{{ hint }}">{{ label }}
</label>
<ng-container #flow="flow"
[flowConfig]="{allowDuplicateUploads: true}">

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

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

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

@ -17,5 +17,6 @@
-->
<ng-content></ng-content>
<tb-icon class="tb-hint-tooltip-icon tb-mat-18"
matTooltip="{{ tooltipText }}"
[matTooltipPosition]="tooltipPosition">{{ hintIcon }}</tb-icon>
*ngIf="tooltipText"
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
* limitations under the License.
*/
:host {
:host(.tb-hint-tooltip) {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
:host {
.tb-hint-tooltip-icon {
color: #E0E0E0;
overflow: visible;

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

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

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

@ -179,5 +179,11 @@ export interface EdgeEvent extends BaseData<EventId> {
}
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 {
openedMenuSections?: string[];
notDisplayConnectivityAfterAddDevice?: boolean;
notDisplayInstructionsAfterAddEdge?: boolean;
}
export const initialUserSettings: UserSettings = {

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

@ -1293,7 +1293,6 @@
"device-details": "Device details",
"add-device-text": "Add new device",
"credentials": "Credentials",
"commands": "Commands",
"manage-credentials": "Manage credentials",
"delete": "Delete device",
"assign-devices": "Assign devices",
@ -1395,11 +1394,6 @@
"copyId": "Copy device Id",
"copyAccessToken": "Copy access token",
"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",
"accessTokenCopiedMessage": "Device access token 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.",
"import": "Import edge",
"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",
"load-entity-error": "Failed to load data. Entity has been deleted.",
"assign-new-edge": "Assign new edge",
@ -2643,43 +2639,18 @@
"gateway": {
"add-entry": "Add configuration",
"advanced": "Advanced",
"checking-device-activity": "Checking Device Activity:",
"checking-device-activity": "Checking device activity",
"command": "Docker commands",
"command-copied-message": "Docker command has been copied to clipboard",
"configuration": "Configuration",
"connector-json": "Connector JSON",
"connector-add": "Add new connector",
"connector-enabled": "Enable connector",
"connector-name": "Connector name",
"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-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-config": "Connectors configuration",
"connectors-active": "Connector active",
"connectors-inactive": "Connector inactive",
"connectors-table-enabled": "Enabled",
"connectors-table-name": "Name",
"connectors-table-type": "Type",
@ -2690,10 +2661,8 @@
"rpc-command-send": "Send",
"rpc-command-result": "Result",
"rpc-command-edit-params": "Edit parameters",
"select-connector": "Select connector",
"gateway-configuration": "Gateway Configuration",
"gateway-configuration": "General Configuration",
"docker-label": "In order to run ThingsBoard IoT gateway in docker with credentials for this device you can use the following commands.",
"copy-command": "Copy docker command",
"create-new-gateway": "Create a new gateway",
"create-new-gateway-text": "Are you sure you want create a new gateway with name: '{{gatewayName}}'?",
"created-time": "Created time",
@ -2710,7 +2679,6 @@
"gateway-name": "Gateway name",
"gateway-name-required": "Gateway name is required.",
"gateway-saved": "Gateway configuration successfully saved.",
"gateway-search": "Gateway search",
"grpc": "GRPC",
"grpc-keep-alive-timeout": "Keep alive timeout (in ms)",
"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-min": "Max pings without data can not be less then 1",
"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-required": "Inactivity check period is required",
"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",
"json-parse": "Not valid JSON.",
"json-required": "Field cannot be empty.",
"linux-macos": "Linux/MacOS",
"logs": {
"logs": "Logs",
"days": "days",
@ -2792,7 +2758,6 @@
"tls-private-key": "TLS + Private Key"
},
"server-port": "Server port",
"stats-send-period-in-sec": "Stats send period in seconds",
"statistics": {
"statistic": "Statistic",
"statistics": "Statistics",
@ -2851,7 +2816,7 @@
"sqlite": "SQLITE"
},
"thingsboard": "ThingsBoard",
"thingsboard-general": "General",
"general": "General",
"thingsboard-host": "ThingsBoard host",
"thingsboard-host-required": "Host is required.",
"thingsboard-port": "ThingsBoard port",
@ -2879,7 +2844,6 @@
"toggle-fullscreen": "Toggle fullscreen",
"transformer-json-config": "Configuration JSON*",
"update-config": "Add/update configuration JSON",
"windows": "Windows",
"hints": {
"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",
@ -2890,17 +2854,11 @@
"username": "MQTT username for the gateway form ThingsBoard server",
"password": "MQTT password for the gateway form ThingsBoard server",
"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",
"log-format": "Log message format",
"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",
"storage": "Provides configuration for saving incoming data before it is sent to the ThingsBoard 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)",
"storage": "Provides configuration for saving incoming data before it is sent to the platform",
"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-records": "Maximum count of records that will be stored in one file",
@ -3505,14 +3463,7 @@
"clientId-required": "Client ID is required",
"username": "Username",
"username-required": "Username is required",
"password": "Password",
"password-required": "Password is required",
"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": "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.",
@ -5343,8 +5294,11 @@
"events-title": "Gateway events form title",
"events-filter": "Events filter",
"event-key-contains": "Event key contains...",
"is-connector": "Is Connector",
"state-param-name": "State parameter connector name"
"show-connector": "Show for the connector",
"connector-state-param-key": "Connector state parameter key",
"status": "Status",
"message": "Message",
"created-time": "Created time"
},
"gauge": {
"default-color": "Default color",
@ -6419,7 +6373,6 @@
"element-click": "On HTML element click",
"pie-slice-click": "On slice click",
"row-double-click": "On row double click",
"action-button-click": "Action button click",
"card-click": "On card click"
}
},

5
ui-ngx/src/form.scss

@ -65,6 +65,9 @@
box-shadow: none;
border-radius: 0;
}
&.no-gap {
gap: 0;
}
&.tb-slide-toggle {
padding: 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 {
.mdc-text-field--filled:not(.mdc-text-field--disabled) {
&:before {

Loading…
Cancel
Save