Browse Source

Merge branch 'master' into feature/advanced-timewindow-configuration

pull/12151/head
Chantsova Ekaterina 2 years ago
parent
commit
1bc0dcd542
  1. 1
      README.md
  2. 1210
      application/src/main/data/json/system/scada_symbols/conical-tank.svg
  3. 426
      application/src/main/data/json/system/scada_symbols/extra-long-horizontal-pipe.svg
  4. 438
      application/src/main/data/json/system/scada_symbols/extra-long-vertical-pipe.svg
  5. 97
      application/src/main/data/json/system/scada_symbols/horizontal-broken-pipe.svg
  6. 7
      application/src/main/data/json/system/scada_symbols/horizontal-tank.svg
  7. 1220
      application/src/main/data/json/system/scada_symbols/large-conical-tank.svg
  8. 97
      application/src/main/data/json/system/scada_symbols/long-horizontal-broken-pipe.svg
  9. 97
      application/src/main/data/json/system/scada_symbols/long-vertical-broken-pipe.svg
  10. 1376
      application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg
  11. 7
      application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg
  12. 7
      application/src/main/data/json/system/scada_symbols/spherical-tank.svg
  13. 7
      application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg
  14. 97
      application/src/main/data/json/system/scada_symbols/vertical-broken-pipe.svg
  15. 9
      application/src/main/data/json/system/widget_bundles/scada_fluid_system.json
  16. 2
      application/src/main/data/json/system/widget_types/rpc_remote_shell.json
  17. 7023
      application/src/main/data/json/tenant/dashboards/gateways.json
  18. 7
      application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DashboardSyncService.java
  19. 76
      application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
  20. 4
      application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java
  21. 66
      application/src/main/java/org/thingsboard/server/service/update/DeprecationService.java
  22. 4
      application/src/main/resources/thingsboard.yml
  23. 19
      application/src/test/java/org/thingsboard/server/service/entitiy/dashboard/DashboardSyncServiceTest.java
  24. 5
      application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java
  25. 17
      application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java
  26. 48
      application/src/test/java/org/thingsboard/server/service/script/AbstractTbelInvokeTest.java
  27. 2151
      application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java
  28. 35
      application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java
  29. 4
      application/src/test/resources/logback-test.xml
  30. 36
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/GeneralNotificationInfo.java
  31. 1
      common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusAdmin.java
  32. 1
      common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubAdmin.java
  33. 1
      common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqAdmin.java
  34. 1
      common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsAdmin.java
  35. 2
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java
  36. 1
      common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java
  37. 12
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
  38. 9
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java
  39. 15
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java
  40. 2
      msa/js-executor/docker/Dockerfile
  41. 48
      msa/js-executor/package.json
  42. 4
      msa/js-executor/pom.xml
  43. 2
      msa/js-executor/queue/awsSqsTemplate.ts
  44. 2
      msa/js-executor/queue/serviceBusTemplate.ts
  45. 4310
      msa/js-executor/yarn.lock
  46. 4
      msa/vc-executor/src/main/resources/tb-vc-executor.yml
  47. 2
      msa/web-ui/docker/Dockerfile
  48. 39
      msa/web-ui/package.json
  49. 4
      msa/web-ui/pom.xml
  50. 1982
      msa/web-ui/yarn.lock
  51. 3
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java
  52. 37
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java
  53. 2
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  54. 32
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeTest.java
  55. 4
      transport/coap/src/main/resources/tb-coap-transport.yml
  56. 4
      transport/http/src/main/resources/tb-http-transport.yml
  57. 4
      transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml
  58. 4
      transport/mqtt/src/main/resources/tb-mqtt-transport.yml
  59. 4
      transport/snmp/src/main/resources/tb-snmp-transport.yml
  60. 67
      ui-ngx/angular.json
  61. 104
      ui-ngx/esbuild/tb-esbuild-plugins.ts
  62. 100
      ui-ngx/extra-webpack.config.js
  63. 40
      ui-ngx/generate-types.js
  64. 77
      ui-ngx/package.json
  65. 43
      ui-ngx/patches/@angular+build+18.2.11.patch
  66. 4
      ui-ngx/patches/@angular+core+18.2.10.patch
  67. 14
      ui-ngx/patches/jquery.terminal+2.43.1.patch
  68. 2
      ui-ngx/pom.xml
  69. 35
      ui-ngx/src/app/core/api/alias-controller.ts
  70. 18
      ui-ngx/src/app/core/interceptors/entity-conflict.interceptor.ts
  71. 2
      ui-ngx/src/app/core/services/menu.models.ts
  72. 19
      ui-ngx/src/app/core/services/utils.service.ts
  73. 8
      ui-ngx/src/app/core/utils.ts
  74. 2
      ui-ngx/src/app/modules/common/modules-map.ts
  75. 2
      ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html
  76. 12
      ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts
  77. 2
      ui-ngx/src/app/modules/home/components/vc/entity-version-diff.component.ts
  78. 20
      ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.component.ts
  79. 77
      ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts
  80. 9
      ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.models.ts
  81. 4
      ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts
  82. 4
      ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.ts
  83. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action.models.ts
  84. 15
      ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-provider-settings.component.ts
  85. 26
      ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-settings.component.ts
  86. 28
      ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-widget-settings.component.ts
  87. 8
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  88. 3
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.html
  89. 10
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  90. 8
      ui-ngx/src/app/modules/home/models/datasource/attribute-datasource.ts
  91. 12
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  92. 3
      ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html
  93. 2
      ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss
  94. 3
      ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts
  95. 2
      ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.ts
  96. 3
      ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.ts
  97. 1
      ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.html
  98. 2
      ui-ngx/src/app/shared/components/dialog/confirm-dialog.component.html
  99. 4
      ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts
  100. 2
      ui-ngx/src/app/shared/components/entity/entity-select.component.ts

1
README.md

@ -1,5 +1,4 @@
# ThingsBoard # ThingsBoard
[![Join the chat at https://gitter.im/thingsboard/chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/thingsboard/chat?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![ThingsBoard Builds Server Status](https://img.shields.io/teamcity/build/e/ThingsBoard_Build?label=TB%20builds%20server&server=https%3A%2F%2Fbuilds.thingsboard.io&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAALzAAAC8wHS6QoqAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAB9FJREFUeJzVm3+MXUUVx7+zWwqEtnRLWisQ2lKVUisIQmsqYCohpUhpEGsFKSJJTS0qGiGIISJ/8CNGYzSaEKBQEZUiP7RgVbCVdpE0xYKBWgI2rFLZJZQWtFKobPfjH3Pfdu7s3Pvmzntv3/JNNr3bOXPO+Z6ZO3PumVmjFgEYJWmWpDmSZks6VtIESV3Zv29LWmGMubdVPgw7gEOBJcAaYC/18fd2+zyqngAwXdL7M9keSduMMXgyH5R0laRPSRpbwf62CrLDB8AAS4HnAqP2EvA1YBTwPuBnwP46I70H+DPwALAS+B5wBTCu3VyHIJvG98dMX+B/BW1vAvcAnwdmAp3t5hWFbORXR5AvwmPARcCYdnNJAnCBR+gd7HQ9HZgLfAt4PUB8AzCv3f43DGCTQ6o/RAo43gtCL2Da4W9TAUwEBhxiPymRvcabAR8eTl+biQ7neYokdyTXlvR7xPt9etM8GmZ0FDxL+WD42FdBdkTDJd0jyU1wzi7pd473e0+qA8AM4AbgkrK1BDgOWAc8ChyTaq+eM5ud93ofcHpAZiY2sanhZaDDaTfAZ7HJUmlWCJzm6bqLQM6QBanXkfthcxgPNbTEW9z2AT8AzgTmANdikxwXX/d0XOi0bQEmFNj6GPAfhuKnXkB98kNsNjsITwacKkI3MNrrf4UnswXoiiRfwyqgo4D8L2hVZglMw456DDYCRwR0jCH/KuWCgE2oysjX8KsA+V+2jHzm3CrP4PMBx/4JfAU4qETP+EAQ/gKcA/w7gnwNbl5yD7bG0DLyM7DZXw3d2f9PA+YD5wIzK+gLBSEFA/XIA2cAVwLvbSQAt3mGP5Gs7IDO8dg1ZYDGcAfOwujZuIwDn+ObUx09hHx+v7Eh5nndCyIIDgBbgd0lMiv9IABfIF+LeDnVyU97xj5XR/6bwI5sZEaXyH2UuHd+WSbfRXktYjAIAfL9wGdSA/Cgo+gtSio12IKJa3hNKAgZ+TciyL+AlwECKzI/ioLgTvsa+YtTyXeSz8ZW15E3wN88p3JBwCZNMeShIKkBTsRmmSG4a0o/sDSJfGboBE/5pRF9pgI9oSBUJP8mXpLk2bm6pO9Aw+QzI8s8xVFbXRaEf3h911cgD7Cyjg0/L/GxnoLdoUoA3O1vDxUyLWyO4AehCpYX6D2L/LpUhtsaCkIWxRoeT+g/DVsqT8EWYDowC5jh6FxUUc+tJJblOmSPqWp4JUFHl6TDUoxLOlnSdknPSnK3sA2S9lfQs0zS7SkzwQ/A61U6A6dKWufpSMVg5mmMeUPSXyv2v0zSN6oa7ZAdwRqiA5CRf0TS+KpGAxiQ1OFN4z8l6PErVXUxSvmp1hvTqUnk35adPWskPWSM6fPaq84ASXqscg/gi9gcvJuC6o0nfwrhw5EYvIpNn88HStcN4M6KulfTys/lzKlO0lb8P2Lrf6VbLDAF+DLweEX998aSx372bwP6gPlVA3BEAvm9FJwVYtPqjwDXA08n6AZbOYoeeeAWp++mSlPGGLMLeFjSuRW6Iektx4GDJc2TdJ6khZKOruKDh/skXWSM6a/Q5yjn+dDKFrE1vw0VR2m2039x4kj7uJ+SslyJ/+7rtaly4mCM+a+kBaq2TbnVpfWy216jmCzpkIR+7kK/MymHNsbslX0NYoMweMpsjNklaWuKXQ9zJf2eOocvAbzHee5N/ojIgvBVxY3madh3v4b1iWZ/o3zw5kpaS+SFDGCq8jPguUQ/CmsCZfi403dhwjv/AHAQMAl41mvbGBMEhq4/c1PJTwmQr1f7u97pfzj5EnwUead/KAg/ivD7Zkf+HSBpFwiRfwibI3SXkOj29PgEivAggdU+C8JWR+6+CN9dm1tSyHcBLwbIj87ax1Kcxe0DJmVyY4CdEeR/TXnVeRLwc+C3wHF1fP+Qp/uGlABc6Cl5mPziVi8IzwDfAZ6KIN9LyhQt9v1GT/+sFCXTOVBBXuOTd+TGkp+eqWjKSTBwMPAvR+9TjSibjK35l93mWIxdZFKOxPzFseEgAJd7Olt6v+AC8jdIqwRhLbZM758HRH3tYa/vnoqtKZ4JHIk99tvh6HqNVl3RLSB/JfBEBPnBwxXsJ2uf176qxO7hwE3ALq/PfuyVXhdXt4r8+QHyK7K2cXWCMLiTOPqODwTh2IDdD2CP12LwCnUKMankO8kfiAySd2SKgjCEfEEQ+nznsZc7eyLJA9zddPKZIx0c2NcHgMsL5MZhr83XULiTeCSXAEcG2m4PjPCXsEWWBdhbZ/4h6knN4u07Mxv4MbCojtxo7DW6RTRwopMFxt0xeoCJAblLvCDdlWpzRAG42CO2sET2UUfuVbetsYPF9mKq8zwg6Q8lsm7bRJxt8N0cAPdar5FUupYU9X03B2C782wknVUi+0nneacxZk9rXBpGABO8RXA72demJ7fcWyvubIe/TQN2y11MuJ6wA5v3z8HeMbjba+8n5StwJCDb9lYUEI/Fde3mEQ1svnBKRvp32K/LEPYQd1z3XQJfsG3/Sw/gKElLZev8tb8rnizpBEmF1SDZ06ZbJN0saa+kayQtV77qi6QnJF1njFnXdOebAcIXssvQB3yfcGrcCZwEnAfMC8mMKGArNUVT28VubF4/nyZflx8Jr8BVkr4tm83tzn5ek/S8pM2SnpT0gv8H283C/wGTFfhGtexQwQAAAABJRU5ErkJggg==&labelColor=305680)](https://builds.thingsboard.io/viewType.html?buildTypeId=ThingsBoard_Build&guest=1) [![ThingsBoard Builds Server Status](https://img.shields.io/teamcity/build/e/ThingsBoard_Build?label=TB%20builds%20server&server=https%3A%2F%2Fbuilds.thingsboard.io&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAALzAAAC8wHS6QoqAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAB9FJREFUeJzVm3+MXUUVx7+zWwqEtnRLWisQ2lKVUisIQmsqYCohpUhpEGsFKSJJTS0qGiGIISJ/8CNGYzSaEKBQEZUiP7RgVbCVdpE0xYKBWgI2rFLZJZQWtFKobPfjH3Pfdu7s3Pvmzntv3/JNNr3bOXPO+Z6ZO3PumVmjFgEYJWmWpDmSZks6VtIESV3Zv29LWmGMubdVPgw7gEOBJcAaYC/18fd2+zyqngAwXdL7M9keSduMMXgyH5R0laRPSRpbwf62CrLDB8AAS4HnAqP2EvA1YBTwPuBnwP46I70H+DPwALAS+B5wBTCu3VyHIJvG98dMX+B/BW1vAvcAnwdmAp3t5hWFbORXR5AvwmPARcCYdnNJAnCBR+gd7HQ9HZgLfAt4PUB8AzCv3f43DGCTQ6o/RAo43gtCL2Da4W9TAUwEBhxiPymRvcabAR8eTl+biQ7neYokdyTXlvR7xPt9etM8GmZ0FDxL+WD42FdBdkTDJd0jyU1wzi7pd473e0+qA8AM4AbgkrK1BDgOWAc8ChyTaq+eM5ud93ofcHpAZiY2sanhZaDDaTfAZ7HJUmlWCJzm6bqLQM6QBanXkfthcxgPNbTEW9z2AT8AzgTmANdikxwXX/d0XOi0bQEmFNj6GPAfhuKnXkB98kNsNjsITwacKkI3MNrrf4UnswXoiiRfwyqgo4D8L2hVZglMw456DDYCRwR0jCH/KuWCgE2oysjX8KsA+V+2jHzm3CrP4PMBx/4JfAU4qETP+EAQ/gKcA/w7gnwNbl5yD7bG0DLyM7DZXw3d2f9PA+YD5wIzK+gLBSEFA/XIA2cAVwLvbSQAt3mGP5Gs7IDO8dg1ZYDGcAfOwujZuIwDn+ObUx09hHx+v7Eh5nndCyIIDgBbgd0lMiv9IABfIF+LeDnVyU97xj5XR/6bwI5sZEaXyH2UuHd+WSbfRXktYjAIAfL9wGdSA/Cgo+gtSio12IKJa3hNKAgZ+TciyL+AlwECKzI/ioLgTvsa+YtTyXeSz8ZW15E3wN88p3JBwCZNMeShIKkBTsRmmSG4a0o/sDSJfGboBE/5pRF9pgI9oSBUJP8mXpLk2bm6pO9Aw+QzI8s8xVFbXRaEf3h911cgD7Cyjg0/L/GxnoLdoUoA3O1vDxUyLWyO4AehCpYX6D2L/LpUhtsaCkIWxRoeT+g/DVsqT8EWYDowC5jh6FxUUc+tJJblOmSPqWp4JUFHl6TDUoxLOlnSdknPSnK3sA2S9lfQs0zS7SkzwQ/A61U6A6dKWufpSMVg5mmMeUPSXyv2v0zSN6oa7ZAdwRqiA5CRf0TS+KpGAxiQ1OFN4z8l6PErVXUxSvmp1hvTqUnk35adPWskPWSM6fPaq84ASXqscg/gi9gcvJuC6o0nfwrhw5EYvIpNn88HStcN4M6KulfTys/lzKlO0lb8P2Lrf6VbLDAF+DLweEX998aSx372bwP6gPlVA3BEAvm9FJwVYtPqjwDXA08n6AZbOYoeeeAWp++mSlPGGLMLeFjSuRW6Iektx4GDJc2TdJ6khZKOruKDh/skXWSM6a/Q5yjn+dDKFrE1vw0VR2m2039x4kj7uJ+SslyJ/+7rtaly4mCM+a+kBaq2TbnVpfWy216jmCzpkIR+7kK/MymHNsbslX0NYoMweMpsjNklaWuKXQ9zJf2eOocvAbzHee5N/ojIgvBVxY3madh3v4b1iWZ/o3zw5kpaS+SFDGCq8jPguUQ/CmsCZfi403dhwjv/AHAQMAl41mvbGBMEhq4/c1PJTwmQr1f7u97pfzj5EnwUead/KAg/ivD7Zkf+HSBpFwiRfwibI3SXkOj29PgEivAggdU+C8JWR+6+CN9dm1tSyHcBLwbIj87ax1Kcxe0DJmVyY4CdEeR/TXnVeRLwc+C3wHF1fP+Qp/uGlABc6Cl5mPziVi8IzwDfAZ6KIN9LyhQt9v1GT/+sFCXTOVBBXuOTd+TGkp+eqWjKSTBwMPAvR+9TjSibjK35l93mWIxdZFKOxPzFseEgAJd7Olt6v+AC8jdIqwRhLbZM758HRH3tYa/vnoqtKZ4JHIk99tvh6HqNVl3RLSB/JfBEBPnBwxXsJ2uf176qxO7hwE3ALq/PfuyVXhdXt4r8+QHyK7K2cXWCMLiTOPqODwTh2IDdD2CP12LwCnUKMankO8kfiAySd2SKgjCEfEEQ+nznsZc7eyLJA9zddPKZIx0c2NcHgMsL5MZhr83XULiTeCSXAEcG2m4PjPCXsEWWBdhbZ/4h6knN4u07Mxv4MbCojtxo7DW6RTRwopMFxt0xeoCJAblLvCDdlWpzRAG42CO2sET2UUfuVbetsYPF9mKq8zwg6Q8lsm7bRJxt8N0cAPdar5FUupYU9X03B2C782wknVUi+0nneacxZk9rXBpGABO8RXA72demJ7fcWyvubIe/TQN2y11MuJ6wA5v3z8HeMbjba+8n5StwJCDb9lYUEI/Fde3mEQ1svnBKRvp32K/LEPYQd1z3XQJfsG3/Sw/gKElLZev8tb8rnizpBEmF1SDZ06ZbJN0saa+kayQtV77qi6QnJF1njFnXdOebAcIXssvQB3yfcGrcCZwEnAfMC8mMKGArNUVT28VubF4/nyZflx8Jr8BVkr4tm83tzn5ek/S8pM2SnpT0gv8H283C/wGTFfhGtexQwQAAAABJRU5ErkJggg==&labelColor=305680)](https://builds.thingsboard.io/viewType.html?buildTypeId=ThingsBoard_Build&guest=1)
ThingsBoard is an open-source IoT platform for data collection, processing, visualization, and device management. ThingsBoard is an open-source IoT platform for data collection, processing, visualization, and device management.

1210
application/src/main/data/json/system/scada_symbols/conical-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 85 KiB

426
application/src/main/data/json/system/scada_symbols/extra-long-horizontal-pipe.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

438
application/src/main/data/json/system/scada_symbols/extra-long-vertical-pipe.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

97
application/src/main/data/json/system/scada_symbols/horizontal-broken-pipe.svg

@ -0,0 +1,97 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="200" fill="none" version="1.1" viewBox="0 0 200 200"><tb:metadata xmlns=""><![CDATA[{
"title": "Horizontal broken pipe",
"description": "Horizontal broken pipe.",
"searchTags": [
"pipe",
"horizontal pipe",
"broken pipe"
],
"widgetSizeX": 1,
"widgetSizeY": 1,
"tags": [
{
"tag": "clickArea",
"stateRenderFunction": null,
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'click');"
}
}
},
{
"tag": "pipe-background",
"stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});",
"actions": null
}
],
"behavior": [
{
"id": "click",
"name": "{i18n:scada.symbol.on-click}",
"hint": "{i18n:scada.symbol.on-click-hint}",
"group": null,
"type": "widgetAction",
"valueType": "BOOLEAN",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": null,
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": {
"type": "doNothing",
"targetDashboardStateId": null,
"openRightLayout": false,
"setEntityId": false,
"stateEntityParamName": null
}
}
],
"properties": [
{
"id": "pipeColor",
"name": "{i18n:scada.symbol.pipe-color}",
"type": "color",
"default": "#FFFFFF",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
]
}]]></tb:metadata>
<g tb:tag="clickArea">
<path d="m14 64s53.5 0.2293 60.5 0 17-6.5 17-6.5l-14.5 21.5 14.5 6.5-10.5 9 14.5 11-18.5 6.5 10 6-2.5 11.5 7 14s-10-7.5-17-7.5h-60.5v-72z" fill="#fff" tb:tag="pipe-background"/>
<path d="m14 64s53.5 0.2293 60.5 0 17-6.5 17-6.5l-14.5 21.5 14.5 6.5-10.5 9 14.5 11-18.5 6.5 10 6-2.5 11.5 7 14s-10-7.5-17-7.5h-60.5v-72z" fill="url(#paint0_linear_2474_252376)"/>
<path d="m86.266 62.02c0.1879-0.0889 0.3731-0.1777 0.5555-0.2661l-12.062 17.886 13.968 6.2614-8.7036 7.4602-1.4118 1.2101 13.759 10.438-15.869 5.576-3.0127 1.058 2.7382 1.643 9.0774 5.447-2.2715 10.448-0.1123 0.517 0.2365 0.473 4.5114 9.023c-0.4206-0.249-0.8606-0.502-1.3163-0.755-3.3898-1.883-7.9357-3.939-11.854-3.939h-59v-68.994l0.8066 0.0033c1.4767 0.0058 3.5942 0.0139 6.1569 0.0228 5.1255 0.0179 12.032 0.0394 19.158 0.0538 14.23 0.0286 29.393 0.0289 32.927-0.0868 3.8302-0.1255 8.3197-1.8722 11.717-3.4792z" stroke="#000" stroke-opacity=".12" stroke-width="3"/>
<path d="m105.5 56.5s13.5 7.395 18.5 7.5 62 0 62 0v72s-54.5-0.086-62 0-18.5 4.5-18.5 4.5l6-13.5-15-14 16.5-5-7.5-6 6-13-18-13 19.5-4.5-7.5-15z" fill="#fff" tb:tag="pipe-background"/>
<path d="m105.5 56.5s13.5 7.395 18.5 7.5 62 0 62 0v72s-54.5-0.086-62 0-18.5 4.5-18.5 4.5l6-13.5-15-14 16.5-5-7.5-6 6-13-18-13 19.5-4.5-7.5-15z" fill="url(#paint1_linear_2474_252376)"/>
<path d="m184.5 65.503v68.995l-0.854-2e-3c-1.505-2e-3 -3.663-5e-3 -6.277-8e-3 -5.227-7e-3 -12.274-0.015-19.555-0.02-14.556-0.011-30.069-0.011-33.831 0.032-3.973 0.046-8.768 1.225-12.479 2.346-1.18 0.357-2.264 0.712-3.197 1.031l4.564-10.268 0.434-0.976-0.782-0.73-13.096-12.223 14.008-4.244 2.727-0.827-2.225-1.78-6.583-5.267 5.508-11.933 0.518-1.1215-1.002-0.7231-15.321-11.065 16.28-3.757 1.857-0.4284-0.852-1.704-5.435-10.87c1.009 0.5039 2.144 1.0566 3.335 1.611 2.02 0.9404 4.222 1.8953 6.25 2.6248 1.981 0.7123 3.962 1.2726 5.476 1.3044 2.522 0.0529 18.049 0.0528 32.908 0.0397 7.438-0.0066 14.72-0.0164 20.142-0.0246 2.711-0.0041 4.957-0.0078 6.526-0.0105l0.956-0.0017z" stroke="#000" stroke-opacity=".12" stroke-width="3"/>
<rect x="187.5" y="51.5" width="11" height="97" rx="5.5" fill="#D9D9D9" stroke="#727171" stroke-width="3"/>
<rect x="1.5" y="51.5" width="11" height="97" rx="5.5" fill="#D9D9D9" stroke="#727171" stroke-width="3"/>
</g><defs>
<linearGradient id="paint0_linear_2474_252376" x1="32.98" x2="32.532" y1="64" y2="136" gradientUnits="userSpaceOnUse">
<stop stop-color="#727171" offset="0"/>
<stop stop-color="#727171" stop-opacity=".35" offset=".26388"/>
<stop stop-color="#727171" stop-opacity=".1" offset=".41759"/>
<stop stop-color="#fff" stop-opacity="0" offset=".49829"/>
<stop stop-color="#727171" stop-opacity=".1" offset=".58094"/>
<stop stop-color="#727171" stop-opacity=".35" offset=".71855"/>
<stop stop-color="#727171" offset="1"/>
</linearGradient>
<linearGradient id="paint1_linear_2474_252376" x1="129.76" x2="129.33" y1="64" y2="136" gradientUnits="userSpaceOnUse">
<stop stop-color="#727171" offset="0"/>
<stop stop-color="#727171" stop-opacity=".35" offset=".26388"/>
<stop stop-color="#727171" stop-opacity=".1" offset=".41759"/>
<stop stop-color="#fff" stop-opacity="0" offset=".49829"/>
<stop stop-color="#727171" stop-opacity=".1" offset=".58094"/>
<stop stop-color="#727171" stop-opacity=".35" offset=".71855"/>
<stop stop-color="#727171" offset="1"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

7
application/src/main/data/json/system/scada_symbols/horizontal-tank.svg

@ -36,6 +36,11 @@
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 17;\n var majorIntervalLength = 568 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(715, y, 747, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 705, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(727, minorY, 747, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 17;\n var majorIntervalLength = 568 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(715, y, 747, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 705, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(727, minorY, 747, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{
"tag": "scale-background",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n}",
"actions": null
},
{ {
"tag": "top-layer", "tag": "top-layer",
"stateRenderFunction": "if (ctx.properties.transparent || !ctx.properties.scale) {\n element.hide();\n}", "stateRenderFunction": "if (ctx.properties.transparent || !ctx.properties.scale) {\n element.hide();\n}",
@ -555,7 +560,7 @@
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<path d="m174 600c-174 0-174-151.63-174-239 4.9551e-6 -1.386 9.6938e-6 -2.741 1.4223e-5 -4.065 1.18e-8 -4e-3 -1.4223e-5 -113.93-1.4223e-5 -113.94 0-87.373-8.3346e-7 -243 174-243h653.5c173 0 172.5 156.06 172.5 243v118c0 86.939 0 239-172.5 239h-653.5z" fill="#E5E5E5" tb:tag="background"/><path d="m174 600c-174 0-174-151.63-174-239 4.9551e-6 -1.386 9.6938e-6 -2.741 1.4223e-5 -4.065 1.18e-8 -4e-3 -1.4223e-5 -113.93-1.4223e-5 -113.94 0-87.373-8.3346e-7 -243 174-243h653.5c173 0 172.5 156.06 172.5 243v118c0 86.939 0 239-172.5 239h-653.5z" fill="url(#paint0_linear_1694_158298)"/><path d="m177.27 0.4874c198.56-1.0967 560.65 1.9e-5 640.18 0 79.522-1.9e-5 183.55 15 183.55 220v169c0 192.5-105.53 211.5-184.55 211.5s-516.64-0.5-639.18-0.5c-122.53 0-176.55-68.001-176.55-219 5.26e-4 -151-6.59e-4 -49.502-2.47e-4 -119s-22.006-260.91 176.55-262.01z" fill="#4A4848" fill-opacity=".5"/><mask id="mask0_1694_158298" x="17" y="16" width="968" height="570" style="mask-type:alpha" maskUnits="userSpaceOnUse"> <path d="m174 600c-174 0-174-151.63-174-239 4.9551e-6 -1.386 9.6938e-6 -2.741 1.4223e-5 -4.065 1.18e-8 -4e-3 -1.4223e-5 -113.93-1.4223e-5 -113.94 0-87.373-8.3346e-7 -243 174-243h653.5c173 0 172.5 156.06 172.5 243v118c0 86.939 0 239-172.5 239h-653.5z" fill="#E5E5E5" tb:tag="background"/><path d="m174 600c-174 0-174-151.63-174-239 4.9551e-6 -1.386 9.6938e-6 -2.741 1.4223e-5 -4.065 1.18e-8 -4e-3 -1.4223e-5 -113.93-1.4223e-5 -113.94 0-87.373-8.3346e-7 -243 174-243h653.5c173 0 172.5 156.06 172.5 243v118c0 86.939 0 239-172.5 239h-653.5z" fill="url(#paint0_linear_1694_158298)"/><path d="m177.27 0.4874c198.56-1.0967 560.65 1.9e-5 640.18 0 79.522-1.9e-5 183.55 15 183.55 220v169c0 192.5-105.53 211.5-184.55 211.5s-516.64-0.5-639.18-0.5c-122.53 0-176.55-68.001-176.55-219 5.26e-4 -151-6.59e-4 -49.502-2.47e-4 -119s-22.006-260.91 176.55-262.01z" fill="#4A4848" fill-opacity=".5" tb:tag="scale-background"/><mask id="mask0_1694_158298" x="17" y="16" width="968" height="570" style="mask-type:alpha" maskUnits="userSpaceOnUse">
<path d="m178.68 16h654.82c117.67 0 151.5 98.586 151.5 184.16v204.18c0 135.62-68.502 181.66-162.5 181.66h-627.98c-129 0-177.5-66.558-177.5-181.66s0.0023-129.32 0-212.19c-3e-4 -9.509-5.4994-176.16 161.66-176.16z" fill="#D9D9D9"/> <path d="m178.68 16h654.82c117.67 0 151.5 98.586 151.5 184.16v204.18c0 135.62-68.502 181.66-162.5 181.66h-627.98c-129 0-177.5-66.558-177.5-181.66s0.0023-129.32 0-212.19c-3e-4 -9.509-5.4994-176.16 161.66-176.16z" fill="#D9D9D9"/>
</mask><g mask="url(#mask0_1694_158298)"> </mask><g mask="url(#mask0_1694_158298)">
<rect transform="scale(1,-1)" x="9" y="-585" width="984" height="200" fill="#1EC1F4" fill-opacity=".5" tb:tag="fluid-background"/> <rect transform="scale(1,-1)" x="9" y="-585" width="984" height="200" fill="#1EC1F4" fill-opacity=".5" tb:tag="fluid-background"/>

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 115 KiB

1220
application/src/main/data/json/system/scada_symbols/large-conical-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 85 KiB

97
application/src/main/data/json/system/scada_symbols/long-horizontal-broken-pipe.svg

@ -0,0 +1,97 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="400" height="200" fill="none" version="1.1" viewBox="0 0 400 200"><tb:metadata xmlns=""><![CDATA[{
"title": "Long horizontal broken pipe",
"description": "Long horizontal broken pipe.",
"searchTags": [
"long pipe",
"horizontal pipe",
"broken pipe"
],
"widgetSizeX": 2,
"widgetSizeY": 1,
"tags": [
{
"tag": "clickArea",
"stateRenderFunction": null,
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'click');"
}
}
},
{
"tag": "pipe-background",
"stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});",
"actions": null
}
],
"behavior": [
{
"id": "click",
"name": "{i18n:scada.symbol.on-click}",
"hint": "{i18n:scada.symbol.on-click-hint}",
"group": null,
"type": "widgetAction",
"valueType": "BOOLEAN",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": null,
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": {
"type": "doNothing",
"targetDashboardStateId": null,
"openRightLayout": false,
"setEntityId": false,
"stateEntityParamName": null
}
}
],
"properties": [
{
"id": "pipeColor",
"name": "{i18n:scada.symbol.pipe-color}",
"type": "color",
"default": "#FFFFFF",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
]
}]]></tb:metadata>
<g tb:tag="clickArea">
<path d="m14 64s158.08 0.2293 165.06 0c6.982-0.2293 16.955-6 16.955-6l-14.461 21 14.461 6.5-10.472 9 14.461 11-18.45 6.5 9.973 6-2.494 11.5 6.982 14.5s-9.973-8-16.955-8h-165.06v-72z" fill="#fff" tb:tag="pipe-background"/>
<path d="m14 64s158.08 0.2293 165.06 0c6.982-0.2293 16.955-6 16.955-6l-14.461 21 14.461 6.5-10.472 9 14.461 11-18.45 6.5 9.973 6-2.494 11.5 6.982 14.5s-9.973-8-16.955-8h-165.06v-72z" fill="url(#paint0_linear_2474_252377)"/>
<path d="m190.75 62.288c0.237-0.1035 0.47-0.2069 0.698-0.3097l-11.136 16.171-1.02 1.4814 1.641 0.7374 12.308 5.5325-8.682 7.4618-1.406 1.2087 13.723 10.439-15.827 5.575-3.004 1.058 11.782 7.089-2.265 10.45-0.11 0.504 0.224 0.465 4.49 9.325c-0.401-0.252-0.818-0.509-1.25-0.766-3.376-2.005-7.926-4.21-11.862-4.21h-163.56v-68.998l0.2749 4e-4 5.029 0.0069c4.3361 0.0058 10.538 0.0139 18.003 0.0228 14.929 0.018 34.907 0.0394 55.103 0.0538 40.363 0.0286 81.673 0.0289 85.197-0.0868 3.794-0.1246 8.256-1.7286 11.646-3.2108z" stroke="#000" stroke-opacity=".12" stroke-width="3"/>
<path d="m218 57s13.5 6.895 18.5 7 149.5 0 149.5 0v72s-142-0.086-149.5 0-18.5 5-18.5 5l6-14-15-14 16.5-5-7.5-6 6-13-18-13 19.5-4.5-7.5-14.5z" fill="#fff" tb:tag="pipe-background"/>
<path d="m218 57s13.5 6.895 18.5 7 149.5 0 149.5 0v72s-142-0.086-149.5 0-18.5 5-18.5 5l6-14-15-14 16.5-5-7.5-6 6-13-18-13 19.5-4.5-7.5-14.5z" fill="url(#paint1_linear_2474_252377)"/>
<path d="m384.5 65.501v68.998h-0.096l-4.519-3e-3c-3.898-2e-3 -9.474-5e-3 -16.189-8e-3 -13.43-7e-3 -31.414-0.015-49.633-0.02-36.428-0.011-73.818-0.011-77.58 0.032-3.999 0.046-8.811 1.363-12.523 2.61-1.157 0.389-2.223 0.777-3.144 1.127l4.563-10.646 0.415-0.969-0.771-0.719-13.096-12.223 14.008-4.244 2.727-0.827-2.225-1.78-6.583-5.267 5.508-11.933 0.518-1.1215-1.002-0.7231-15.321-11.065 16.28-3.757 1.883-0.4345-0.888-1.7162-5.428-10.495c1.018 0.4748 2.167 0.9969 3.374 1.5206 2.019 0.877 4.219 1.7672 6.244 2.4474 1.982 0.6654 3.95 1.1843 5.446 1.2158 2.522 0.0529 39.926 0.0528 76.658 0.0397 18.375-0.0066 36.594-0.0164 50.219-0.0246 6.812-0.0041 12.476-0.0078 16.437-0.0105l4.595-0.0031 0.123-1e-4z" stroke="#000" stroke-opacity=".12" stroke-width="3"/>
<rect x="1.5" y="51.5" width="11" height="97" rx="5.5" fill="#D9D9D9" stroke="#727171" stroke-width="3"/>
<rect x="387.5" y="51.5" width="11" height="97" rx="5.5" fill="#D9D9D9" stroke="#727171" stroke-width="3"/>
</g><defs>
<linearGradient id="paint0_linear_2474_252377" x1="32.929" x2="32.48" y1="64" y2="136" gradientUnits="userSpaceOnUse">
<stop stop-color="#727171" offset="0"/>
<stop stop-color="#727171" stop-opacity=".35" offset=".26388"/>
<stop stop-color="#727171" stop-opacity=".1" offset=".41759"/>
<stop stop-color="#fff" stop-opacity="0" offset=".49829"/>
<stop stop-color="#727171" stop-opacity=".1" offset=".58094"/>
<stop stop-color="#727171" stop-opacity=".35" offset=".71855"/>
<stop stop-color="#727171" offset="1"/>
</linearGradient>
<linearGradient id="paint1_linear_2474_252377" x1="329.76" x2="329.33" y1="64" y2="136" gradientUnits="userSpaceOnUse">
<stop stop-color="#727171" offset="0"/>
<stop stop-color="#727171" stop-opacity=".35" offset=".26388"/>
<stop stop-color="#727171" stop-opacity=".1" offset=".41759"/>
<stop stop-color="#fff" stop-opacity="0" offset=".49829"/>
<stop stop-color="#727171" stop-opacity=".1" offset=".58094"/>
<stop stop-color="#727171" stop-opacity=".35" offset=".71855"/>
<stop stop-color="#727171" offset="1"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

97
application/src/main/data/json/system/scada_symbols/long-vertical-broken-pipe.svg

@ -0,0 +1,97 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="400" fill="none" version="1.1" viewBox="0 0 200 400"><tb:metadata xmlns=""><![CDATA[{
"title": "Long vertical broken pipe",
"description": "Long vertical broken pipe.",
"searchTags": [
"long pipe",
"vertical pipe",
"broken pipe"
],
"widgetSizeX": 1,
"widgetSizeY": 2,
"tags": [
{
"tag": "clickArea",
"stateRenderFunction": null,
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'click');"
}
}
},
{
"tag": "pipe-background",
"stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});",
"actions": null
}
],
"behavior": [
{
"id": "click",
"name": "{i18n:scada.symbol.on-click}",
"hint": "{i18n:scada.symbol.on-click-hint}",
"group": null,
"type": "widgetAction",
"valueType": "BOOLEAN",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": null,
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": {
"type": "doNothing",
"targetDashboardStateId": null,
"openRightLayout": false,
"setEntityId": false,
"stateEntityParamName": null
}
}
],
"properties": [
{
"id": "pipeColor",
"name": "{i18n:scada.symbol.pipe-color}",
"type": "color",
"default": "#FFFFFF",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
]
}]]></tb:metadata>
<g tb:tag="clickArea">
<path d="m64 386s0.2293-158.08 0-165.06c-0.2293-6.982-6-16.955-6-16.955l21 14.461 6.5-14.461 9 10.472 11-14.461 6.5 18.45 6-9.973 11.5 2.494 14.5-6.982s-8 9.973-8 16.955v165.06h-72z" fill="#fff" tb:tag="pipe-background"/>
<path d="m64 386s0.2293-158.08 0-165.06c-0.2293-6.982-6-16.955-6-16.955l21 14.461 6.5-14.461 9 10.472 11-14.461 6.5 18.45 6-9.973 11.5 2.494 14.5-6.982s-8 9.973-8 16.955v165.06h-72z" fill="url(#paint0_linear_2474_252379)"/>
<path d="m62.288 209.25c-0.1035-0.237-0.2069-0.47-0.3097-0.698l16.171 11.136 1.4814 1.02 0.7374-1.641 5.5325-12.308 7.4618 8.682 1.2087 1.406 10.439-13.723 5.575 15.827 1.058 3.004 7.089-11.782 10.45 2.265 0.504 0.11 0.465-0.224 9.325-4.49c-0.252 0.401-0.509 0.818-0.766 1.25-2.005 3.376-4.21 7.926-4.21 11.862v163.56h-68.998l4e-4 -0.275 0.0069-5.029c0.0058-4.336 0.0139-10.538 0.0228-18.003 0.018-14.928 0.0394-34.906 0.0538-55.102 0.0286-40.363 0.0289-81.673-0.0868-85.197-0.1246-3.794-1.7286-8.256-3.2108-11.646z" stroke="#000" stroke-opacity=".12" stroke-width="3"/>
<path d="m57 182s6.895-13.5 7-18.5 0-149.5 0-149.5h72s-0.086 142 0 149.5 5 18.5 5 18.5l-14-6-14 15-5-16.5-6 7.5-13-6-13 18-4.5-19.5-14.5 7.5z" fill="#fff" tb:tag="pipe-background"/>
<path d="m57 182s6.895-13.5 7-18.5 0-149.5 0-149.5h72s-0.086 142 0 149.5 5 18.5 5 18.5l-14-6-14 15-5-16.5-6 7.5-13-6-13 18-4.5-19.5-14.5 7.5z" fill="url(#paint1_linear_2474_252379)"/>
<path d="m65.501 15.5h68.998v0.0957l-3e-3 4.5197c-2e-3 3.8974-5e-3 9.4736-8e-3 16.188-7e-3 13.43-0.015 31.414-0.02 49.633-0.011 36.428-0.011 73.818 0.032 77.58 0.046 3.999 1.363 8.811 2.61 12.523 0.389 1.157 0.777 2.223 1.127 3.144l-10.646-4.563-0.969-0.415-0.719 0.771-12.223 13.096-4.244-14.008-0.827-2.727-1.78 2.225-5.267 6.583-11.933-5.508-1.1215-0.518-0.7231 1.002-11.065 15.321-3.757-16.28-0.4345-1.883-1.7162 0.888-10.495 5.428c0.4748-1.018 0.9969-2.167 1.5206-3.374 0.877-2.019 1.7672-4.219 2.4474-6.244 0.6654-1.982 1.1843-3.95 1.2158-5.446 0.0529-2.522 0.0528-39.926 0.0397-76.658-0.0066-18.375-0.0164-36.594-0.0246-50.219-0.0041-6.8125-0.0078-12.477-0.0105-16.438l-0.0031-4.5947-1e-4 -0.123z" stroke="#000" stroke-opacity=".12" stroke-width="3"/>
<rect transform="rotate(-90 51.5 398.5)" x="51.5" y="398.5" width="11" height="97" rx="5.5" fill="#D9D9D9" stroke="#727171" stroke-width="3"/>
<rect transform="rotate(-90 51.5 12.5)" x="51.5" y="12.5" width="11" height="97" rx="5.5" fill="#D9D9D9" stroke="#727171" stroke-width="3"/>
</g><defs>
<linearGradient id="paint0_linear_2474_252379" x1="64" x2="136" y1="367.07" y2="367.52" gradientUnits="userSpaceOnUse">
<stop stop-color="#727171" offset="0"/>
<stop stop-color="#727171" stop-opacity=".35" offset=".26388"/>
<stop stop-color="#727171" stop-opacity=".1" offset=".41759"/>
<stop stop-color="#fff" stop-opacity="0" offset=".49829"/>
<stop stop-color="#727171" stop-opacity=".1" offset=".58094"/>
<stop stop-color="#727171" stop-opacity=".35" offset=".71855"/>
<stop stop-color="#727171" offset="1"/>
</linearGradient>
<linearGradient id="paint1_linear_2474_252379" x1="64" x2="136" y1="70.24" y2="70.67" gradientUnits="userSpaceOnUse">
<stop stop-color="#727171" offset="0"/>
<stop stop-color="#727171" stop-opacity=".35" offset=".26388"/>
<stop stop-color="#727171" stop-opacity=".1" offset=".41759"/>
<stop stop-color="#fff" stop-opacity="0" offset=".49829"/>
<stop stop-color="#727171" stop-opacity=".1" offset=".58094"/>
<stop stop-color="#727171" stop-opacity=".35" offset=".71855"/>
<stop stop-color="#727171" offset="1"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

1376
application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 100 KiB

7
application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg

@ -37,6 +37,11 @@
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 23;\n var majorIntervalLength = 560 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(268, y, 300, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 258, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(280, minorY, 300, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 23;\n var majorIntervalLength = 560 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(268, y, 300, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 258, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(280, minorY, 300, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{
"tag": "scale-background",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n}",
"actions": null
},
{ {
"tag": "top-layer", "tag": "top-layer",
"stateRenderFunction": "if (ctx.properties.transparent || !ctx.properties.scale) {\n element.hide();\n}", "stateRenderFunction": "if (ctx.properties.transparent || !ctx.properties.scale) {\n element.hide();\n}",
@ -556,7 +561,7 @@
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<circle cx="300" cy="300" r="300" fill="#E5E5E5" tb:tag="background"/><circle cx="300" cy="300" r="300" fill="url(#paint0_radial_1711_268272)"/><circle cx="300" cy="300" r="298.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><circle cx="300" cy="300" r="300" fill="#4A4848" fill-opacity=".5"/><mask id="mask0_1711_268272" x="16" y="16" width="568" height="568" style="mask-type:alpha" maskUnits="userSpaceOnUse"> <circle cx="300" cy="300" r="300" fill="#E5E5E5" tb:tag="background"/><circle cx="300" cy="300" r="300" fill="url(#paint0_radial_1711_268272)"/><circle cx="300" cy="300" r="298.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><circle cx="300" cy="300" r="300" fill="#4A4848" fill-opacity=".5" tb:tag="scale-background"/><mask id="mask0_1711_268272" x="16" y="16" width="568" height="568" style="mask-type:alpha" maskUnits="userSpaceOnUse">
<circle cx="300" cy="300" r="284" fill="#D9D9D9"/> <circle cx="300" cy="300" r="284" fill="#D9D9D9"/>
</mask><g mask="url(#mask0_1711_268272)"> </mask><g mask="url(#mask0_1711_268272)">
<rect transform="scale(1,-1)" y="-584" width="600" height="200" fill="#1ec1f4" fill-opacity=".5" tb:tag="fluid-background"/> <rect transform="scale(1,-1)" y="-584" width="600" height="200" fill="#1ec1f4" fill-opacity=".5" tb:tag="fluid-background"/>

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

7
application/src/main/data/json/system/scada_symbols/spherical-tank.svg

@ -37,6 +37,11 @@
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 23;\n var majorIntervalLength = 960 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(458, y, 490, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 448, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(470, minorY, 490, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 23;\n var majorIntervalLength = 960 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(458, y, 490, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 448, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(470, minorY, 490, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{
"tag": "scale-background",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n}",
"actions": null
},
{ {
"tag": "top-layer", "tag": "top-layer",
"stateRenderFunction": "if (ctx.properties.transparent || !ctx.properties.scale) {\n element.hide();\n}", "stateRenderFunction": "if (ctx.properties.transparent || !ctx.properties.scale) {\n element.hide();\n}",
@ -556,7 +561,7 @@
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<path d="m1e3 500c0 276.14-223.86 500-500 500s-500-223.86-500-500 223.86-500 500-500 500 223.86 500 500z" fill="#E5E5E5" tb:tag="background"/><path d="m1e3 500c0 276.14-223.86 500-500 500s-500-223.86-500-500 223.86-500 500-500 500 223.86 500 500z" fill="url(#paint0_radial_1711_251491)"/><path d="m998.5 500c0 275.31-223.19 498.5-498.5 498.5s-498.5-223.19-498.5-498.5 223.19-498.5 498.5-498.5 498.5 223.19 498.5 498.5z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><circle cx="500" cy="500" r="500" fill="#4A4848" fill-opacity=".5"/><mask id="mask0_1711_251491" x="16" y="16" width="968" height="968" style="mask-type:alpha" maskUnits="userSpaceOnUse"> <path d="m1e3 500c0 276.14-223.86 500-500 500s-500-223.86-500-500 223.86-500 500-500 500 223.86 500 500z" fill="#E5E5E5" tb:tag="background"/><path d="m1e3 500c0 276.14-223.86 500-500 500s-500-223.86-500-500 223.86-500 500-500 500 223.86 500 500z" fill="url(#paint0_radial_1711_251491)"/><path d="m998.5 500c0 275.31-223.19 498.5-498.5 498.5s-498.5-223.19-498.5-498.5 223.19-498.5 498.5-498.5 498.5 223.19 498.5 498.5z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><circle cx="500" cy="500" r="500" fill="#4A4848" fill-opacity=".5" tb:tag="scale-background"/><mask id="mask0_1711_251491" x="16" y="16" width="968" height="968" style="mask-type:alpha" maskUnits="userSpaceOnUse">
<circle cx="500" cy="500" r="484" fill="#D9D9D9"/> <circle cx="500" cy="500" r="484" fill="#D9D9D9"/>
</mask><g mask="url(#mask0_1711_251491)"> </mask><g mask="url(#mask0_1711_251491)">
<rect transform="scale(1,-1)" x="8" y="-984" width="984" height="200" fill="#1EC1F4" fill-opacity=".5" tb:tag="fluid-background"/> <rect transform="scale(1,-1)" x="8" y="-984" width="984" height="200" fill="#1EC1F4" fill-opacity=".5" tb:tag="fluid-background"/>

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

7
application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg

@ -37,6 +37,11 @@
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 17;\n var majorIntervalLength = 568 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(715, y, 747, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 705, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(727, minorY, 747, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 17;\n var majorIntervalLength = 568 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(715, y, 747, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (100 - i * (100/majorIntervals)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 705, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(727, minorY, 747, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{
"tag": "scale-background",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n}",
"actions": null
},
{ {
"tag": "top-layer", "tag": "top-layer",
"stateRenderFunction": "if (ctx.properties.transparent || !ctx.properties.scale) {\n element.hide();\n}", "stateRenderFunction": "if (ctx.properties.transparent || !ctx.properties.scale) {\n element.hide();\n}",
@ -556,7 +561,7 @@
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<path d="m174 600c-174 0-174-151.63-174-239 4.9551e-6 -1.386 9.6938e-6 -2.741 1.4223e-5 -4.065 1.18e-8 -4e-3 -1.4223e-5 -113.93-1.4223e-5 -113.94 0-87.373-8.3346e-7 -243 174-243h653.5c173 0 172.5 156.06 172.5 243v118c0 86.939 0 239-172.5 239h-653.5z" fill="#E5E5E5" tb:tag="background"/><path d="m174 600c-174 0-174-151.63-174-239 4.9551e-6 -1.386 9.6938e-6 -2.741 1.4223e-5 -4.065 1.18e-8 -4e-3 -1.4223e-5 -113.93-1.4223e-5 -113.94 0-87.373-8.3346e-7 -243 174-243h653.5c173 0 172.5 156.06 172.5 243v118c0 86.939 0 239-172.5 239h-653.5z" fill="url(#paint0_linear_1694_158298)"/><path d="m177.27 0.4874c198.56-1.0967 560.65 1.9e-5 640.18 0 79.522-1.9e-5 183.55 15 183.55 220v169c0 192.5-105.53 211.5-184.55 211.5s-516.64-0.5-639.18-0.5c-122.53 0-176.55-68.001-176.55-219 5.26e-4 -151-6.59e-4 -49.502-2.47e-4 -119s-22.006-260.91 176.55-262.01z" fill="#4A4848" fill-opacity=".5"/><mask id="mask0_1694_158298" x="17" y="16" width="968" height="570" style="mask-type:alpha" maskUnits="userSpaceOnUse"> <path d="m174 600c-174 0-174-151.63-174-239 4.9551e-6 -1.386 9.6938e-6 -2.741 1.4223e-5 -4.065 1.18e-8 -4e-3 -1.4223e-5 -113.93-1.4223e-5 -113.94 0-87.373-8.3346e-7 -243 174-243h653.5c173 0 172.5 156.06 172.5 243v118c0 86.939 0 239-172.5 239h-653.5z" fill="#E5E5E5" tb:tag="background"/><path d="m174 600c-174 0-174-151.63-174-239 4.9551e-6 -1.386 9.6938e-6 -2.741 1.4223e-5 -4.065 1.18e-8 -4e-3 -1.4223e-5 -113.93-1.4223e-5 -113.94 0-87.373-8.3346e-7 -243 174-243h653.5c173 0 172.5 156.06 172.5 243v118c0 86.939 0 239-172.5 239h-653.5z" fill="url(#paint0_linear_1694_158298)"/><path d="m177.27 0.4874c198.56-1.0967 560.65 1.9e-5 640.18 0 79.522-1.9e-5 183.55 15 183.55 220v169c0 192.5-105.53 211.5-184.55 211.5s-516.64-0.5-639.18-0.5c-122.53 0-176.55-68.001-176.55-219 5.26e-4 -151-6.59e-4 -49.502-2.47e-4 -119s-22.006-260.91 176.55-262.01z" fill="#4A4848" fill-opacity=".5" tb:tag="scale-background"/><mask id="mask0_1694_158298" x="17" y="16" width="968" height="570" style="mask-type:alpha" maskUnits="userSpaceOnUse">
<path d="m178.68 16h654.82c117.67 0 151.5 98.586 151.5 184.16v204.18c0 135.62-68.502 181.66-162.5 181.66h-627.98c-129 0-177.5-66.558-177.5-181.66s0.0023-129.32 0-212.19c-3e-4 -9.509-5.4994-176.16 161.66-176.16z" fill="#D9D9D9"/> <path d="m178.68 16h654.82c117.67 0 151.5 98.586 151.5 184.16v204.18c0 135.62-68.502 181.66-162.5 181.66h-627.98c-129 0-177.5-66.558-177.5-181.66s0.0023-129.32 0-212.19c-3e-4 -9.509-5.4994-176.16 161.66-176.16z" fill="#D9D9D9"/>
</mask><g mask="url(#mask0_1694_158298)"> </mask><g mask="url(#mask0_1694_158298)">
<rect transform="scale(1,-1)" x="9" y="-585" width="984" height="200" fill="#1EC1F4" fill-opacity=".5" tb:tag="fluid-background"/> <rect transform="scale(1,-1)" x="9" y="-585" width="984" height="200" fill="#1EC1F4" fill-opacity=".5" tb:tag="fluid-background"/>

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 122 KiB

97
application/src/main/data/json/system/scada_symbols/vertical-broken-pipe.svg

@ -0,0 +1,97 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="200" fill="none" version="1.1" viewBox="0 0 200 200"><tb:metadata xmlns=""><![CDATA[{
"title": "Vertical broken pipe",
"description": "Vertical broken pipe.",
"searchTags": [
"pipe",
"vertical pipe",
"broken pipe"
],
"widgetSizeX": 1,
"widgetSizeY": 1,
"tags": [
{
"tag": "clickArea",
"stateRenderFunction": null,
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'click');"
}
}
},
{
"tag": "pipe-background",
"stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});",
"actions": null
}
],
"behavior": [
{
"id": "click",
"name": "{i18n:scada.symbol.on-click}",
"hint": "{i18n:scada.symbol.on-click-hint}",
"group": null,
"type": "widgetAction",
"valueType": "BOOLEAN",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": null,
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": {
"type": "doNothing",
"targetDashboardStateId": null,
"openRightLayout": false,
"setEntityId": false,
"stateEntityParamName": null
}
}
],
"properties": [
{
"id": "pipeColor",
"name": "{i18n:scada.symbol.pipe-color}",
"type": "color",
"default": "#FFFFFF",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
]
}]]></tb:metadata>
<g tb:tag="clickArea">
<path d="m63.5 186.5s0.2293-53.5 0-60.5-6.5-17-6.5-17l21.5 14.5 6.5-14.5 9 10.5 11-14.5 6.5 18.5 6-10 11.5 2.5 14-7s-7.5 10-7.5 17v60.5h-72z" fill="#fff" tb:tag="pipe-background"/>
<path d="m63.5 186.5s0.2293-53.5 0-60.5-6.5-17-6.5-17l21.5 14.5 6.5-14.5 9 10.5 11-14.5 6.5 18.5 6-10 11.5 2.5 14-7s-7.5 10-7.5 17v60.5h-72z" fill="url(#paint0_linear_2474_252378)"/>
<path d="m61.52 114.23c-0.0889-0.188-0.1777-0.374-0.2661-0.556l16.407 11.066 1.4782 0.997 0.7293-1.627 5.5321-12.341 7.4602 8.703 1.2101 1.412 1.1238-1.481 9.314-12.278 5.576 15.868 1.058 3.013 1.643-2.738 5.447-9.078 10.448 2.272 0.517 0.112 0.473-0.236 9.023-4.512c-0.249 0.421-0.502 0.861-0.755 1.317-1.883 3.389-3.939 7.935-3.939 11.853v59h-68.994l0.0033-0.807c0.0058-1.476 0.0139-3.594 0.0228-6.157 0.0179-5.125 0.0394-12.032 0.0538-19.158 0.0286-14.23 0.0289-29.393-0.0868-32.927-0.1255-3.83-1.8722-8.32-3.4792-11.717z" stroke="#000" stroke-opacity=".12" stroke-width="3"/>
<path d="m58 94.5s7.395-13.5 7.5-18.5 0-62 0-62h72s-0.086 54.5 0 62 4.5 18.5 4.5 18.5l-13.5-6-14 15-5-16.5-6 7.5-13-6-13 18-4.5-19.5-15 7.5z" fill="#fff" tb:tag="pipe-background"/>
<path d="m58 94.5s7.395-13.5 7.5-18.5 0-62 0-62h72s-0.086 54.5 0 62 4.5 18.5 4.5 18.5l-13.5-6-14 15-5-16.5-6 7.5-13-6-13 18-4.5-19.5-15 7.5z" fill="url(#paint1_linear_2474_252378)"/>
<path d="m67.003 15.5h68.995l-2e-3 0.8543c-2e-3 1.5049-5e-3 3.6632-8e-3 6.2766-7e-3 5.2267-0.015 12.274-0.02 19.555-0.011 14.556-0.011 30.069 0.032 33.831 0.046 3.9731 1.225 8.7675 2.346 12.479 0.357 1.1792 0.712 2.2636 1.031 3.1964l-10.268-4.5635-0.976-0.4341-0.73 0.7813-12.223 13.096-4.244-14.008-0.827-2.7275-1.78 2.2255-5.267 6.5829-13.055-6.0254-0.7231 1.0013-11.065 15.321-3.757-16.28-0.4284-1.8563-12.574 6.2868c0.5039-1.009 1.0566-2.1442 1.611-3.3352 0.9404-2.0204 1.8953-4.222 2.6248-6.2504 0.7123-1.9804 1.2726-3.9616 1.3044-5.4761 0.0529-2.5216 0.0528-18.048 0.0397-32.908-0.0066-7.4379-0.0164-14.72-0.0246-20.142-0.0041-2.711-0.0078-4.9572-0.0105-6.5257l-0.0017-0.9564z" stroke="#000" stroke-opacity=".12" stroke-width="3"/>
<rect transform="rotate(-90 51.5 198.5)" x="51.5" y="198.5" width="11" height="97" rx="5.5" fill="#D9D9D9" stroke="#727171" stroke-width="3"/>
<rect transform="rotate(-90 51.5 12.5)" x="51.5" y="12.5" width="11" height="97" rx="5.5" fill="#D9D9D9" stroke="#727171" stroke-width="3"/>
</g><defs>
<linearGradient id="paint0_linear_2474_252378" x1="63.5" x2="135.5" y1="167.52" y2="167.97" gradientUnits="userSpaceOnUse">
<stop stop-color="#727171" offset="0"/>
<stop stop-color="#727171" stop-opacity=".35" offset=".26388"/>
<stop stop-color="#727171" stop-opacity=".1" offset=".41759"/>
<stop stop-color="#fff" stop-opacity="0" offset=".49829"/>
<stop stop-color="#727171" stop-opacity=".1" offset=".58094"/>
<stop stop-color="#727171" stop-opacity=".35" offset=".71855"/>
<stop stop-color="#727171" offset="1"/>
</linearGradient>
<linearGradient id="paint1_linear_2474_252378" x1="65.5" x2="137.5" y1="70.24" y2="70.67" gradientUnits="userSpaceOnUse">
<stop stop-color="#727171" offset="0"/>
<stop stop-color="#727171" stop-opacity=".35" offset=".26388"/>
<stop stop-color="#727171" stop-opacity=".1" offset=".41759"/>
<stop stop-color="#fff" stop-opacity="0" offset=".49829"/>
<stop stop-color="#727171" stop-opacity=".1" offset=".58094"/>
<stop stop-color="#727171" stop-opacity=".35" offset=".71855"/>
<stop stop-color="#727171" offset="1"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

9
application/src/main/data/json/system/widget_bundles/scada_fluid_system.json

@ -11,8 +11,10 @@
"widgetTypeFqns": [ "widgetTypeFqns": [
"horizontal_pipe", "horizontal_pipe",
"long_horizontal_pipe", "long_horizontal_pipe",
"extra_long_horizontal_pipe",
"vertical_pipe", "vertical_pipe",
"long_vertical_pipe", "long_vertical_pipe",
"extra_long_vertical_pipe",
"left_bottom_elbow_pipe", "left_bottom_elbow_pipe",
"bottom_right_elbow_pipe", "bottom_right_elbow_pipe",
"top_right_elbow_pipe", "top_right_elbow_pipe",
@ -28,6 +30,10 @@
"right_drain_pipe", "right_drain_pipe",
"short_left_drain_pipe", "short_left_drain_pipe",
"short_right_drain_pipe", "short_right_drain_pipe",
"horizontal_broken_pipe",
"vertical_broken_pipe",
"long_horizontal_broken_pipe",
"long_vertical_broken_pipe",
"top_flow_meter", "top_flow_meter",
"right_flow_meter", "right_flow_meter",
"bottom_flow_meter", "bottom_flow_meter",
@ -58,6 +64,7 @@
"stand_vertical_tank", "stand_vertical_tank",
"cylindrical_tank", "cylindrical_tank",
"stand_cylindrical_tank", "stand_cylindrical_tank",
"small_cylindrical_tank",
"vertical_short_tank", "vertical_short_tank",
"stand_vertical_short_tank", "stand_vertical_short_tank",
"large_cylindrical_tank", "large_cylindrical_tank",
@ -68,6 +75,8 @@
"stand_horizontal_tank", "stand_horizontal_tank",
"spherical_tank", "spherical_tank",
"small_spherical_tank", "small_spherical_tank",
"conical_tank",
"large_conical_tank",
"elevated_tank", "elevated_tank",
"pool" "pool"
] ]

2
application/src/main/data/json/system/widget_types/rpc_remote_shell.json

File diff suppressed because one or more lines are too long

7023
application/src/main/data/json/tenant/dashboards/gateways.json

File diff suppressed because it is too large

7
application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DashboardSyncService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.entitiy.dashboard;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -39,6 +40,7 @@ import java.util.stream.Stream;
@TbCoreComponent @TbCoreComponent
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j @Slf4j
@ConditionalOnProperty(value = "transport.gateway.dashboard.sync.enabled", havingValue = "true")
public class DashboardSyncService { public class DashboardSyncService {
private final GitSyncService gitSyncService; private final GitSyncService gitSyncService;
@ -46,8 +48,6 @@ public class DashboardSyncService {
private final WidgetsBundleService widgetsBundleService; private final WidgetsBundleService widgetsBundleService;
private final PartitionService partitionService; private final PartitionService partitionService;
@Value("${transport.gateway.dashboard.sync.enabled:true}")
private boolean enabled;
@Value("${transport.gateway.dashboard.sync.repository_url:}") @Value("${transport.gateway.dashboard.sync.repository_url:}")
private String repoUrl; private String repoUrl;
@Value("${transport.gateway.dashboard.sync.branch:main}") @Value("${transport.gateway.dashboard.sync.branch:main}")
@ -60,9 +60,6 @@ public class DashboardSyncService {
@AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE)
public void init() throws Exception { public void init() throws Exception {
if (!enabled) {
return;
}
gitSyncService.registerSync(REPO_KEY, repoUrl, branch, TimeUnit.HOURS.toMillis(fetchFrequencyHours), this::update); gitSyncService.registerSync(REPO_KEY, repoUrl, branch, TimeUnit.HOURS.toMillis(fetchFrequencyHours), this::update);
} }

76
application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java

@ -19,7 +19,6 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -54,8 +53,8 @@ import org.thingsboard.server.service.install.update.ImagesUpdater;
import java.io.IOException; import java.io.IOException;
import java.io.UncheckedIOException; import java.io.UncheckedIOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
@ -170,7 +169,6 @@ public class InstallScripts {
loadRuleChainsFromPath(tenantId, edgeChainsDir); loadRuleChainsFromPath(tenantId, edgeChainsDir);
} }
@SneakyThrows
private void loadRuleChainsFromPath(TenantId tenantId, Path ruleChainsPath) { private void loadRuleChainsFromPath(TenantId tenantId, Path ruleChainsPath) {
findRuleChainsFromPath(ruleChainsPath).forEach(path -> { findRuleChainsFromPath(ruleChainsPath).forEach(path -> {
try { try {
@ -182,12 +180,10 @@ public class InstallScripts {
}); });
} }
List<Path> findRuleChainsFromPath(Path ruleChainsPath) throws IOException { List<Path> findRuleChainsFromPath(Path ruleChainsPath) {
List<Path> paths = new ArrayList<>(); try (Stream<Path> files = listDir(ruleChainsPath).filter(path -> path.toString().endsWith(InstallScripts.JSON_EXT))) {
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(ruleChainsPath, path -> path.toString().endsWith(InstallScripts.JSON_EXT))) { return files.toList();
dirStream.forEach(paths::add);
} }
return paths;
} }
public RuleChain createDefaultRuleChain(TenantId tenantId, String ruleChainName) { public RuleChain createDefaultRuleChain(TenantId tenantId, String ruleChainName) {
@ -211,11 +207,11 @@ public class InstallScripts {
return ruleChain; return ruleChain;
} }
public void loadSystemWidgets() throws Exception { public void loadSystemWidgets() {
log.info("Loading system widgets"); log.info("Loading system widgets");
Map<Path, JsonNode> widgetsBundlesMap = new HashMap<>(); Map<Path, JsonNode> widgetsBundlesMap = new HashMap<>();
Path widgetBundlesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR); Path widgetBundlesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR);
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(widgetBundlesDir, path -> path.toString().endsWith(JSON_EXT))) { try (Stream<Path> dirStream = listDir(widgetBundlesDir).filter(path -> path.toString().endsWith(JSON_EXT))) {
dirStream.forEach( dirStream.forEach(
path -> { path -> {
JsonNode widgetsBundleDescriptorJson; JsonNode widgetsBundleDescriptorJson;
@ -247,12 +243,14 @@ public class InstallScripts {
} }
Path widgetTypesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_TYPES_DIR); Path widgetTypesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_TYPES_DIR);
if (Files.exists(widgetTypesDir)) { if (Files.exists(widgetTypesDir)) {
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(widgetTypesDir, path -> path.toString().endsWith(JSON_EXT))) { try (Stream<Path> dirStream = listDir(widgetTypesDir).filter(path -> path.toString().endsWith(JSON_EXT))) {
dirStream.forEach( dirStream.forEach(
path -> { path -> {
try { try {
JsonNode widgetTypeJson = JacksonUtil.toJsonNode(path.toFile()); String widgetTypeJson = Files.readString(path);
WidgetTypeDetails widgetTypeDetails = JacksonUtil.treeToValue(widgetTypeJson, WidgetTypeDetails.class); widgetTypeJson = resourceService.checkSystemResourcesUsage(widgetTypeJson, ResourceType.JS_MODULE);
WidgetTypeDetails widgetTypeDetails = JacksonUtil.fromString(widgetTypeJson, WidgetTypeDetails.class);
widgetTypeService.saveWidgetType(widgetTypeDetails); widgetTypeService.saveWidgetType(widgetTypeDetails);
} catch (Exception e) { } catch (Exception e) {
log.error("Unable to load widget type from json: [{}]", path.toString()); log.error("Unable to load widget type from json: [{}]", path.toString());
@ -300,12 +298,12 @@ public class InstallScripts {
} }
} }
private void loadSystemScadaSymbols() throws Exception { private void loadSystemScadaSymbols() {
log.info("Loading system SCADA symbols"); log.info("Loading system SCADA symbols");
Path scadaSymbolsDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, SCADA_SYMBOLS_DIR); Path scadaSymbolsDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, SCADA_SYMBOLS_DIR);
if (Files.exists(scadaSymbolsDir)) { if (Files.exists(scadaSymbolsDir)) {
WidgetTypeDetails scadaSymbolWidgetTemplate = widgetTypeService.findWidgetTypeDetailsByTenantIdAndFqn(TenantId.SYS_TENANT_ID, "scada_symbol"); WidgetTypeDetails scadaSymbolWidgetTemplate = widgetTypeService.findWidgetTypeDetailsByTenantIdAndFqn(TenantId.SYS_TENANT_ID, "scada_symbol");
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(scadaSymbolsDir, path -> path.toString().endsWith(SVG_EXT))) { try (Stream<Path> dirStream = listDir(scadaSymbolsDir).filter(path -> path.toString().endsWith(SVG_EXT))) {
dirStream.forEach( dirStream.forEach(
path -> { path -> {
try { try {
@ -404,11 +402,10 @@ public class InstallScripts {
imagesUpdater.updateAssetProfilesImages(); imagesUpdater.updateAssetProfilesImages();
} }
@SneakyThrows
public void loadSystemImages() { public void loadSystemImages() {
log.info("Loading system images..."); log.info("Loading system images...");
Stream<Path> dashboardsFiles = Stream.concat(Files.list(Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, DASHBOARDS_DIR)), Stream<Path> dashboardsFiles = Stream.concat(listDir(Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, DASHBOARDS_DIR)),
Files.list(Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, DASHBOARDS_DIR))); listDir(Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, DASHBOARDS_DIR)));
try (dashboardsFiles) { try (dashboardsFiles) {
dashboardsFiles.forEach(file -> { dashboardsFiles.forEach(file -> {
try { try {
@ -431,25 +428,22 @@ public class InstallScripts {
loadDashboardsFromDir(tenantId, customerId, dashboardsDir); loadDashboardsFromDir(tenantId, customerId, dashboardsDir);
} }
@SneakyThrows
private void loadDashboardsFromDir(TenantId tenantId, CustomerId customerId, Path dashboardsDir) { private void loadDashboardsFromDir(TenantId tenantId, CustomerId customerId, Path dashboardsDir) {
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(dashboardsDir, path -> path.toString().endsWith(JSON_EXT))) { try (Stream<Path> dashboards = listDir(dashboardsDir).filter(path -> path.toString().endsWith(JSON_EXT))) {
dirStream.forEach( dashboards.forEach(path -> {
path -> { try {
try { JsonNode dashboardJson = JacksonUtil.toJsonNode(path.toFile());
JsonNode dashboardJson = JacksonUtil.toJsonNode(path.toFile()); Dashboard dashboard = JacksonUtil.treeToValue(dashboardJson, Dashboard.class);
Dashboard dashboard = JacksonUtil.treeToValue(dashboardJson, Dashboard.class); dashboard.setTenantId(tenantId);
dashboard.setTenantId(tenantId); Dashboard savedDashboard = dashboardService.saveDashboard(dashboard);
Dashboard savedDashboard = dashboardService.saveDashboard(dashboard); if (customerId != null && !customerId.isNullUid()) {
if (customerId != null && !customerId.isNullUid()) { dashboardService.assignDashboardToCustomer(TenantId.SYS_TENANT_ID, savedDashboard.getId(), customerId);
dashboardService.assignDashboardToCustomer(TenantId.SYS_TENANT_ID, savedDashboard.getId(), customerId);
}
} catch (Exception e) {
log.error("Unable to load dashboard from json: [{}]", path.toString());
throw new RuntimeException("Unable to load dashboard from json", e);
}
} }
); } catch (Exception e) {
log.error("Unable to load dashboard from json: [{}]", path.toString());
throw new RuntimeException("Unable to load dashboard from json", e);
}
});
} }
} }
@ -464,9 +458,9 @@ public class InstallScripts {
} }
} }
public void createOAuth2Templates() throws Exception { public void createOAuth2Templates() {
Path oauth2ConfigTemplatesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, OAUTH2_CONFIG_TEMPLATES_DIR); Path oauth2ConfigTemplatesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, OAUTH2_CONFIG_TEMPLATES_DIR);
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(oauth2ConfigTemplatesDir, path -> path.toString().endsWith(JSON_EXT))) { try (Stream<Path> dirStream = listDir(oauth2ConfigTemplatesDir).filter(path -> path.toString().endsWith(JSON_EXT))) {
dirStream.forEach( dirStream.forEach(
path -> { path -> {
try { try {
@ -489,7 +483,7 @@ public class InstallScripts {
public void loadSystemLwm2mResources() { public void loadSystemLwm2mResources() {
Path resourceLwm2mPath = Paths.get(getDataDir(), MODELS_LWM2M_DIR); Path resourceLwm2mPath = Paths.get(getDataDir(), MODELS_LWM2M_DIR);
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(resourceLwm2mPath, path -> path.toString().endsWith(InstallScripts.XML_EXT))) { try (Stream<Path> dirStream = listDir(resourceLwm2mPath).filter(path -> path.toString().endsWith(InstallScripts.XML_EXT))) {
dirStream.forEach( dirStream.forEach(
path -> { path -> {
try { try {
@ -539,9 +533,11 @@ public class InstallScripts {
} }
} }
private Stream<Path> listDir(Path resourcesDir) { private Stream<Path> listDir(Path dir) {
try { try {
return Files.list(resourcesDir); return Files.list(dir);
} catch (NoSuchFileException e) {
return Stream.empty();
} catch (IOException e) { } catch (IOException e) {
throw new UncheckedIOException(e); throw new UncheckedIOException(e);
} }

4
application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java

@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.notification.NotificationRequestConfig
import org.thingsboard.server.common.data.notification.NotificationRequestStats; import org.thingsboard.server.common.data.notification.NotificationRequestStats;
import org.thingsboard.server.common.data.notification.NotificationRequestStatus; import org.thingsboard.server.common.data.notification.NotificationRequestStatus;
import org.thingsboard.server.common.data.notification.NotificationStatus; import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.common.data.notification.info.GeneralNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings;
@ -187,7 +188,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
} }
@Override @Override
public void sendGeneralWebNotification(TenantId tenantId, UsersFilter recipients, NotificationTemplate template) { public void sendGeneralWebNotification(TenantId tenantId, UsersFilter recipients, NotificationTemplate template, GeneralNotificationInfo info) {
NotificationTarget target = new NotificationTarget(); NotificationTarget target = new NotificationTarget();
target.setTenantId(tenantId); target.setTenantId(tenantId);
PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig(); PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig();
@ -198,6 +199,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
.tenantId(tenantId) .tenantId(tenantId)
.template(template) .template(template)
.targets(List.of(EntityId.NULL_UUID)) // this is temporary and will be removed when 'create from scratch' functionality is implemented for recipients .targets(List.of(EntityId.NULL_UUID)) // this is temporary and will be removed when 'create from scratch' functionality is implemented for recipients
.info(info)
.status(NotificationRequestStatus.PROCESSING) .status(NotificationRequestStatus.PROCESSING)
.build(); .build();
try { try {

66
application/src/main/java/org/thingsboard/server/service/update/DeprecationService.java

@ -0,0 +1,66 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.update;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.info.GeneralNotificationInfo;
import org.thingsboard.server.common.data.notification.targets.platform.SystemAdministratorsFilter;
import org.thingsboard.server.dao.notification.DefaultNotifications;
import org.thingsboard.server.queue.util.AfterStartUp;
import java.util.Map;
@Service
@Slf4j
@RequiredArgsConstructor
public class DeprecationService {
private final NotificationCenter notificationCenter;
@Value("${queue.type}")
private String queueType;
@AfterStartUp(order = Integer.MAX_VALUE)
public void checkDeprecation() {
checkQueueTypeDeprecation();
}
private void checkQueueTypeDeprecation() {
String queueTypeName;
switch (queueType) {
case "aws-sqs" -> queueTypeName = "AWS SQS";
case "pubsub" -> queueTypeName = "PubSub";
case "service-bus" -> queueTypeName = "Azure Service Bus";
case "rabbitmq" -> queueTypeName = "RabbitMQ";
default -> {
return;
}
}
log.warn("WARNING: Starting with ThingsBoard 4.0, {} will no longer be supported as a message queue for microservices. " +
"Please migrate to Apache Kafka. This change will not impact any rule nodes", queueTypeName);
notificationCenter.sendGeneralWebNotification(TenantId.SYS_TENANT_ID, new SystemAdministratorsFilter(),
DefaultNotifications.queueTypeDeprecation.toTemplate(), new GeneralNotificationInfo(Map.of(
"queueType", queueTypeName
)));
}
}

4
application/src/main/resources/thingsboard.yml

@ -1450,7 +1450,9 @@ swagger:
# Queue configuration parameters # Queue configuration parameters
queue: queue:
type: "${TB_QUEUE_TYPE:in-memory}" # in-memory or kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) # in-memory or kafka (Apache Kafka). The following queue types are deprecated and will no longer be supported in ThingsBoard 4.0:
# aws-sqs (AWS SQS), pubsub (PubSub), service-bus (Azure Service Bus), rabbitmq (RabbitMQ)
type: "${TB_QUEUE_TYPE:in-memory}"
prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka).
in_memory: in_memory:
stats: stats:

19
application/src/test/java/org/thingsboard/server/service/entitiy/dashboard/DashboardSyncServiceTest.java

@ -15,13 +15,18 @@
*/ */
package org.thingsboard.server.service.entitiy.dashboard; package org.thingsboard.server.service.entitiy.dashboard;
import org.junit.After;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.sql.resource.TbResourceRepository;
import org.thingsboard.server.dao.sql.widget.WidgetTypeRepository;
import org.thingsboard.server.dao.sql.widget.WidgetsBundleRepository;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -35,6 +40,20 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
}) })
public class DashboardSyncServiceTest extends AbstractControllerTest { public class DashboardSyncServiceTest extends AbstractControllerTest {
@Autowired
private WidgetTypeRepository widgetTypeRepository;
@Autowired
private WidgetsBundleRepository widgetsBundleRepository;
@Autowired
private TbResourceRepository resourceRepository;
@After
public void after() throws Exception {
widgetsBundleRepository.deleteAll();
widgetTypeRepository.deleteAll();
resourceRepository.deleteAll();
}
@Test @Test
public void testGatewaysDashboardSync() throws Exception { public void testGatewaysDashboardSync() throws Exception {
loginTenantAdmin(); loginTenantAdmin();

5
application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java

@ -39,7 +39,6 @@ import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.service.install.update.ImagesUpdater; import org.thingsboard.server.service.install.update.ImagesUpdater;
import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -87,14 +86,14 @@ class InstallScriptsTest {
} }
@Test @Test
void testDefaultRuleChainsTemplates() throws IOException { void testDefaultRuleChainsTemplates() {
Path dir = installScripts.getTenantRuleChainsDir(); Path dir = installScripts.getTenantRuleChainsDir();
installScripts.findRuleChainsFromPath(dir) installScripts.findRuleChainsFromPath(dir)
.forEach(this::validateRuleChainTemplate); .forEach(this::validateRuleChainTemplate);
} }
@Test @Test
void testDefaultEdgeRuleChainsTemplates() throws IOException { void testDefaultEdgeRuleChainsTemplates() {
Path dir = installScripts.getEdgeRuleChainsDir(); Path dir = installScripts.getEdgeRuleChainsDir();
installScripts.findRuleChainsFromPath(dir) installScripts.findRuleChainsFromPath(dir)
.forEach(this::validateRuleChainTemplate); .forEach(this::validateRuleChainTemplate);

17
application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java

@ -56,6 +56,7 @@ import org.thingsboard.server.common.data.notification.NotificationRequestStatus
import org.thingsboard.server.common.data.notification.NotificationType; import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.info.AlarmCommentNotificationInfo; import org.thingsboard.server.common.data.notification.info.AlarmCommentNotificationInfo;
import org.thingsboard.server.common.data.notification.info.EntityActionNotificationInfo; import org.thingsboard.server.common.data.notification.info.EntityActionNotificationInfo;
import org.thingsboard.server.common.data.notification.info.GeneralNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmCommentNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmCommentNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.settings.MobileAppNotificationDeliveryMethodConfig; import org.thingsboard.server.common.data.notification.settings.MobileAppNotificationDeliveryMethodConfig;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
@ -84,6 +85,7 @@ import org.thingsboard.server.common.data.notification.template.WebDeliveryMetho
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.notification.DefaultNotifications; import org.thingsboard.server.dao.notification.DefaultNotifications;
import org.thingsboard.server.dao.notification.DefaultNotifications.DefaultNotification;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.service.notification.channels.MicrosoftTeamsNotificationChannel; import org.thingsboard.server.service.notification.channels.MicrosoftTeamsNotificationChannel;
import org.thingsboard.server.service.notification.channels.TeamsAdaptiveCard; import org.thingsboard.server.service.notification.channels.TeamsAdaptiveCard;
@ -746,14 +748,21 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
getAnotherWsClient().registerWaitForUpdate(); getAnotherWsClient().registerWaitForUpdate();
DefaultNotifications.DefaultNotification expectedNotification = DefaultNotifications.maintenanceWork; NotificationTemplate template = DefaultNotification.builder()
.name("Test")
.subject("Testing ${subjectVariable}")
.text("Testing ${bodyVariable}")
.build().toTemplate();
notificationCenter.sendGeneralWebNotification(TenantId.SYS_TENANT_ID, new SystemAdministratorsFilter(), notificationCenter.sendGeneralWebNotification(TenantId.SYS_TENANT_ID, new SystemAdministratorsFilter(),
expectedNotification.toTemplate()); template, new GeneralNotificationInfo(Map.of(
"subjectVariable", "subject",
"bodyVariable", "body"
)));
getAnotherWsClient().waitForUpdate(true); getAnotherWsClient().waitForUpdate(true);
Notification notification = getAnotherWsClient().getLastDataUpdate().getUpdate(); Notification notification = getAnotherWsClient().getLastDataUpdate().getUpdate();
assertThat(notification.getSubject()).isEqualTo(expectedNotification.getSubject()); assertThat(notification.getSubject()).isEqualTo("Testing subject");
assertThat(notification.getText()).isEqualTo(expectedNotification.getText()); assertThat(notification.getText()).isEqualTo("Testing body");
} }
@Test @Test

48
application/src/test/java/org/thingsboard/server/service/script/AbstractTbelInvokeTest.java

@ -0,0 +1,48 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.script;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.ScriptType;
import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.controller.AbstractControllerTest;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST;
public abstract class AbstractTbelInvokeTest extends AbstractControllerTest {
@Autowired
protected TbelInvokeService invokeService;
protected UUID evalScript(String script) throws ExecutionException, InterruptedException {
return invokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, script, "msg", "metadata", "msgType").get();
}
protected String invokeScriptResultString(UUID scriptId, String str) throws ExecutionException, InterruptedException {
var msg = JacksonUtil.fromString(str, Map.class);
return invokeScript(scriptId, str).toString();
}
protected Object invokeScript(UUID scriptId, String str) throws ExecutionException, InterruptedException {
var msg = JacksonUtil.fromString(str, Map.class);
return invokeService.invokeScript(TenantId.SYS_TENANT_ID, null, scriptId, msg, "{}", POST_TELEMETRY_REQUEST.name()).get();
}
}

2151
application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java

File diff suppressed because it is too large

35
application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java

@ -20,15 +20,10 @@ import com.github.benmanes.caffeine.cache.Cache;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.ScriptType;
import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.script.api.tbel.TbelScript; import org.thingsboard.script.api.tbel.TbelScript;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import java.io.Serializable; import java.io.Serializable;
@ -42,7 +37,6 @@ import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST;
@DaoSqlTest @DaoSqlTest
@TestPropertySource(properties = { @TestPropertySource(properties = {
@ -52,10 +46,7 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_RE
"tbel.max_errors=2", "tbel.max_errors=2",
"tbel.compiled_scripts_cache_size=100" "tbel.compiled_scripts_cache_size=100"
}) })
class TbelInvokeServiceTest extends AbstractControllerTest { class TbelInvokeServiceTest extends AbstractTbelInvokeTest {
@Autowired
private TbelInvokeService invokeService;
@Value("${tbel.max_errors}") @Value("${tbel.max_errors}")
private int maxJsErrors; private int maxJsErrors;
@ -69,14 +60,14 @@ class TbelInvokeServiceTest extends AbstractControllerTest {
for (int i = 0; i < 100; i++) { for (int i = 0; i < 100; i++) {
msg.put("temperature", i); msg.put("temperature", i);
boolean expected = i > 20; boolean expected = i > 20;
boolean result = Boolean.parseBoolean(invokeScript(scriptId, JacksonUtil.toString(msg))); boolean result = Boolean.parseBoolean(invokeScriptResultString(scriptId, JacksonUtil.toString(msg)));
Assert.assertEquals(expected, result); Assert.assertEquals(expected, result);
} }
long startTs = System.currentTimeMillis(); long startTs = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) { for (int i = 0; i < iterations; i++) {
msg.put("temperature", i); msg.put("temperature", i);
boolean expected = i > 20; boolean expected = i > 20;
boolean result = Boolean.parseBoolean(invokeScript(scriptId, JacksonUtil.toString(msg))); boolean result = Boolean.parseBoolean(invokeScriptResultString(scriptId, JacksonUtil.toString(msg)));
Assert.assertEquals(expected, result); Assert.assertEquals(expected, result);
} }
long duration = System.currentTimeMillis() - startTs; long duration = System.currentTimeMillis() - startTs;
@ -101,7 +92,7 @@ class TbelInvokeServiceTest extends AbstractControllerTest {
for (int i = 0; i < maxJsErrors; i++) { for (int i = 0; i < maxJsErrors; i++) {
assertThatThrownBy(() -> { assertThatThrownBy(() -> {
invokeScript(scriptId, hugeMsg); invokeScriptResultString(scriptId, hugeMsg);
}).hasMessageContaining("input arguments exceed maximum"); }).hasMessageContaining("input arguments exceed maximum");
} }
assertThatScriptIsBlocked(scriptId); assertThatScriptIsBlocked(scriptId);
@ -114,7 +105,7 @@ class TbelInvokeServiceTest extends AbstractControllerTest {
for (int i = 0; i < maxJsErrors; i++) { for (int i = 0; i < maxJsErrors; i++) {
assertThatThrownBy(() -> { assertThatThrownBy(() -> {
invokeScript(scriptId, "{}"); invokeScriptResultString(scriptId, "{}");
}).hasMessageContaining("result exceeds maximum allowed size"); }).hasMessageContaining("result exceeds maximum allowed size");
} }
assertThatScriptIsBlocked(scriptId); assertThatScriptIsBlocked(scriptId);
@ -182,7 +173,7 @@ class TbelInvokeServiceTest extends AbstractControllerTest {
scriptsIds.add(scriptId); scriptsIds.add(scriptId);
for (int j = 0; j < i; j++) { for (int j = 0; j < i; j++) {
invokeScript(scriptId, "{ \"temperature\": 12 }"); // so that scriptsIds is ordered by number of invocations invokeScriptResultString(scriptId, "{ \"temperature\": 12 }"); // so that scriptsIds is ordered by number of invocations
} }
} }
@ -201,23 +192,13 @@ class TbelInvokeServiceTest extends AbstractControllerTest {
UUID scriptRemovedFromCache = scriptsIds.get(0); UUID scriptRemovedFromCache = scriptsIds.get(0);
assertThat(compiledScriptsCache.getIfPresent(scriptIdToHash.get(scriptRemovedFromCache))).isNull(); assertThat(compiledScriptsCache.getIfPresent(scriptIdToHash.get(scriptRemovedFromCache))).isNull();
invokeScript(scriptRemovedFromCache, "{ \"temperature\": 12 }"); invokeScriptResultString(scriptRemovedFromCache, "{ \"temperature\": 12 }");
assertThat(compiledScriptsCache.getIfPresent(scriptIdToHash.get(scriptRemovedFromCache))).isNotNull(); assertThat(compiledScriptsCache.getIfPresent(scriptIdToHash.get(scriptRemovedFromCache))).isNotNull();
} }
private void assertThatScriptIsBlocked(UUID scriptId) { private void assertThatScriptIsBlocked(UUID scriptId) {
assertThatThrownBy(() -> { assertThatThrownBy(() -> {
invokeScript(scriptId, "{}"); invokeScriptResultString(scriptId, "{}");
}).hasMessageContaining("invocation is blocked due to maximum error"); }).hasMessageContaining("invocation is blocked due to maximum error");
} }
private UUID evalScript(String script) throws ExecutionException, InterruptedException {
return invokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, script, "msg", "metadata", "msgType").get();
}
private String invokeScript(UUID scriptId, String str) throws ExecutionException, InterruptedException {
var msg = JacksonUtil.fromString(str, Map.class);
return invokeService.invokeScript(TenantId.SYS_TENANT_ID, null, scriptId, msg, "{}", POST_TELEMETRY_REQUEST.name()).get().toString();
}
} }

4
application/src/test/resources/logback-test.xml

@ -17,9 +17,7 @@
<logger name="org.eclipse.leshan" level="INFO"/> <logger name="org.eclipse.leshan" level="INFO"/>
<logger name="org.thingsboard.server.controller.AbstractWebTest" level="INFO"/> <logger name="org.thingsboard.server.controller.AbstractWebTest" level="INFO"/>
<logger name="org.thingsboard.server.service.script" level="INFO"/> <logger name="org.thingsboard.server.service.script" level="INFO"/>
<logger name="org.thingsboard.server.service.entitiy.dashboard" level="TRACE"/>
<logger name="org.thingsboard.server.service.entitiy.widgets" level="TRACE"/>
<logger name="org.thingsboard.server.service.sync" level="TRACE"/>
<!-- mute TelemetryEdgeSqlTest that causes a lot of randomly generated errors --> <!-- mute TelemetryEdgeSqlTest that causes a lot of randomly generated errors -->
<logger name="org.thingsboard.server.service.edge.rpc.EdgeGrpcSession" level="OFF"/> <logger name="org.thingsboard.server.service.edge.rpc.EdgeGrpcSession" level="OFF"/>

36
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/GeneralNotificationInfo.java

@ -0,0 +1,36 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.notification.info;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GeneralNotificationInfo implements RuleOriginatedNotificationInfo {
private Map<String, String> data;
@Override
public Map<String, String> getTemplateData() {
return data;
}
}

1
common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusAdmin.java

@ -31,6 +31,7 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@Slf4j @Slf4j
@Deprecated(forRemoval = true, since = "3.9") // for removal in 4.0
public class TbServiceBusAdmin implements TbQueueAdmin { public class TbServiceBusAdmin implements TbQueueAdmin {
private final String MAX_SIZE = "maxSizeInMb"; private final String MAX_SIZE = "maxSizeInMb";
private final String MESSAGE_TIME_TO_LIVE = "messageTimeToLiveInSec"; private final String MESSAGE_TIME_TO_LIVE = "messageTimeToLiveInSec";

1
common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubAdmin.java

@ -37,6 +37,7 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@Slf4j @Slf4j
@Deprecated(forRemoval = true, since = "3.9") // for removal in 4.0
public class TbPubSubAdmin implements TbQueueAdmin { public class TbPubSubAdmin implements TbQueueAdmin {
private static final String ACK_DEADLINE = "ackDeadlineInSec"; private static final String ACK_DEADLINE = "ackDeadlineInSec";
private static final String MESSAGE_RETENTION = "messageRetentionInSec"; private static final String MESSAGE_RETENTION = "messageRetentionInSec";

1
common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqAdmin.java

@ -27,6 +27,7 @@ import java.util.Map;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
@Slf4j @Slf4j
@Deprecated(forRemoval = true, since = "3.9") // for removal in 4.0
public class TbRabbitMqAdmin implements TbQueueAdmin { public class TbRabbitMqAdmin implements TbQueueAdmin {
private final Channel channel; private final Channel channel;

1
common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsAdmin.java

@ -36,6 +36,7 @@ import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j @Slf4j
@Deprecated(forRemoval = true, since = "3.9") // for removal in 4.0
public class TbAwsSqsAdmin implements TbQueueAdmin { public class TbAwsSqsAdmin implements TbQueueAdmin {
private final Map<String, String> attributes; private final Map<String, String> attributes;

2
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java

@ -1255,7 +1255,7 @@ public class TbUtils {
if (str == null || str.isEmpty()) { if (str == null || str.isEmpty()) {
return -1; return -1;
} }
return str.matches("-?\\d+(\\.\\d+)?") ? DEC_RADIX : -1; return str.matches("[+-]?\\d+(\\.\\d+)?") ? DEC_RADIX : -1;
} }
public static int isHexadecimal(String str) { public static int isHexadecimal(String str) {

1
common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java

@ -213,6 +213,7 @@ public class TbUtilsTest {
Assertions.assertEquals((Integer) 0, TbUtils.parseInt("0")); Assertions.assertEquals((Integer) 0, TbUtils.parseInt("0"));
Assertions.assertEquals((Integer) 0, TbUtils.parseInt("-0")); Assertions.assertEquals((Integer) 0, TbUtils.parseInt("-0"));
Assertions.assertEquals((Integer) 0, TbUtils.parseInt("+0"));
Assertions.assertEquals(java.util.Optional.of(473).get(), TbUtils.parseInt("473")); Assertions.assertEquals(java.util.Optional.of(473).get(), TbUtils.parseInt("473"));
Assertions.assertEquals(java.util.Optional.of(-255).get(), TbUtils.parseInt("-0xFF")); Assertions.assertEquals(java.util.Optional.of(-255).get(), TbUtils.parseInt("-0xFF"));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("-0xFF123")); Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("-0xFF123"));

12
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java

@ -1101,11 +1101,19 @@ public class DefaultTransportService extends TransportActivityManager implements
} }
private void sendToCore(TenantId tenantId, EntityId entityId, ToCoreMsg msg, UUID routingKey, TransportServiceCallback<Void> callback) { private void sendToCore(TenantId tenantId, EntityId entityId, ToCoreMsg msg, UUID routingKey, TransportServiceCallback<Void> callback) {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId); TopicPartitionInfo tpi;
try {
tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId);
} catch (Exception e) {
log.warn("Failed to send message to core. Tenant with ID [{}], routingKey [{}], msg [{}]. Message delivery aborted.", tenantId, routingKey, msg, e);
if (callback != null) {
callback.onError(e);
}
return;
}
if (log.isTraceEnabled()) { if (log.isTraceEnabled()) {
log.trace("[{}][{}] Pushing to topic {} message {}", tenantId, entityId, tpi.getFullTopicName(), msg); log.trace("[{}][{}] Pushing to topic {} message {}", tenantId, entityId, tpi.getFullTopicName(), msg);
} }
TransportTbQueueCallback transportTbQueueCallback = callback != null ? TransportTbQueueCallback transportTbQueueCallback = callback != null ?
new TransportTbQueueCallback(callback) : null; new TransportTbQueueCallback(callback) : null;
tbCoreProducerStats.incrementTotal(); tbCoreProducerStats.incrementTotal();

9
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java

@ -372,6 +372,15 @@ public class DefaultNotifications {
.build()) .build())
.build(); .build();
public static final DefaultNotification queueTypeDeprecation = DefaultNotification.builder()
.name("Queue type deprecation")
.type(NotificationType.GENERAL)
.subject("WARNING: ${queueType} deprecation")
.text("Starting with ThingsBoard 4.0, ${queueType} will no longer be supported as a message queue for microservices. " +
"Please migrate to Apache Kafka. This change will not impact any rule nodes.")
.icon("warning").color(RED_COLOR)
.build();
private final NotificationTemplateService templateService; private final NotificationTemplateService templateService;
private final NotificationRuleService ruleService; private final NotificationRuleService ruleService;

15
dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java

@ -17,15 +17,19 @@ package org.thingsboard.server.dao.resource;
import com.google.common.hash.Hashing; import com.google.common.hash.Hashing;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.AllArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException; import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.event.TransactionalEventListener; import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.RegexUtils; import org.thingsboard.common.util.RegexUtils;
import org.thingsboard.server.cache.resourceInfo.ResourceInfoCacheKey; import org.thingsboard.server.cache.resourceInfo.ResourceInfoCacheKey;
import org.thingsboard.server.cache.resourceInfo.ResourceInfoEvictEvent; import org.thingsboard.server.cache.resourceInfo.ResourceInfoEvictEvent;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResource;
@ -54,7 +58,7 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
@Service("TbResourceDaoService") @Service("TbResourceDaoService")
@Slf4j @Slf4j
@AllArgsConstructor @RequiredArgsConstructor
@Primary @Primary
public class BaseResourceService extends AbstractCachedEntityService<ResourceInfoCacheKey, TbResourceInfo, ResourceInfoEvictEvent> implements ResourceService { public class BaseResourceService extends AbstractCachedEntityService<ResourceInfoCacheKey, TbResourceInfo, ResourceInfoEvictEvent> implements ResourceService {
@ -62,6 +66,8 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
protected final TbResourceDao resourceDao; protected final TbResourceDao resourceDao;
protected final TbResourceInfoDao resourceInfoDao; protected final TbResourceInfoDao resourceInfoDao;
protected final ResourceDataValidator resourceValidator; protected final ResourceDataValidator resourceValidator;
@Autowired @Lazy
private ImageService imageService;
@Override @Override
public TbResource saveResource(TbResource resource, boolean doValidate) { public TbResource saveResource(TbResource resource, boolean doValidate) {
@ -243,6 +249,11 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
public TbResource createOrUpdateSystemResource(ResourceType resourceType, String resourceKey, String data) { public TbResource createOrUpdateSystemResource(ResourceType resourceType, String resourceKey, String data) {
if (resourceType == ResourceType.DASHBOARD) { if (resourceType == ResourceType.DASHBOARD) {
data = checkSystemResourcesUsage(data, ResourceType.JS_MODULE); data = checkSystemResourcesUsage(data, ResourceType.JS_MODULE);
Dashboard dashboard = JacksonUtil.fromString(data, Dashboard.class);
dashboard.setTenantId(TenantId.SYS_TENANT_ID);
imageService.replaceBase64WithImageUrl(dashboard);
data = JacksonUtil.toString(dashboard);
} }
TbResource resource = findResourceByTenantIdAndKey(TenantId.SYS_TENANT_ID, resourceType, resourceKey); TbResource resource = findResourceByTenantIdAndKey(TenantId.SYS_TENANT_ID, resourceType, resourceKey);

2
msa/js-executor/docker/Dockerfile

@ -14,7 +14,7 @@
# limitations under the License. # limitations under the License.
# #
FROM thingsboard/node:16.20.2-bookworm-slim FROM thingsboard/node:20.18.0-bookworm-slim
ENV NODE_ENV production ENV NODE_ENV production
ENV DOCKER_MODE true ENV DOCKER_MODE true

48
msa/js-executor/package.json

@ -6,26 +6,26 @@
"main": "server.ts", "main": "server.ts",
"bin": "server.js", "bin": "server.js",
"scripts": { "scripts": {
"pkg": "tsc && pkg -t node16-linux-x64,node16-win-x64 --out-path ./target ./target/src && node install.js", "pkg": "tsc && pkg -t node18-linux-x64,node18-win-x64 --out-path ./target ./target/src && node install.js",
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon --watch '.' --ext 'ts' --exec 'ts-node server.ts'", "start": "nodemon --watch '.' --ext 'ts' --exec 'ts-node server.ts'",
"start-prod": "nodemon --watch '.' --ext 'ts' --exec 'NODE_ENV=production ts-node server.ts'", "start-prod": "nodemon --watch '.' --ext 'ts' --exec 'NODE_ENV=production ts-node server.ts'",
"build": "tsc" "build": "tsc"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-sqs": "^3.121.0", "@aws-sdk/client-sqs": "^3.682.0",
"@azure/service-bus": "^7.5.1", "@azure/service-bus": "^7.9.5",
"@google-cloud/pubsub": "^4.3.3", "@google-cloud/pubsub": "^4.8.0",
"amqplib": "^0.10.0", "amqplib": "^0.10.4",
"config": "^3.3.7", "config": "^3.3.12",
"express": "^4.18.1", "express": "^4.21.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"kafkajs": "^2.1.0", "kafkajs": "^2.2.4",
"long": "^5.2.0", "long": "^5.2.3",
"uuid-parse": "^1.1.0", "uuid-parse": "^1.1.0",
"uuid-random": "^1.3.2", "uuid-random": "^1.3.2",
"winston": "^3.7.2", "winston": "^3.16.0",
"winston-daily-rotate-file": "^4.7.1" "winston-daily-rotate-file": "^5.0.0"
}, },
"nyc": { "nyc": {
"exclude": [ "exclude": [
@ -36,26 +36,20 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@types/amqplib": "^0.8.2", "@types/amqplib": "^0.10.5",
"@types/config": "^0.0.41", "@types/config": "^3.3.5",
"@types/express": "^4.17.13", "@types/express": "~4.17.21",
"@types/node": "^18.0.1", "@types/node": "~20.17.6",
"@types/uuid-parse": "^1.0.0", "@types/uuid-parse": "^1.0.2",
"fs-extra": "^10.1.0", "fs-extra": "^11.2.0",
"nodemon": "^2.0.16", "nodemon": "^3.1.7",
"pkg": "^5.7.0", "pkg": "^5.8.1",
"ts-node": "^10.8.1", "ts-node": "^10.9.2",
"typescript": "^4.7.4" "typescript": "5.5.4"
}, },
"pkg": { "pkg": {
"assets": [ "assets": [
"node_modules/config/**/*.*" "node_modules/config/**/*.*"
] ]
},
"resolutions": {
"ansi-regex": "^5.0.1",
"color-string": "^1.5.5",
"minimist": "^1.2.6",
"node-fetch": "^2.6.7"
} }
} }

4
msa/js-executor/pom.xml

@ -71,8 +71,8 @@
<goal>install-node-and-yarn</goal> <goal>install-node-and-yarn</goal>
</goals> </goals>
<configuration> <configuration>
<nodeVersion>v16.20.2</nodeVersion> <nodeVersion>v20.18.0</nodeVersion>
<yarnVersion>v1.22.17</yarnVersion> <yarnVersion>v1.22.22</yarnVersion>
</configuration> </configuration>
</execution> </execution>
<execution> <execution>

2
msa/js-executor/queue/awsSqsTemplate.ts

@ -52,7 +52,7 @@ export class AwsSqsTemplate implements IQueue {
private queueAttributes: { [n: string]: string } = { private queueAttributes: { [n: string]: string } = {
FifoQueue: 'true' FifoQueue: 'true'
}; };
private timer: NodeJS.Timer; private timer: NodeJS.Timeout;
name = 'AWS SQS'; name = 'AWS SQS';

2
msa/js-executor/queue/serviceBusTemplate.ts

@ -57,7 +57,7 @@ export class ServiceBusTemplate implements IQueue {
this.parseQueueProperties(); this.parseQueueProperties();
const listQueues = await this.serviceBusService.listQueues(); const listQueues = this.serviceBusService.listQueues();
for await (const queue of listQueues) { for await (const queue of listQueues) {
this.queues.push(queue.name); this.queues.push(queue.name);
} }

4310
msa/js-executor/yarn.lock

File diff suppressed because it is too large

4
msa/vc-executor/src/main/resources/tb-vc-executor.yml

@ -47,7 +47,9 @@ zk:
# Queue configuration parameters # Queue configuration parameters
queue: queue:
type: "${TB_QUEUE_TYPE:kafka}" # in-memory or kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) # kafka (Apache Kafka). The following queue types are deprecated and will no longer be supported in ThingsBoard 4.0:
# aws-sqs (AWS SQS), pubsub (PubSub), service-bus (Azure Service Bus), rabbitmq (RabbitMQ)
type: "${TB_QUEUE_TYPE:kafka}"
prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka).
in_memory: in_memory:
stats: stats:

2
msa/web-ui/docker/Dockerfile

@ -14,7 +14,7 @@
# limitations under the License. # limitations under the License.
# #
FROM thingsboard/node:16.20.2-bookworm-slim FROM thingsboard/node:20.18.0-bookworm-slim
ENV NODE_ENV production ENV NODE_ENV production
ENV DOCKER_MODE true ENV DOCKER_MODE true

39
msa/web-ui/package.json

@ -6,22 +6,22 @@
"main": "server.ts", "main": "server.ts",
"bin": "server.js", "bin": "server.js",
"scripts": { "scripts": {
"pkg": "tsc && pkg -t node16-linux-x64,node16-win-x64 --out-path ./target ./target/src && node install.js", "pkg": "tsc && pkg -t node18-linux-x64,node18-win-x64 --out-path ./target ./target/src && node install.js",
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon --watch '.' --ext 'ts' --exec 'WEB_FOLDER=./target/web ts-node server.ts'", "start": "nodemon --watch '.' --ext 'ts' --exec 'WEB_FOLDER=./target/web ts-node server.ts'",
"start-prod": "nodemon --watch '.' --ext 'ts' --exec 'WEB_FOLDER=./target/web NODE_ENV=production ts-node server.ts'", "start-prod": "nodemon --watch '.' --ext 'ts' --exec 'WEB_FOLDER=./target/web NODE_ENV=production ts-node server.ts'",
"build": "tsc" "build": "tsc"
}, },
"dependencies": { "dependencies": {
"compression": "^1.7.4", "compression": "^1.7.5",
"config": "^3.3.7", "config": "^3.3.12",
"connect-history-api-fallback": "^1.6.0", "connect-history-api-fallback": "^1.6.0",
"express": "^4.18.1", "express": "^4.21.1",
"http": "0.0.0", "http": "0.0.0",
"http-proxy": "^1.18.1", "http-proxy": "^1.18.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"winston": "^3.7.2", "winston": "^3.16.0",
"winston-daily-rotate-file": "^4.7.1" "winston-daily-rotate-file": "^5.0.0"
}, },
"nyc": { "nyc": {
"exclude": [ "exclude": [
@ -32,26 +32,21 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@types/compression": "^1.7.2", "@types/compression": "^1.7.5",
"@types/config": "^0.0.41", "@types/config": "^3.3.5",
"@types/connect-history-api-fallback": "^1.3.5", "@types/connect-history-api-fallback": "^1.5.4",
"@types/express": "^4.17.13", "@types/express": "~4.17.21",
"@types/http-proxy": "^1.17.9", "@types/http-proxy": "^1.17.15",
"@types/node": "^18.0.0", "@types/node": "~20.17.6",
"fs-extra": "^10.1.0", "fs-extra": "^11.2.0",
"nodemon": "^2.0.16", "nodemon": "^3.1.7",
"pkg": "^5.7.0", "pkg": "^5.8.1",
"ts-node": "^10.8.1", "ts-node": "^10.9.2",
"typescript": "^4.7.4" "typescript": "5.5.4"
}, },
"pkg": { "pkg": {
"assets": [ "assets": [
"node_modules/config/**/*.*" "node_modules/config/**/*.*"
] ]
},
"resolutions": {
"color-string": "^1.5.5",
"follow-redirects": "^1.14.8",
"minimist": "^1.2.6"
} }
} }

4
msa/web-ui/pom.xml

@ -80,8 +80,8 @@
<goal>install-node-and-yarn</goal> <goal>install-node-and-yarn</goal>
</goals> </goals>
<configuration> <configuration>
<nodeVersion>v16.20.2</nodeVersion> <nodeVersion>v20.18.0</nodeVersion>
<yarnVersion>v1.22.17</yarnVersion> <yarnVersion>v1.22.22</yarnVersion>
</configuration> </configuration>
</execution> </execution>
<execution> <execution>

1982
msa/web-ui/yarn.lock

File diff suppressed because it is too large

3
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestStats; import org.thingsboard.server.common.data.notification.NotificationRequestStats;
import org.thingsboard.server.common.data.notification.info.GeneralNotificationInfo;
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter; import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
@ -32,7 +33,7 @@ public interface NotificationCenter {
NotificationRequest processNotificationRequest(TenantId tenantId, NotificationRequest notificationRequest, FutureCallback<NotificationRequestStats> callback); NotificationRequest processNotificationRequest(TenantId tenantId, NotificationRequest notificationRequest, FutureCallback<NotificationRequestStats> callback);
void sendGeneralWebNotification(TenantId tenantId, UsersFilter recipients, NotificationTemplate template); void sendGeneralWebNotification(TenantId tenantId, UsersFilter recipients, NotificationTemplate template, GeneralNotificationInfo info);
void deleteNotificationRequest(TenantId tenantId, NotificationRequestId notificationRequestId); void deleteNotificationRequest(TenantId tenantId, NotificationRequestId notificationRequestId);

37
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java

@ -52,7 +52,7 @@ import java.util.stream.Collectors;
@RuleNode(type = ComponentType.ENRICHMENT, @RuleNode(type = ComponentType.ENRICHMENT,
name = "originator telemetry", name = "originator telemetry",
configClazz = TbGetTelemetryNodeConfiguration.class, configClazz = TbGetTelemetryNodeConfiguration.class,
version = 1, version = 2,
nodeDescription = "Adds message originator telemetry for selected time range into message metadata", nodeDescription = "Adds message originator telemetry for selected time range into message metadata",
nodeDetails = "Useful when you need to get telemetry data set from the message originator for a specific time range " + nodeDetails = "Useful when you need to get telemetry data set from the message originator for a specific time range " +
"instead of fetching just the latest telemetry or if you need to get the closest telemetry to the fetch interval start or end. " + "instead of fetching just the latest telemetry or if you need to get the closest telemetry to the fetch interval start or end. " +
@ -232,21 +232,21 @@ public class TbGetTelemetryNode implements TbNode {
public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException { public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
boolean hasChanges = false; boolean hasChanges = false;
switch (fromVersion) { switch (fromVersion) {
case 0 -> { case 0: {
if (oldConfiguration.hasNonNull("fetchMode")) { if (oldConfiguration.hasNonNull("fetchMode")) {
String fetchMode = oldConfiguration.get("fetchMode").asText(); String fetchMode = oldConfiguration.get("fetchMode").asText();
switch (fetchMode) { switch (fetchMode) {
case "FIRST": case "FIRST" -> {
((ObjectNode) oldConfiguration).put("orderBy", Direction.ASC.name()); ((ObjectNode) oldConfiguration).put("orderBy", Direction.ASC.name());
((ObjectNode) oldConfiguration).put("aggregation", Aggregation.NONE.name()); ((ObjectNode) oldConfiguration).put("aggregation", Aggregation.NONE.name());
hasChanges = true; hasChanges = true;
break; }
case "LAST": case "LAST" -> {
((ObjectNode) oldConfiguration).put("orderBy", Direction.DESC.name()); ((ObjectNode) oldConfiguration).put("orderBy", Direction.DESC.name());
((ObjectNode) oldConfiguration).put("aggregation", Aggregation.NONE.name()); ((ObjectNode) oldConfiguration).put("aggregation", Aggregation.NONE.name());
hasChanges = true; hasChanges = true;
break; }
case "ALL": case "ALL" -> {
if (oldConfiguration.has("orderBy") && if (oldConfiguration.has("orderBy") &&
(oldConfiguration.get("orderBy").isNull() || oldConfiguration.get("orderBy").asText().isEmpty())) { (oldConfiguration.get("orderBy").isNull() || oldConfiguration.get("orderBy").asText().isEmpty())) {
((ObjectNode) oldConfiguration).put("orderBy", Direction.ASC.name()); ((ObjectNode) oldConfiguration).put("orderBy", Direction.ASC.name());
@ -257,16 +257,33 @@ public class TbGetTelemetryNode implements TbNode {
((ObjectNode) oldConfiguration).put("aggregation", Aggregation.NONE.name()); ((ObjectNode) oldConfiguration).put("aggregation", Aggregation.NONE.name());
hasChanges = true; hasChanges = true;
} }
break; }
default: default -> {
((ObjectNode) oldConfiguration).put("fetchMode", FetchMode.LAST.name()); ((ObjectNode) oldConfiguration).put("fetchMode", FetchMode.LAST.name());
((ObjectNode) oldConfiguration).put("orderBy", Direction.DESC.name()); ((ObjectNode) oldConfiguration).put("orderBy", Direction.DESC.name());
((ObjectNode) oldConfiguration).put("aggregation", Aggregation.NONE.name()); ((ObjectNode) oldConfiguration).put("aggregation", Aggregation.NONE.name());
hasChanges = true; hasChanges = true;
break; }
} }
} }
} }
case 1: {
if (!oldConfiguration.hasNonNull("limit")) {
((ObjectNode) oldConfiguration).put("limit", 1000);
hasChanges = true;
}
if (oldConfiguration.has("fetchMode") && oldConfiguration.get("fetchMode").asText().equals("ALL")) {
if (!oldConfiguration.hasNonNull("aggregation")) {
((ObjectNode) oldConfiguration).put("aggregation", Aggregation.NONE.name());
hasChanges = true;
}
if (!oldConfiguration.hasNonNull("orderBy")) {
((ObjectNode) oldConfiguration).put("orderBy", Direction.ASC.name());
hasChanges = true;
}
}
break;
}
} }
return new TbPair<>(hasChanges, oldConfiguration); return new TbPair<>(hasChanges, oldConfiguration);
} }

2
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js

File diff suppressed because one or more lines are too long

32
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeTest.java

@ -637,6 +637,38 @@ public class TbGetTelemetryNodeTest extends AbstractRuleNodeUpgradeTest {
"endInterval": 1, "endInterval": 1,
"endIntervalTimeUnit": "MINUTES" "endIntervalTimeUnit": "MINUTES"
} }
"""),
// config for version 0 (fetchMode is 'ALL' and limit, aggregation and orderBy do not exist)
Arguments.of(0,
"""
{
"latestTsKeyNames": ["key"],
"fetchMode": "ALL",
"useMetadataIntervalPatterns": false,
"startIntervalPattern": "",
"endIntervalPattern": "",
"startInterval": 2,
"startIntervalTimeUnit": "MINUTES",
"endInterval": 1,
"endIntervalTimeUnit": "MINUTES"
}
""",
true,
"""
{
"latestTsKeyNames": ["key"],
"aggregation": "NONE",
"fetchMode": "ALL",
"orderBy": "ASC",
"limit": 1000,
"useMetadataIntervalPatterns": false,
"startIntervalPattern": "",
"endIntervalPattern": "",
"startInterval": 2,
"startIntervalTimeUnit": "MINUTES",
"endInterval": 1,
"endIntervalTimeUnit": "MINUTES"
}
""") """)
); );
} }

4
transport/coap/src/main/resources/tb-coap-transport.yml

@ -232,7 +232,9 @@ coap:
# Queue configuration parameters # Queue configuration parameters
queue: queue:
type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) # kafka (Apache Kafka). The following queue types are deprecated and will no longer be supported in ThingsBoard 4.0:
# aws-sqs (AWS SQS), pubsub (PubSub), service-bus (Azure Service Bus), rabbitmq (RabbitMQ)
type: "${TB_QUEUE_TYPE:kafka}"
prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka).
kafka: kafka:
# Kafka Bootstrap Servers # Kafka Bootstrap Servers

4
transport/http/src/main/resources/tb-http-transport.yml

@ -202,7 +202,9 @@ transport:
# Queue configuration parameters # Queue configuration parameters
queue: queue:
type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) # kafka (Apache Kafka). The following queue types are deprecated and will no longer be supported in ThingsBoard 4.0:
# aws-sqs (AWS SQS), pubsub (PubSub), service-bus (Azure Service Bus), rabbitmq (RabbitMQ)
type: "${TB_QUEUE_TYPE:kafka}"
prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka) . prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka) .
kafka: kafka:
# Kafka Bootstrap Servers # Kafka Bootstrap Servers

4
transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml

@ -302,7 +302,9 @@ transport:
# Queue configuration properties # Queue configuration properties
queue: queue:
type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) # kafka (Apache Kafka). The following queue types are deprecated and will no longer be supported in ThingsBoard 4.0:
# aws-sqs (AWS SQS), pubsub (PubSub), service-bus (Azure Service Bus), rabbitmq (RabbitMQ)
type: "${TB_QUEUE_TYPE:kafka}"
prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka).
kafka: kafka:
# Kafka Bootstrap Servers # Kafka Bootstrap Servers

4
transport/mqtt/src/main/resources/tb-mqtt-transport.yml

@ -235,7 +235,9 @@ transport:
# Queue configuration parameters # Queue configuration parameters
queue: queue:
type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) # kafka (Apache Kafka). The following queue types are deprecated and will no longer be supported in ThingsBoard 4.0:
# aws-sqs (AWS SQS), pubsub (PubSub), service-bus (Azure Service Bus), rabbitmq (RabbitMQ)
type: "${TB_QUEUE_TYPE:kafka}"
prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka).
kafka: kafka:
# Kafka Bootstrap Servers # Kafka Bootstrap Servers

4
transport/snmp/src/main/resources/tb-snmp-transport.yml

@ -181,7 +181,9 @@ transport:
# Queue configuration parameters # Queue configuration parameters
queue: queue:
type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) # kafka (Apache Kafka). The following queue types are deprecated and will no longer be supported in ThingsBoard 4.0:
# aws-sqs (AWS SQS), pubsub (PubSub), service-bus (Azure Service Bus), rabbitmq (RabbitMQ)
type: "${TB_QUEUE_TYPE:kafka}"
prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka).
kafka: kafka:
# Kafka Bootstrap Servers # Kafka Bootstrap Servers

67
ui-ngx/angular.json

@ -18,12 +18,20 @@
}, },
"architect": { "architect": {
"build": { "build": {
"builder": "@angular-builders/custom-webpack:browser", "builder": "@angular-builders/custom-esbuild:application",
"options": { "options": {
"outputPath": "target/generated-resources/public", "plugins": ["./esbuild/tb-esbuild-plugins.ts"],
"loader": {
".raw": "text"
},
"outputPath": {
"base": "target/generated-resources/public",
"media": ".",
"browser": ""
},
"index": "src/index.html", "index": "src/index.html",
"main": "src/main.ts", "browser": "src/main.ts",
"polyfills": "src/polyfills.ts", "polyfills": ["src/polyfills.ts"],
"tsConfig": "src/tsconfig.app.json", "tsConfig": "src/tsconfig.app.json",
"aot": true, "aot": true,
"assets": [ "assets": [
@ -122,49 +130,8 @@
"node_modules/prismjs/components/prism-typescript.min.js", "node_modules/prismjs/components/prism-typescript.min.js",
"node_modules/prismjs/plugins/line-numbers/prism-line-numbers.min.js" "node_modules/prismjs/plugins/line-numbers/prism-line-numbers.min.js"
], ],
"customWebpackConfig": {
"path": "./extra-webpack.config.js"
},
"allowedCommonJsDependencies": [ "allowedCommonJsDependencies": [
"hammerjs", "*"
"react",
"react-dom",
"reactcss",
"react-ace",
"schema-inspector",
"@flowjs/flow.js",
"@material-ui/icons/Add",
"@material-ui/icons/Clear",
"js-beautify",
"mousetrap",
"prop-types",
"react-is",
"hoist-non-react-statics",
"classnames",
"raf",
"moment-timezone",
"tinycolor2",
"json-schema-defaults",
"leaflet-providers",
"lodash",
"jquery",
"jquery.terminal",
"tooltipster",
"jstree",
"qrcode",
"wcwidth",
"leaflet-polylinedecorator",
"ace-diff",
"messageformat-parser",
"html2canvas",
"jszip",
"moment",
"ace",
"ace-builds",
"diff-match-patch",
"tv4",
"@messageformat/parser",
"sorted-btree"
] ]
}, },
"configurations": { "configurations": {
@ -187,8 +154,6 @@
"sourceMap": false, "sourceMap": false,
"namedChunks": false, "namedChunks": false,
"extractLicenses": true, "extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": false,
"budgets": [ "budgets": [
{ {
"type": "initial", "type": "initial",
@ -198,7 +163,6 @@
] ]
}, },
"development": { "development": {
"buildOptimizer": false,
"optimization": { "optimization": {
"scripts": false, "scripts": false,
"styles": { "styles": {
@ -207,7 +171,6 @@
}, },
"fonts": false "fonts": false
}, },
"vendorChunk": true,
"extractLicenses": false, "extractLicenses": false,
"sourceMap": true, "sourceMap": true,
"namedChunks": true "namedChunks": true
@ -215,7 +178,7 @@
} }
}, },
"serve": { "serve": {
"builder": "@angular-builders/custom-webpack:dev-server", "builder": "@angular-builders/custom-esbuild:dev-server",
"options": { "options": {
"buildTarget": "thingsboard:build", "buildTarget": "thingsboard:build",
"proxyConfig": "proxy.conf.js" "proxyConfig": "proxy.conf.js"
@ -231,7 +194,7 @@
"defaultConfiguration": "development" "defaultConfiguration": "development"
}, },
"extract-i18n": { "extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n", "builder": "@angular/build:extract-i18n",
"options": { "options": {
"buildTarget": "thingsboard:build" "buildTarget": "thingsboard:build"
} }

104
ui-ngx/esbuild/tb-esbuild-plugins.ts

@ -0,0 +1,104 @@
///
/// Copyright © 2016-2024 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 type { Plugin, PluginBuild, OutputFile } from 'esbuild';
import dirTree from 'directory-tree';
import * as packageJson from '../package.json';
import { gzip } from 'node:zlib';
import * as path from 'node:path';
const defineTbVariablesPlugin: Plugin = {
name: 'tb-define-variables',
setup(build: PluginBuild) {
const options = build.initialOptions;
const langs: string[] = [];
dirTree("./src/assets/locale/", {extensions: /\.json$/}, (item) => {
/* It is expected what the name of a locale file has the following format: */
/* 'locale.constant-LANG_CODE[_REGION_CODE].json', e.g. locale.constant-es.json or locale.constant-zh_CN.json*/
langs.push(item.name.slice(item.name.lastIndexOf("-") + 1, -5));
});
options.define.TB_VERSION = JSON.stringify(packageJson.version);
options.define.SUPPORTED_LANGS = JSON.stringify(langs);
options.define.ngJitMode = 'true';
},
};
const resolveJQueryPlugin: Plugin = {
name: 'tb-resolve-jquery-plugin',
setup(build: PluginBuild) {
if (isProduction()) {
const jQueryPath = require.resolve('jquery');
build.onResolve({filter: /^(jquery|\$)$/}, () => {
return {path: jQueryPath};
})
}
}
};
const compressFileTypes = ['.js', '.css', '.html', '.svg', '.png', '.jpg', '.ttf', '.gif', '.woff', '.woff2', '.eot', '.json'];
const compressThreshold = 10240;
const compressorPlugin: Plugin = {
name: 'tb-compressor-plugin',
setup(build) {
build.onEnd(async result => {
if (!result.outputFiles || !isProduction()) return;
const outputExt = '.gz';
const gzippedFiles: OutputFile[] = [];
for (const file of result.outputFiles) {
if (!compressFileTypes.some((ext) => ext === path.extname(file.path))) continue;
if (file.contents.byteLength <= compressThreshold) continue;
const compressedContent = await gzipContent(file.contents);
const compressedFilePath = `${file.path}${outputExt}`;
gzippedFiles.push(
{
path: compressedFilePath,
hash: file.hash,
contents: new Uint8Array(compressedContent),
text: '',
}
);
}
result.outputFiles.push(...gzippedFiles);
});
},
};
async function gzipContent(content): Promise<Buffer> {
return new Promise((resolve, reject) => {
gzip(content, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
}
function isProduction(): boolean {
const configurationIndex = process.argv.indexOf('--configuration');
let production = false;
if (configurationIndex > -1) {
const configurationValue = process.argv[configurationIndex + 1];
production = configurationValue === 'production';
}
return production;
}
export default [defineTbVariablesPlugin, resolveJQueryPlugin, compressorPlugin];

100
ui-ngx/extra-webpack.config.js

@ -1,100 +0,0 @@
/*
* Copyright © 2016-2024 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.
*/
const CompressionPlugin = require("compression-webpack-plugin");
const JavaScriptOptimizerPlugin = require("@angular-devkit/build-angular/src/tools/webpack/plugins/javascript-optimizer-plugin").JavaScriptOptimizerPlugin;
const webpack = require("webpack");
const dirTree = require("directory-tree");
const ngWebpack = require('@ngtools/webpack');
const keysTransformer = require('ts-transformer-keys/transformer').default;
var langs = [];
dirTree("./src/assets/locale/", {extensions: /\.json$/}, (item) => {
/* It is expected what the name of a locale file has the following format: */
/* 'locale.constant-LANG_CODE[_REGION_CODE].json', e.g. locale.constant-es.json or locale.constant-zh_CN.json*/
langs.push(item.name.slice(item.name.lastIndexOf("-") + 1, -5));
});
module.exports = (config, options) => {
config.ignoreWarnings.push(/Usage of '~' in imports is deprecated/);
config.ignoreWarnings.push(/Did you mean "left" instead?/);
config.ignoreWarnings.push(/autoprefixer/);
config.plugins.push(
new webpack.DefinePlugin({
TB_VERSION: JSON.stringify(require("./package.json").version),
SUPPORTED_LANGS: JSON.stringify(langs),
})
);
config.plugins.push(
new webpack.ProvidePlugin(
{
$: "jquery"
}
)
);
config.plugins.push(
new CompressionPlugin({
filename: "[path][base].gz[query]",
algorithm: "gzip",
test: /\.js$|\.css$|\.html$|\.svg?.+$|\.jpg$|\.ttf?.+$|\.woff?.+$|\.eot?.+$|\.json$/,
threshold: 10240,
minRatio: 0.8,
deleteOriginalAssets: false,
})
);
config.plugins.push(
new webpack.IgnorePlugin({
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/,
})
);
config.module.rules[2].use[0].options.aot = false;
const index = config.plugins.findIndex(p => p instanceof ngWebpack.AngularWebpackPlugin);
let angularWebpackPlugin = config.plugins[index];
if (config.mode === 'production') {
const angularCompilerOptions = angularWebpackPlugin.pluginOptions;
angularCompilerOptions.emitClassMetadata = true;
angularCompilerOptions.emitNgModuleScope = true;
config.plugins.splice(index, 1);
angularWebpackPlugin = new ngWebpack.AngularWebpackPlugin(angularCompilerOptions);
config.plugins.push(angularWebpackPlugin);
const javascriptOptimizerOptions = config.optimization.minimizer[0].options;
delete javascriptOptimizerOptions.define.ngJitMode;
config.optimization.minimizer.splice(0, 1);
config.optimization.minimizer.unshift(new JavaScriptOptimizerPlugin(javascriptOptimizerOptions));
}
addTransformerToAngularWebpackPlugin(angularWebpackPlugin, keysTransformer);
return config;
};
function addTransformerToAngularWebpackPlugin(plugin, transformer) {
const originalCreateFileEmitter = plugin.createFileEmitter; // private method
plugin.createFileEmitter = function (program, transformers, getExtraDependencies, onAfterEmit) {
if (!transformers) {
transformers = {};
}
if (!transformers.before) {
transformers = { before: [] };
}
transformers.before.push(transformer(program.getProgram()));
return originalCreateFileEmitter.apply(plugin, [program, transformers, getExtraDependencies, onAfterEmit]);
};
}

40
ui-ngx/generate-types.js

@ -19,8 +19,10 @@ const path = require('path');
const typeDir = path.join('.', 'target', 'types'); const typeDir = path.join('.', 'target', 'types');
const srcDir = path.join('.', 'target', 'types', 'src'); const srcDir = path.join('.', 'target', 'types', 'src');
const stylesCss = path.join(srcDir, 'styles.css');
const moduleMapPath = path.join('src', 'app', 'modules', 'common', 'modules-map.ts'); const moduleMapPath = path.join('src', 'app', 'modules', 'common', 'modules-map.ts');
const ngcPath = path.join('.', 'node_modules', '.bin', 'ngc'); const ngcPath = path.join('.', 'node_modules', '.bin', 'ngc');
const tailwindcss = path.join('.', 'node_modules', '.bin', 'tailwindcss');
const tsconfigPath = path.join('src', 'tsconfig.app.json'); const tsconfigPath = path.join('src', 'tsconfig.app.json');
console.log(`Remove directory: ${typeDir}`); console.log(`Remove directory: ${typeDir}`);
@ -31,12 +33,28 @@ try {
} }
const cliCommand = `${ngcPath} --p ${tsconfigPath} --declaration --outDir ${srcDir}`; const cliCommand = `${ngcPath} --p ${tsconfigPath} --declaration --outDir ${srcDir}`;
console.log(cliCommand); executeCliCommand(cliCommand, 'Build types');
try {
child_process.execSync(cliCommand); fromDir(srcDir, /(\.js|\.js\.map)$/, function (filename) {
} catch (err) { try {
console.error("Build types", err); fs.rmSync(filename);
process.exit(1); } catch (err) {
console.error(`Remove file error ${filename}: ${err}`);
}
});
fs.cpSync(moduleMapPath, `${typeDir}/${moduleMapPath}`);
const generateStyleCssCommand = `${tailwindcss} -o ${stylesCss} --minify`;
executeCliCommand(generateStyleCssCommand, 'Generate styles.css');
function executeCliCommand(cliCommand, description) {
console.log(cliCommand);
try {
child_process.execSync(cliCommand);
} catch (err) {
console.error(description, err);
process.exit(1);
}
} }
function fromDir(startPath, filter, callback) { function fromDir(startPath, filter, callback) {
@ -56,13 +74,3 @@ function fromDir(startPath, filter, callback) {
} }
} }
} }
fromDir(srcDir, /(\.js|\.js\.map)$/, function (filename) {
try {
fs.rmSync(filename);
} catch (err) {
console.error(`Remove file error ${filename}: ${err}`);
}
});
fs.cpSync(moduleMapPath, `${typeDir}/${moduleMapPath}`);

77
ui-ngx/package.json

@ -5,7 +5,7 @@
"ng": "ng", "ng": "ng",
"start": "node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng serve --configuration development --host 0.0.0.0 --open", "start": "node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng serve --configuration development --host 0.0.0.0 --open",
"build": "ng build", "build": "ng build",
"build:prod": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --configuration production --vendor-chunk", "build:prod": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --configuration production",
"build:types": "node generate-types.js", "build:types": "node generate-types.js",
"build:icon-metadata": "node generate-icon-metadata.js", "build:icon-metadata": "node generate-icon-metadata.js",
"lint": "ng lint", "lint": "ng lint",
@ -13,22 +13,22 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "18.2.6", "@angular/animations": "18.2.10",
"@angular/cdk": "18.2.6", "@angular/cdk": "18.2.11",
"@angular/common": "18.2.6", "@angular/common": "18.2.10",
"@angular/compiler": "18.2.6", "@angular/compiler": "18.2.10",
"@angular/core": "18.2.6", "@angular/core": "18.2.10",
"@angular/flex-layout": "^15.0.0-beta.42", "@angular/flex-layout": "^15.0.0-beta.42",
"@angular/forms": "18.2.6", "@angular/forms": "18.2.10",
"@angular/material": "18.2.6", "@angular/material": "18.2.11",
"@angular/platform-browser": "18.2.6", "@angular/platform-browser": "18.2.10",
"@angular/platform-browser-dynamic": "18.2.6", "@angular/platform-browser-dynamic": "18.2.10",
"@angular/router": "18.2.6", "@angular/router": "18.2.10",
"@auth0/angular-jwt": "^5.2.0", "@auth0/angular-jwt": "^5.2.0",
"@emotion/react": "11.13.3", "@emotion/react": "11.13.3",
"@emotion/styled": "11.13.0", "@emotion/styled": "11.13.0",
"@flowjs/flow.js": "^2.14.1", "@flowjs/flow.js": "^2.14.1",
"@flowjs/ngx-flow": "~0.8.1", "@flowjs/ngx-flow": "18.0.1",
"@fortawesome/fontawesome-svg-core": "^6.6.0", "@fortawesome/fontawesome-svg-core": "^6.6.0",
"@geoman-io/leaflet-geoman-free": "2.17.0", "@geoman-io/leaflet-geoman-free": "2.17.0",
"@iplab/ngx-color-picker": "^18.0.1", "@iplab/ngx-color-picker": "^18.0.1",
@ -41,9 +41,9 @@
"@mui/styles": "6.1.2", "@mui/styles": "6.1.2",
"@mui/system": "6.1.2", "@mui/system": "6.1.2",
"@mui/x-date-pickers": "7.18.0", "@mui/x-date-pickers": "7.18.0",
"@ngrx/effects": "^18.0.2", "@ngrx/effects": "^18.1.1",
"@ngrx/store": "^18.0.2", "@ngrx/store": "^18.1.1",
"@ngrx/store-devtools": "^18.0.2", "@ngrx/store-devtools": "^18.1.1",
"@ngx-translate/core": "^15.0.0", "@ngx-translate/core": "^15.0.0",
"@svgdotjs/svg.filter.js": "^3.0.8", "@svgdotjs/svg.filter.js": "^3.0.8",
"@svgdotjs/svg.js": "^3.2.4", "@svgdotjs/svg.js": "^3.2.4",
@ -105,26 +105,25 @@
"tinycolor2": "^1.6.0", "tinycolor2": "^1.6.0",
"tinymce": "~6.8.5", "tinymce": "~6.8.5",
"tooltipster": "^4.2.8", "tooltipster": "^4.2.8",
"ts-transformer-keys": "^0.4.4",
"tslib": "^2.7.0", "tslib": "^2.7.0",
"tv4": "^1.3.0", "tv4": "^1.3.0",
"typeface-roboto": "^1.1.13", "typeface-roboto": "^1.1.13",
"zone.js": "~0.14.10" "zone.js": "~0.14.10"
}, },
"devDependencies": { "devDependencies": {
"@angular-builders/custom-webpack": "~18.0.0", "@angular-builders/custom-esbuild": "18.0.0",
"@angular-devkit/build-angular": "18.2.7", "@angular-devkit/build-angular": "18.2.11",
"@angular-devkit/core": "18.2.7", "@angular-devkit/core": "18.2.11",
"@angular-devkit/schematics": "18.2.7", "@angular-devkit/schematics": "18.2.11",
"@angular-eslint/builder": "18.3.1", "@angular-eslint/builder": "18.4.0",
"@angular-eslint/eslint-plugin": "18.3.1", "@angular-eslint/eslint-plugin": "18.4.0",
"@angular-eslint/eslint-plugin-template": "18.3.1", "@angular-eslint/eslint-plugin-template": "18.4.0",
"@angular-eslint/schematics": "18.3.1", "@angular-eslint/schematics": "18.4.0",
"@angular-eslint/template-parser": "18.3.1", "@angular-eslint/template-parser": "18.4.0",
"@angular/cli": "18.2.7", "@angular/build": "18.2.11",
"@angular/compiler-cli": "18.2.6", "@angular/cli": "18.2.11",
"@angular/language-service": "18.2.6", "@angular/compiler-cli": "18.2.10",
"@ngtools/webpack": "18.2.7", "@angular/language-service": "18.2.10",
"@types/ace-diff": "^2.1.4", "@types/ace-diff": "^2.1.4",
"@types/canvas-gauges": "^2.1.8", "@types/canvas-gauges": "^2.1.8",
"@types/flot": "^0.0.36", "@types/flot": "^0.0.36",
@ -144,31 +143,31 @@
"@types/systemjs": "6.15.1", "@types/systemjs": "6.15.1",
"@types/tinycolor2": "^1.4.6", "@types/tinycolor2": "^1.4.6",
"@types/tooltipster": "^0.0.35", "@types/tooltipster": "^0.0.35",
"@typescript-eslint/eslint-plugin": "^8.11.0", "@typescript-eslint/eslint-plugin": "^8.12.2",
"@typescript-eslint/parser": "^8.11.0", "@typescript-eslint/parser": "^8.12.2",
"@typescript-eslint/utils": "^8.11.0", "@typescript-eslint/types": "^8.12.2",
"@typescript-eslint/utils": "^8.12.2",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"compression-webpack-plugin": "^11.1.0",
"directory-tree": "^3.5.2", "directory-tree": "^3.5.2",
"eslint": "~9.13.0", "eslint": "~9.13.0",
"eslint-plugin-import": "latest", "eslint-plugin-import": "latest",
"eslint-plugin-jsdoc": "latest", "eslint-plugin-jsdoc": "^50.4.3",
"eslint-plugin-prefer-arrow": "latest", "eslint-plugin-prefer-arrow": "latest",
"eslint-plugin-tailwindcss": "^3.17.5", "eslint-plugin-tailwindcss": "^3.17.5",
"ngrx-store-freeze": "^0.2.4", "ngrx-store-freeze": "^0.2.4",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"postcss": "^8.4.47", "postcss": "^8.4.47",
"postinstall-prepare": "^2.0.0", "postinstall-prepare": "^2.0.0",
"raw-loader": "^4.0.2", "tailwindcss": "^3.4.14",
"tailwindcss": "^3.4.13",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "~5.5.4", "typescript": "~5.5.4"
"webpack": "5.95.0"
}, },
"resolutions": { "resolutions": {
"@types/react": "18.3.10", "@types/react": "18.3.10",
"rc-virtual-list": "3.5.2", "rc-virtual-list": "3.5.2",
"ace-builds": "1.36.2", "ace-builds": "1.36.2",
"tinymce": "6.8.5" "tinymce": "6.8.5",
"rollup": "4.22.4",
"@babel/core": "7.25.2"
} }
} }

43
ui-ngx/patches/@angular+build+18.2.11.patch

@ -0,0 +1,43 @@
diff --git a/node_modules/@angular/build/src/tools/angular/compilation/angular-compilation.js b/node_modules/@angular/build/src/tools/angular/compilation/angular-compilation.js
index 625c621..4fc8bd8 100755
--- a/node_modules/@angular/build/src/tools/angular/compilation/angular-compilation.js
+++ b/node_modules/@angular/build/src/tools/angular/compilation/angular-compilation.js
@@ -68,8 +68,6 @@ class AngularCompilation {
allowEmptyCodegenFiles: false,
annotationsAs: 'decorators',
enableResourceInlining: false,
- supportTestBed: false,
- supportJitMode: false,
}));
}
async diagnoseFiles(modes = DiagnosticModes.All) {
diff --git a/node_modules/@angular/build/src/tools/esbuild/angular/compiler-plugin.js b/node_modules/@angular/build/src/tools/esbuild/angular/compiler-plugin.js
index b1bb6ea..c76b4c6 100755
--- a/node_modules/@angular/build/src/tools/esbuild/angular/compiler-plugin.js
+++ b/node_modules/@angular/build/src/tools/esbuild/angular/compiler-plugin.js
@@ -79,7 +79,7 @@ function createCompilerPlugin(pluginOptions, styleOptions) {
sourcemap: !!pluginOptions.sourcemap,
thirdPartySourcemaps: pluginOptions.thirdPartySourcemaps,
advancedOptimizations: pluginOptions.advancedOptimizations,
- jit: pluginOptions.jit,
+ jit: true, // pluginOptions.jit,
}, environment_options_1.maxWorkers, cacheStore?.createCache('jstransformer'));
// Setup defines based on the values used by the Angular compiler-cli
build.initialOptions.define ??= {};
@@ -377,12 +377,14 @@ function createCompilerPlugin(pluginOptions, styleOptions) {
async function hasSideEffects(path) {
if (!pluginOptions.advancedOptimizations) {
return undefined;
+ } else {
+ return true;
}
- const { sideEffects } = await build.resolve(path, {
+ /*const { sideEffects } = await build.resolve(path, {
kind: 'import-statement',
resolveDir: build.initialOptions.absWorkingDir ?? '',
});
- return sideEffects;
+ return sideEffects;*/
}
},
};

4
ui-ngx/patches/@angular+core+18.2.6.patch → ui-ngx/patches/@angular+core+18.2.10.patch

@ -1,8 +1,8 @@
diff --git a/node_modules/@angular/core/fesm2022/core.mjs b/node_modules/@angular/core/fesm2022/core.mjs diff --git a/node_modules/@angular/core/fesm2022/core.mjs b/node_modules/@angular/core/fesm2022/core.mjs
index 0fa881f..b844dfa 100755 index 6fe7ad6..291e4ed 100755
--- a/node_modules/@angular/core/fesm2022/core.mjs --- a/node_modules/@angular/core/fesm2022/core.mjs
+++ b/node_modules/@angular/core/fesm2022/core.mjs +++ b/node_modules/@angular/core/fesm2022/core.mjs
@@ -12868,13 +12868,13 @@ function findDirectiveDefMatches(tView, tNode) { @@ -12859,13 +12859,13 @@ function findDirectiveDefMatches(tView, tNode) {
if (isNodeMatchingSelectorList(tNode, def.selectors, /* isProjectionMode */ false)) { if (isNodeMatchingSelectorList(tNode, def.selectors, /* isProjectionMode */ false)) {
matches || (matches = []); matches || (matches = []);
if (isComponentDef(def)) { if (isComponentDef(def)) {

14
ui-ngx/patches/jquery.terminal+2.43.1.patch

File diff suppressed because one or more lines are too long

2
ui-ngx/pom.xml

@ -56,7 +56,7 @@
<goal>install-node-and-yarn</goal> <goal>install-node-and-yarn</goal>
</goals> </goals>
<configuration> <configuration>
<nodeVersion>v20.11.1</nodeVersion> <nodeVersion>v20.18.0</nodeVersion>
<yarnVersion>v1.22.22</yarnVersion> <yarnVersion>v1.22.22</yarnVersion>
</configuration> </configuration>
</execution> </execution>

35
ui-ngx/src/app/core/api/alias-controller.ts

@ -61,6 +61,9 @@ export class AliasController implements IAliasController {
resolvedAliases: { [aliasId: string]: AliasInfo } = {}; resolvedAliases: { [aliasId: string]: AliasInfo } = {};
resolvedAliasesObservable: { [aliasId: string]: Observable<AliasInfo> } = {}; resolvedAliasesObservable: { [aliasId: string]: Observable<AliasInfo> } = {};
resolvedDevices: { [deviceId: string]: EntityInfo } = {};
resolvedDevicesObservable: { [deviceId: string]: Observable<EntityInfo> } = {};
resolvedAliasesToStateEntities: { [aliasId: string]: StateEntityInfo } = {}; resolvedAliasesToStateEntities: { [aliasId: string]: StateEntityInfo } = {};
constructor(private utils: UtilsService, constructor(private utils: UtilsService,
@ -261,9 +264,35 @@ export class AliasController implements IAliasController {
} }
resolveSingleEntityInfoForDeviceId(deviceId: string): Observable<EntityInfo> { resolveSingleEntityInfoForDeviceId(deviceId: string): Observable<EntityInfo> {
const entityFilter = singleEntityFilterFromDeviceId(deviceId); let entityInfo = this.resolvedDevices[deviceId];
return this.entityService.findSingleEntityInfoByEntityFilter(entityFilter, if (entityInfo) {
{ignoreLoading: true, ignoreErrors: true}); return of(entityInfo);
} else if (this.resolvedDevicesObservable[deviceId]) {
return this.resolvedDevicesObservable[deviceId];
} else {
const resolvedDeviceSubject = new ReplaySubject<EntityInfo>();
this.resolvedDevicesObservable[deviceId] = resolvedDeviceSubject.asObservable();
const entityFilter = singleEntityFilterFromDeviceId(deviceId);
this.entityService.findSingleEntityInfoByEntityFilter(entityFilter,
{ignoreLoading: true, ignoreErrors: true}).subscribe(
(resolvedEntityInfo) => {
this.resolvedDevices[deviceId] = resolvedEntityInfo;
delete this.resolvedDevicesObservable[deviceId];
resolvedDeviceSubject.next(resolvedEntityInfo);
resolvedDeviceSubject.complete();
},
() => {
resolvedDeviceSubject.error(null);
delete this.resolvedDevicesObservable[deviceId];
}
);
entityInfo = this.resolvedDevices[deviceId];
if (entityInfo) {
return of(entityInfo);
} else {
return this.resolvedDevicesObservable[deviceId];
}
}
} }
resolveSingleEntityInfoForTargetDevice(targetDevice: TargetDevice): Observable<EntityInfo> { resolveSingleEntityInfoForTargetDevice(targetDevice: TargetDevice): Observable<EntityInfo> {

18
ui-ngx/src/app/core/interceptors/entity-conflict.interceptor.ts

@ -22,11 +22,11 @@ import { MatDialog } from '@angular/material/dialog';
import { import {
EntityConflictDialogComponent EntityConflictDialogComponent
} from '@shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component'; } from '@shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component';
import { HasId } from '@shared/models/base-data'; import { EntityInfoData, VersionedEntity } from '@shared/models/entity.models';
import { HasVersion } from '@shared/models/entity.models';
import { getInterceptorConfig } from './interceptor.util'; import { getInterceptorConfig } from './interceptor.util';
import { isDefined } from '@core/utils'; import { isDefined } from '@core/utils';
import { InterceptorConfig } from '@core/interceptors/interceptor-config'; import { InterceptorConfig } from '@core/interceptors/interceptor-config';
import { RuleChainMetaData } from '@shared/models/rule-chain.models';
@Injectable() @Injectable()
export class EntityConflictInterceptor implements HttpInterceptor { export class EntityConflictInterceptor implements HttpInterceptor {
@ -35,7 +35,7 @@ export class EntityConflictInterceptor implements HttpInterceptor {
private dialog: MatDialog, private dialog: MatDialog,
) {} ) {}
intercept(request: HttpRequest<unknown & HasId & HasVersion>, next: HttpHandler): Observable<HttpEvent<unknown>> { intercept(request: HttpRequest<VersionedEntity>, next: HttpHandler): Observable<HttpEvent<unknown>> {
if (!request.url.startsWith('/api/')) { if (!request.url.startsWith('/api/')) {
return next.handle(request); return next.handle(request);
} }
@ -52,11 +52,11 @@ export class EntityConflictInterceptor implements HttpInterceptor {
} }
private handleConflictError( private handleConflictError(
request: HttpRequest<unknown & HasId & HasVersion>, request: HttpRequest<VersionedEntity>,
next: HttpHandler, next: HttpHandler,
error: HttpErrorResponse error: HttpErrorResponse
): Observable<HttpEvent<unknown>> { ): Observable<HttpEvent<unknown>> {
if (getInterceptorConfig(request).ignoreVersionConflict) { if (getInterceptorConfig(request).ignoreVersionConflict || !this.isVersionedEntity(request.body)) {
return throwError(() => error); return throwError(() => error);
} }
@ -74,12 +74,16 @@ export class EntityConflictInterceptor implements HttpInterceptor {
); );
} }
private updateRequestVersion(request: HttpRequest<unknown & HasId & HasVersion>): HttpRequest<unknown & HasId & HasVersion> { private updateRequestVersion(request: HttpRequest<VersionedEntity>): HttpRequest<VersionedEntity> {
const body = { ...request.body, version: null }; const body = { ...request.body, version: null };
return request.clone({ body }); return request.clone({ body });
} }
private openConflictDialog(entity: unknown & HasId & HasVersion, message: string): Observable<boolean> { private isVersionedEntity(entity: VersionedEntity): boolean {
return !!((entity as EntityInfoData)?.id ?? (entity as RuleChainMetaData)?.ruleChainId)
}
private openConflictDialog(entity: VersionedEntity, message: string): Observable<boolean> {
const dialogRef = this.dialog.open(EntityConflictDialogComponent, { const dialogRef = this.dialog.open(EntityConflictDialogComponent, {
disableClose: true, disableClose: true,
data: { message, entity }, data: { message, entity },

2
ui-ngx/src/app/core/services/menu.models.ts

@ -516,7 +516,7 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
name: 'gateway.gateways', name: 'gateway.gateways',
type: 'link', type: 'link',
path: '/entities/gateways', path: '/entities/gateways',
icon: 'tb_gateway' icon: 'lan'
} }
], ],
[ [

19
ui-ngx/src/app/core/services/utils.service.ts

@ -40,24 +40,15 @@ import { WindowMessage } from '@shared/models/window-message.model';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { customTranslationsPrefix, i18nPrefix } from '@app/shared/models/constants'; import { customTranslationsPrefix, i18nPrefix } from '@app/shared/models/constants';
import { DataKey, Datasource, DatasourceType, KeyInfo } from '@shared/models/widget.models'; import { DataKey, Datasource, DatasourceType, KeyInfo } from '@shared/models/widget.models';
import { DataKeyType } from '@app/shared/models/telemetry/telemetry.models'; import { DataKeyType, SharedTelemetrySubscriber } from '@app/shared/models/telemetry/telemetry.models';
import { import { alarmFields, alarmSeverityTranslations, alarmStatusTranslations } from '@shared/models/alarm.models';
alarmFields,
alarmSeverityTranslations,
alarmStatusTranslations
} from '@shared/models/alarm.models';
import { materialColors } from '@app/shared/models/material.models'; import { materialColors } from '@app/shared/models/material.models';
import { WidgetInfo } from '@home/models/widget-component.models'; import { WidgetInfo } from '@home/models/widget-component.models';
import jsonSchemaDefaults from 'json-schema-defaults'; import jsonSchemaDefaults from 'json-schema-defaults';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { publishReplay, refCount } from 'rxjs/operators'; import { publishReplay, refCount } from 'rxjs/operators';
import { WidgetContext } from '@app/modules/home/models/widget-component.models'; import { WidgetContext } from '@app/modules/home/models/widget-component.models';
import { import { AttributeData, LatestTelemetry, TelemetryType } from '@shared/models/telemetry/telemetry.models';
AttributeData,
LatestTelemetry,
TelemetrySubscriber,
TelemetryType
} from '@shared/models/telemetry/telemetry.models';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { DatePipe, DOCUMENT } from '@angular/common'; import { DatePipe, DOCUMENT } from '@angular/common';
import { entityTypeTranslations } from '@shared/models/entity-type.models'; import { entityTypeTranslations } from '@shared/models/entity-type.models';
@ -483,13 +474,13 @@ export class UtilsService {
if (!entityId && ctx.datasources.length > 0) { if (!entityId && ctx.datasources.length > 0) {
entityId = this.getEntityIdFromDatasource(ctx.datasources[0]); entityId = this.getEntityIdFromDatasource(ctx.datasources[0]);
} }
const subscription = TelemetrySubscriber.createEntityAttributesSubscription(ctx.telemetryWsService, entityId, type, ctx.ngZone, keys); const subscription = SharedTelemetrySubscriber.createEntityAttributesSubscription(ctx.telemetryWsService, entityId, type, ctx.ngZone, keys);
if (!ctx.telemetrySubscribers) { if (!ctx.telemetrySubscribers) {
ctx.telemetrySubscribers = []; ctx.telemetrySubscribers = [];
} }
ctx.telemetrySubscribers.push(subscription); ctx.telemetrySubscribers.push(subscription);
subscription.subscribe(); subscription.subscribe();
return subscription.attributeData$().pipe( return subscription.attributeData$.pipe(
publishReplay(1), publishReplay(1),
refCount() refCount()
); );

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

@ -894,3 +894,11 @@ export const camelCase = (str: string): string => {
export const convertKeysToCamelCase = (obj: Record<string, any>): Record<string, any> => { export const convertKeysToCamelCase = (obj: Record<string, any>): Record<string, any> => {
return _.mapKeys(obj, (value, key) => _.camelCase(key)); return _.mapKeys(obj, (value, key) => _.camelCase(key));
}; };
export const unwrapModule = (module: any) : any => {
if ('default' in module && Object.keys(module).length === 1) {
return module.default;
} else {
return module;
}
};

2
ui-ngx/src/app/modules/common/modules-map.ts

@ -100,7 +100,6 @@ import * as TruncateWithTooltipDirective from '@shared/directives/truncate-with-
import * as coercion from '@shared/decorators/coercion'; import * as coercion from '@shared/decorators/coercion';
import * as enumerable from '@shared/decorators/enumerable'; import * as enumerable from '@shared/decorators/enumerable';
import * as TbInject from '@shared/decorators/tb-inject';
import * as FooterComponent from '@shared/components/footer.component'; import * as FooterComponent from '@shared/components/footer.component';
import * as LogoComponent from '@shared/components/logo.component'; import * as LogoComponent from '@shared/components/logo.component';
@ -435,7 +434,6 @@ class ModulesMap implements IModulesMap {
'@shared/decorators/coercion': coercion, '@shared/decorators/coercion': coercion,
'@shared/decorators/enumerable': enumerable, '@shared/decorators/enumerable': enumerable,
'@shared/decorators/tb-inject': TbInject,
'@shared/import-export/import-export.service': ImportExportService, '@shared/import-export/import-export.service': ImportExportService,
'@shared/import-export/import-dialog.component': ImportDialogComponent, '@shared/import-export/import-dialog.component': ImportDialogComponent,

2
ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html

@ -173,7 +173,7 @@
<button mat-icon-button <button mat-icon-button
type="button" type="button"
matSuffix matSuffix
*ngIf="getAlarmCommentFormControl().value" *ngIf="getAlarmCommentValue()"
(click)="saveComment()"> (click)="saveComment()">
<mat-icon color="primary"> <mat-icon color="primary">
send send

12
ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts

@ -156,7 +156,7 @@ export class AlarmCommentComponent implements OnInit {
} }
saveComment(): void { saveComment(): void {
const commentInputValue: string = this.getAlarmCommentFormControl().value; const commentInputValue: string = this.getAlarmCommentValue();
if (commentInputValue) { if (commentInputValue) {
const comment: AlarmComment = { const comment: AlarmComment = {
alarmId: { alarmId: {
@ -174,7 +174,7 @@ export class AlarmCommentComponent implements OnInit {
} }
saveEditedComment(commentId: string): void { saveEditedComment(commentId: string): void {
const commentEditInputValue: string = this.getAlarmCommentEditFormControl().value; const commentEditInputValue: string = this.getAlarmCommentEditValue();
if (commentEditInputValue) { if (commentEditInputValue) {
const editedComment: AlarmComment = this.getAlarmCommentById(commentId); const editedComment: AlarmComment = this.getAlarmCommentById(commentId);
editedComment.comment.text = commentEditInputValue; editedComment.comment.text = commentEditInputValue;
@ -277,6 +277,14 @@ export class AlarmCommentComponent implements OnInit {
return this.alarmCommentFormGroup.get('alarmCommentEdit'); return this.alarmCommentFormGroup.get('alarmCommentEdit');
} }
getAlarmCommentValue(): string {
return this.alarmCommentFormGroup.get('alarmComment').value.trim();
}
private getAlarmCommentEditValue(): string {
return this.alarmCommentFormGroup.get('alarmCommentEdit').value.trim();
}
private clearCommentInput(): void { private clearCommentInput(): void {
this.getAlarmCommentFormControl().patchValue(''); this.getAlarmCommentFormControl().patchValue('');
} }

2
ui-ngx/src/app/modules/home/components/vc/entity-version-diff.component.ts

@ -121,7 +121,7 @@ export class EntityVersionDiffComponent extends PageComponent implements OnInit,
this.popoverComponent.updatePosition(); this.popoverComponent.updatePosition();
} }
setTimeout(() => { setTimeout(() => {
this.differ = new aceDiff.default( this.differ = new aceDiff(
{ {
element: this.diffViewerElmRef.nativeElement, element: this.diffViewerElmRef.nativeElement,
mode: 'ace/mode/json', mode: 'ace/mode/json',

20
ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.component.ts

@ -15,16 +15,13 @@
/// ///
import { MatDialogRef } from '@angular/material/dialog'; import { MatDialogRef } from '@angular/material/dialog';
import { Directive, InjectionToken } from '@angular/core'; import { Directive, inject, InjectionToken } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { CustomDialogContainerComponent } from './custom-dialog-container.component'; import { CustomDialogContainerComponent } from './custom-dialog-container.component';
import { UntypedFormBuilder, Validators } from '@angular/forms'; import { UntypedFormBuilder, Validators } from '@angular/forms';
import { TbInject } from '@shared/decorators/tb-inject';
export const CUSTOM_DIALOG_DATA = new InjectionToken<any>('ConfigDialogData'); export const CUSTOM_DIALOG_DATA = new InjectionToken<CustomDialogData>('ConfigDialogData');
export interface CustomDialogData { export interface CustomDialogData {
controller: (instance: CustomDialogComponent) => void; controller: (instance: CustomDialogComponent) => void;
@ -37,12 +34,13 @@ export class CustomDialogComponent extends PageComponent {
[key: string]: any; [key: string]: any;
constructor(@TbInject(Store) protected store: Store<AppState>, protected router = inject(Router);
@TbInject(Router) protected router: Router, public dialogRef = inject(MatDialogRef<CustomDialogContainerComponent>);
@TbInject(MatDialogRef) public dialogRef: MatDialogRef<CustomDialogContainerComponent>, public data = inject(CUSTOM_DIALOG_DATA);
@TbInject(UntypedFormBuilder) public fb: UntypedFormBuilder, public fb = inject(UntypedFormBuilder);
@TbInject(CUSTOM_DIALOG_DATA) public data: CustomDialogData) {
super(store); constructor() {
super();
// @ts-ignore // @ts-ignore
this.validators = Validators; this.validators = Validators;
this.data.controller(this); this.data.controller(this);

77
ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts

@ -15,10 +15,13 @@
/// ///
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Directive, Injector, OnDestroy, OnInit, TemplateRef } from '@angular/core'; import { Directive, inject, Injector, OnDestroy, OnInit } from '@angular/core';
import { Store } from '@ngrx/store'; import {
import { AppState } from '@core/core.state'; IDynamicWidgetComponent,
import { IDynamicWidgetComponent, WidgetContext } from '@home/models/widget-component.models'; widgetContextToken,
widgetErrorMessagesToken,
widgetTitlePanelToken
} from '@home/models/widget-component.models';
import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { RafService } from '@core/services/raf.service'; import { RafService } from '@core/services/raf.service';
import { import {
@ -45,7 +48,6 @@ import { DatePipe } from '@angular/common';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { TbInject } from '@shared/decorators/tb-inject';
import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
import { UserSettingsService } from '@core/http/user-settings.service'; import { UserSettingsService } from '@core/http/user-settings.service';
import { ImagePipe } from '@shared/pipe/image.pipe'; import { ImagePipe } from '@shared/pipe/image.pipe';
@ -64,38 +66,39 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid
validators = Validators; validators = Validators;
constructor(@TbInject(RafService) public raf: RafService, public raf = inject(RafService);
@TbInject(Store) protected store: Store<AppState>, public fb = inject(UntypedFormBuilder);
@TbInject(UntypedFormBuilder) public fb: UntypedFormBuilder, public readonly $injector = inject(Injector);
@TbInject(Injector) public readonly $injector: Injector, public readonly ctx = inject(widgetContextToken);
@TbInject('widgetContext') public readonly ctx: WidgetContext, public readonly errorMessages = inject(widgetErrorMessagesToken);
@TbInject('errorMessages') public readonly errorMessages: string[], public readonly widgetTitlePanel = inject(widgetTitlePanelToken);
@TbInject('widgetTitlePanel') public readonly widgetTitlePanel: TemplateRef<any>) {
super(store); constructor() {
this.ctx.$injector = $injector; super();
this.ctx.deviceService = $injector.get(DeviceService); this.ctx.$injector = this.$injector;
this.ctx.assetService = $injector.get(AssetService); this.ctx.deviceService = this.$injector.get(DeviceService);
this.ctx.entityViewService = $injector.get(EntityViewService); this.ctx.assetService = this.$injector.get(AssetService);
this.ctx.customerService = $injector.get(CustomerService); this.ctx.entityViewService = this.$injector.get(EntityViewService);
this.ctx.dashboardService = $injector.get(DashboardService); this.ctx.customerService = this.$injector.get(CustomerService);
this.ctx.userService = $injector.get(UserService); this.ctx.dashboardService = this.$injector.get(DashboardService);
this.ctx.attributeService = $injector.get(AttributeService); this.ctx.userService = this.$injector.get(UserService);
this.ctx.entityRelationService = $injector.get(EntityRelationService); this.ctx.attributeService = this.$injector.get(AttributeService);
this.ctx.entityService = $injector.get(EntityService); this.ctx.entityRelationService = this.$injector.get(EntityRelationService);
this.ctx.authService = $injector.get(AuthService); this.ctx.entityService = this.$injector.get(EntityService);
this.ctx.dialogs = $injector.get(DialogService); this.ctx.authService = this.$injector.get(AuthService);
this.ctx.customDialog = $injector.get(CustomDialogService); this.ctx.dialogs = this.$injector.get(DialogService);
this.ctx.resourceService = $injector.get(ResourceService); this.ctx.customDialog = this.$injector.get(CustomDialogService);
this.ctx.userSettingsService = $injector.get(UserSettingsService); this.ctx.resourceService = this.$injector.get(ResourceService);
this.ctx.utilsService = $injector.get(UtilsService); this.ctx.userSettingsService = this.$injector.get(UserSettingsService);
this.ctx.telemetryWsService = $injector.get(TelemetryWebsocketService); this.ctx.utilsService = this.$injector.get(UtilsService);
this.ctx.date = $injector.get(DatePipe); this.ctx.telemetryWsService = this.$injector.get(TelemetryWebsocketService);
this.ctx.imagePipe = $injector.get(ImagePipe); this.ctx.date = this.$injector.get(DatePipe);
this.ctx.milliSecondsToTimeString = $injector.get(MillisecondsToTimeStringPipe); this.ctx.imagePipe = this.$injector.get(ImagePipe);
this.ctx.translate = $injector.get(TranslateService); this.ctx.milliSecondsToTimeString = this.$injector.get(MillisecondsToTimeStringPipe);
this.ctx.http = $injector.get(HttpClient); this.ctx.translate = this.$injector.get(TranslateService);
this.ctx.sanitizer = $injector.get(DomSanitizer); this.ctx.http = this.$injector.get(HttpClient);
this.ctx.router = $injector.get(Router); this.ctx.sanitizer = this.$injector.get(DomSanitizer);
this.ctx.router = this.$injector.get(Router);
this.ctx.$scope = this; this.ctx.$scope = this;
if (this.ctx.defaultSubscription) { if (this.ctx.defaultSubscription) {

9
ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.models.ts

@ -17,8 +17,7 @@
import { import {
AttributeData, AttributeData,
AttributeScope, AttributeScope,
LatestTelemetry, LatestTelemetry, SharedTelemetrySubscriber,
TelemetrySubscriber,
TelemetryType, TelemetryType,
telemetryTypeTranslationsShort telemetryTypeTranslationsShort
} from '@shared/models/telemetry/telemetry.models'; } from '@shared/models/telemetry/telemetry.models';
@ -436,7 +435,7 @@ export class ExecuteRpcValueGetter<V> extends ValueGetter<V> {
export abstract class TelemetryValueGetter<V, S extends TelemetryValueSettings> extends ValueGetter<V> { export abstract class TelemetryValueGetter<V, S extends TelemetryValueSettings> extends ValueGetter<V> {
protected targetEntityId: EntityId; protected targetEntityId: EntityId;
private telemetrySubscriber: TelemetrySubscriber; private telemetrySubscriber: SharedTelemetrySubscriber;
protected constructor(protected ctx: WidgetContext, protected constructor(protected ctx: WidgetContext,
protected settings: GetValueSettings<V>, protected settings: GetValueSettings<V>,
@ -470,10 +469,10 @@ export abstract class TelemetryValueGetter<V, S extends TelemetryValueSettings>
private subscribeForTelemetryValue(): Observable<V> { private subscribeForTelemetryValue(): Observable<V> {
this.telemetrySubscriber = this.telemetrySubscriber =
TelemetrySubscriber.createEntityAttributesSubscription(this.ctx.telemetryWsService, this.targetEntityId, SharedTelemetrySubscriber.createEntityAttributesSubscription(this.ctx.telemetryWsService, this.targetEntityId,
this.scope(), this.ctx.ngZone, [this.getTelemetryValueSettings().key]); this.scope(), this.ctx.ngZone, [this.getTelemetryValueSettings().key]);
this.telemetrySubscriber.subscribe(); this.telemetrySubscriber.subscribe();
return this.telemetrySubscriber.attributeData$().pipe( return this.telemetrySubscriber.attributeData$.pipe(
map((data) => { map((data) => {
let value: V = null; let value: V = null;
const entry = data.find(attr => attr.key === this.getTelemetryValueSettings().key); const entry = data.find(attr => attr.key === this.getTelemetryValueSettings().key);

4
ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts

@ -24,7 +24,7 @@ import { WidgetContext } from '@home/models/widget-component.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { MINUTE } from '@shared/models/time/time.models'; import { MINUTE } from '@shared/models/time/time.models';
import { isDefinedAndNotNull, mergeDeep } from '@core/utils'; import { isDefinedAndNotNull, mergeDeep, unwrapModule } from '@core/utils';
import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/widget-settings.models'; import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/widget-settings.models';
import { ImagePipe } from '@shared/pipe/image.pipe'; import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
@ -157,7 +157,7 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI
private updateQRCode(link: string) { private updateQRCode(link: string) {
import('qrcode').then((QRCode) => { import('qrcode').then((QRCode) => {
QRCode.toString(link, (err, string) => { unwrapModule(QRCode).toString(link, (err, string) => {
this.qrCodeSVG = string; this.qrCodeSVG = string;
this.cd.markForCheck(); this.cd.markForCheck();
}) })

4
ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.ts

@ -28,7 +28,7 @@ import {
isNumber, isNumber,
isObject, isObject,
parseFunction, parseFunction,
safeExecute safeExecute, unwrapModule
} from '@core/utils'; } from '@core/utils';
interface QrCodeWidgetSettings { interface QrCodeWidgetSettings {
@ -124,7 +124,7 @@ export class QrCodeWidgetComponent extends PageComponent implements OnInit, Afte
private updateCanvas() { private updateCanvas() {
if (this.viewInited) { if (this.viewInited) {
import('qrcode').then((QRCode) => { import('qrcode').then((QRCode) => {
QRCode.toCanvas(this.canvasRef.nativeElement, this.qrCodeText); unwrapModule(QRCode).toCanvas(this.canvasRef.nativeElement, this.qrCodeText);
this.canvasRef.nativeElement.style.width = 'auto'; this.canvasRef.nativeElement.style.width = 'auto';
this.canvasRef.nativeElement.style.height = 'auto'; this.canvasRef.nativeElement.style.height = 'auto';
}); });

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action.models.ts

@ -19,9 +19,9 @@ import { widgetContextCompletions } from '@shared/models/ace/widget-completion.m
import { entityIdHref, entityTypeHref, serviceCompletions } from '@shared/models/ace/service-completion.models'; import { entityIdHref, entityTypeHref, serviceCompletions } from '@shared/models/ace/service-completion.models';
import { CustomActionDescriptor, WidgetAction } from '@shared/models/widget.models'; import { CustomActionDescriptor, WidgetAction } from '@shared/models/widget.models';
import { deepClone, isDefined, isUndefined } from '@core/utils'; import { deepClone, isDefined, isUndefined } from '@core/utils';
import customSampleJs from '!raw-loader!./custom-sample-js.raw'; import customSampleJs from './custom-sample-js.raw';
import customSampleCss from '!raw-loader!./custom-sample-css.raw'; import customSampleCss from './custom-sample-css.raw';
import customSampleHtml from '!raw-loader!./custom-sample-html.raw'; import customSampleHtml from './custom-sample-html.raw';
const customActionCompletions: TbEditorCompletions = { const customActionCompletions: TbEditorCompletions = {
...{ ...{

15
ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-provider-settings.component.ts

@ -29,6 +29,10 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { import {
defaultGoogleMapProviderSettings,
defaultHereMapProviderSettings,
defaultImageMapProviderSettings,
defaultOpenStreetMapProviderSettings, defaultTencentMapProviderSettings,
GoogleMapProviderSettings, GoogleMapProviderSettings,
HereMapProviderSettings, HereMapProviderSettings,
ImageMapProviderSettings, ImageMapProviderSettings,
@ -39,7 +43,6 @@ import {
TencentMapProviderSettings TencentMapProviderSettings
} from '@home/components/widget/lib/maps/map-models'; } from '@home/components/widget/lib/maps/map-models';
import { extractType } from '@core/utils'; import { extractType } from '@core/utils';
import { keys } from 'ts-transformer-keys';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
@Component({ @Component({
@ -128,11 +131,11 @@ export class MapProviderSettingsComponent extends PageComponent implements OnIni
writeValue(value: MapProviderSettings): void { writeValue(value: MapProviderSettings): void {
this.modelValue = value; this.modelValue = value;
const provider = value?.provider; const provider = value?.provider;
const googleProviderSettings = extractType<GoogleMapProviderSettings>(value, keys<GoogleMapProviderSettings>()); const googleProviderSettings = extractType<GoogleMapProviderSettings>(value, Object.keys(defaultGoogleMapProviderSettings) as (keyof GoogleMapProviderSettings)[]);
const openstreetProviderSettings = extractType<OpenStreetMapProviderSettings>(value, keys<OpenStreetMapProviderSettings>()); const openstreetProviderSettings = extractType<OpenStreetMapProviderSettings>(value, Object.keys(defaultOpenStreetMapProviderSettings) as (keyof OpenStreetMapProviderSettings)[]);
const hereProviderSettings = extractType<HereMapProviderSettings>(value, keys<HereMapProviderSettings>()); const hereProviderSettings = extractType<HereMapProviderSettings>(value, Object.keys(defaultHereMapProviderSettings) as (keyof HereMapProviderSettings)[]);
const imageMapProviderSettings = extractType<ImageMapProviderSettings>(value, keys<ImageMapProviderSettings>()); const imageMapProviderSettings = extractType<ImageMapProviderSettings>(value, Object.keys(defaultImageMapProviderSettings) as (keyof ImageMapProviderSettings)[]);
const tencentMapProviderSettings = extractType<TencentMapProviderSettings>(value, keys<TencentMapProviderSettings>()); const tencentMapProviderSettings = extractType<TencentMapProviderSettings>(value, Object.keys(defaultTencentMapProviderSettings) as (keyof TencentMapProviderSettings)[]);
this.providerSettingsFormGroup.patchValue( this.providerSettingsFormGroup.patchValue(
{ {
provider, provider,

26
ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-settings.component.ts

@ -17,11 +17,11 @@
import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { import {
ControlValueAccessor, ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormBuilder, UntypedFormBuilder,
UntypedFormControl, UntypedFormControl,
UntypedFormGroup, UntypedFormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
Validator Validator
} from '@angular/forms'; } from '@angular/forms';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
@ -31,6 +31,13 @@ import { TranslateService } from '@ngx-translate/core';
import { import {
CircleSettings, CircleSettings,
CommonMapSettings, CommonMapSettings,
defaultCircleSettings,
defaultCommonMapSettings,
defaultMapEditorSettings,
defaultMapProviderSettings,
defaultMarkerClusteringSettings,
defaultMarkersSettings,
defaultPolygonSettings,
MapEditorSettings, MapEditorSettings,
MapProviders, MapProviders,
MapProviderSettings, MapProviderSettings,
@ -41,7 +48,6 @@ import {
UnitedMapSettings UnitedMapSettings
} from '@home/components/widget/lib/maps/map-models'; } from '@home/components/widget/lib/maps/map-models';
import { extractType } from '@core/utils'; import { extractType } from '@core/utils';
import { keys } from 'ts-transformer-keys';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { Widget } from '@shared/models/widget.models'; import { Widget } from '@shared/models/widget.models';
@ -140,12 +146,12 @@ export class MapSettingsComponent extends PageComponent implements OnInit, Contr
writeValue(value: UnitedMapSettings): void { writeValue(value: UnitedMapSettings): void {
this.modelValue = value; this.modelValue = value;
const mapProviderSettings = extractType<MapProviderSettings>(value, keys<MapProviderSettings>()); const mapProviderSettings = extractType<MapProviderSettings>(value, Object.keys(defaultMapProviderSettings) as (keyof MapProviderSettings)[]);
const commonMapSettings = extractType<CommonMapSettings>(value, keys<CommonMapSettings>()); const commonMapSettings = extractType<CommonMapSettings>(value, Object.keys(defaultCommonMapSettings) as (keyof CommonMapSettings)[]);
const markersSettings = extractType<MarkersSettings>(value, keys<MarkersSettings>()); const markersSettings = extractType<MarkersSettings>(value, Object.keys(defaultMarkersSettings) as (keyof MarkersSettings)[]);
const polygonSettings = extractType<PolygonSettings>(value, keys<PolygonSettings>()); const polygonSettings = extractType<PolygonSettings>(value, Object.keys(defaultPolygonSettings) as (keyof PolygonSettings)[]);
const circleSettings = extractType<CircleSettings>(value, keys<CircleSettings>()); const circleSettings = extractType<CircleSettings>(value, Object.keys(defaultCircleSettings) as (keyof CircleSettings)[]);
const mapEditorSettings = extractType<MapEditorSettings>(value, keys<MapEditorSettings>()); const mapEditorSettings = extractType<MapEditorSettings>(value, Object.keys(defaultMapEditorSettings) as (keyof MapEditorSettings)[]);
const formValue = { const formValue = {
mapProviderSettings, mapProviderSettings,
commonMapSettings, commonMapSettings,
@ -157,7 +163,7 @@ export class MapSettingsComponent extends PageComponent implements OnInit, Contr
if (this.routeMap) { if (this.routeMap) {
formValue.routeMapSettings = extractType<PolylineSettings>(value, ['strokeWeight', 'strokeOpacity']); formValue.routeMapSettings = extractType<PolylineSettings>(value, ['strokeWeight', 'strokeOpacity']);
} else { } else {
formValue.markerClusteringSettings = extractType<MarkerClusteringSettings>(value, keys<MarkerClusteringSettings>()); formValue.markerClusteringSettings = extractType<MarkerClusteringSettings>(value, Object.keys(defaultMarkerClusteringSettings) as (keyof MarkerClusteringSettings)[]);
} }
this.mapSettingsFormGroup.patchValue( formValue, {emitEvent: false} ); this.mapSettingsFormGroup.patchValue( formValue, {emitEvent: false} );
this.updateValidators(false); this.updateValidators(false);

28
ui-ngx/src/app/modules/home/components/widget/lib/settings/map/trip-animation-widget-settings.component.ts

@ -21,16 +21,22 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { import {
CircleSettings, CircleSettings,
CommonMapSettings,
defaultCircleSettings,
defaultCommonMapSettings,
defaultMapProviderSettings,
defaultMarkersSettings,
defaultPolygonSettings,
defaultTripAnimationPathSettings,
defaultTripAnimationPointSettings,
defaultTripAnimationSettings, defaultTripAnimationSettings,
MapProviderSettings, MapProviderSettings,
MarkersSettings,
PointsSettings, PointsSettings,
PolygonSettings, PolygonSettings,
PolylineSettings, PolylineSettings
TripAnimationCommonSettings,
TripAnimationMarkerSettings
} from 'src/app/modules/home/components/widget/lib/maps/map-models'; } from 'src/app/modules/home/components/widget/lib/maps/map-models';
import { extractType } from '@core/utils'; import { extractType } from '@core/utils';
import { keys } from 'ts-transformer-keys';
@Component({ @Component({
selector: 'tb-trip-animation-widget-settings', selector: 'tb-trip-animation-widget-settings',
@ -69,13 +75,13 @@ export class TripAnimationWidgetSettingsComponent extends WidgetSettingsComponen
} }
protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { protected prepareInputSettings(settings: WidgetSettings): WidgetSettings {
const mapProviderSettings = extractType<MapProviderSettings>(settings, keys<MapProviderSettings>()); const mapProviderSettings = extractType<MapProviderSettings>(settings, Object.keys(defaultMapProviderSettings) as (keyof MapProviderSettings)[]);
const commonMapSettings = extractType<TripAnimationCommonSettings>(settings, keys<TripAnimationCommonSettings>()); const commonMapSettings = extractType<CommonMapSettings>(settings, Object.keys(defaultCommonMapSettings) as (keyof CommonMapSettings)[]);
const markersSettings = extractType<TripAnimationMarkerSettings>(settings, keys<TripAnimationMarkerSettings>()); const markersSettings = extractType<MarkersSettings>(settings, Object.keys(defaultMarkersSettings) as (keyof MarkersSettings)[]);
const pathSettings = extractType<PolylineSettings>(settings, keys<PolylineSettings>()); const pathSettings = extractType<PolylineSettings>(settings, Object.keys(defaultTripAnimationPathSettings) as (keyof PolylineSettings)[]);
const pointSettings = extractType<PointsSettings>(settings, keys<PointsSettings>()); const pointSettings = extractType<PointsSettings>(settings, Object.keys(defaultTripAnimationPointSettings) as (keyof PointsSettings)[]);
const polygonSettings = extractType<PolygonSettings>(settings, keys<PolygonSettings>()); const polygonSettings = extractType<PolygonSettings>(settings, Object.keys(defaultPolygonSettings) as (keyof PolygonSettings)[]);
const circleSettings = extractType<CircleSettings>(settings, keys<CircleSettings>()); const circleSettings = extractType<CircleSettings>(settings, Object.keys(defaultCircleSettings) as (keyof CircleSettings)[]);
return { return {
mapProviderSettings, mapProviderSettings,
commonMapSettings, commonMapSettings,

8
ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts

@ -129,11 +129,13 @@ export class WidgetComponentService {
w.tinycolor = tinycolor; w.tinycolor = tinycolor;
w.cssjs = cssjs; w.cssjs = cssjs;
w.moment = moment; w.moment = moment;
w.$ = $;
w.jQuery = $;
const widgetModulesTasks: Observable<any>[] = []; const widgetModulesTasks: Observable<any>[] = [];
widgetModulesTasks.push(from(import('jquery.terminal'))); widgetModulesTasks.push(from(import('jquery.terminal')).pipe(
tap((mod) => {
mod.default(window, $);
})
));
widgetModulesTasks.push(from(import('flot/src/jquery.flot.js')).pipe( widgetModulesTasks.push(from(import('flot/src/jquery.flot.js')).pipe(
mergeMap(() => { mergeMap(() => {

3
ui-ngx/src/app/modules/home/components/widget/widget-container.component.html

@ -38,6 +38,7 @@
[class]="{'tb-widget-actions-absolute': !(widget.showWidgetTitlePanel && !widgetComponent.widgetContext?.embedTitlePanel && (widget.showTitle||widget.hasAggregation))}" [class]="{'tb-widget-actions-absolute': !(widget.showWidgetTitlePanel && !widgetComponent.widgetContext?.embedTitlePanel && (widget.showTitle||widget.hasAggregation))}"
(mousedown)="$event.stopPropagation()"> (mousedown)="$event.stopPropagation()">
<button mat-icon-button *ngFor="let action of widget.customHeaderActions" <button mat-icon-button *ngFor="let action of widget.customHeaderActions"
type="button"
[class.!hidden]="isEdit" [class.!hidden]="isEdit"
(click)="action.onAction($event)" (click)="action.onAction($event)"
matTooltip="{{ action.displayName }}" matTooltip="{{ action.displayName }}"
@ -45,6 +46,7 @@
<tb-icon>{{ action.icon }}</tb-icon> <tb-icon>{{ action.icon }}</tb-icon>
</button> </button>
<button mat-icon-button *ngFor="let action of widget.widgetActions" <button mat-icon-button *ngFor="let action of widget.widgetActions"
type="button"
[class.!hidden]="isEdit || !action.show" [class.!hidden]="isEdit || !action.show"
(click)="action.onAction($event)" (click)="action.onAction($event)"
matTooltip="{{ action.name | translate }}" matTooltip="{{ action.name | translate }}"
@ -52,6 +54,7 @@
<tb-icon>{{ action.icon }}</tb-icon> <tb-icon>{{ action.icon }}</tb-icon>
</button> </button>
<button mat-icon-button <button mat-icon-button
type="button"
[class.!hidden]="isEdit || !widget.enableFullscreen" [class.!hidden]="isEdit || !widget.enableFullscreen"
(click)="$event.stopPropagation(); widget.isFullscreen = !widget.isFullscreen; updateEditWidgetActionsTooltipState()" (click)="$event.stopPropagation(); widget.isFullscreen = !widget.isFullscreen; updateEditWidgetActionsTooltipState()"
matTooltip="{{(widget.isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" matTooltip="{{(widget.isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"

10
ui-ngx/src/app/modules/home/components/widget/widget.component.ts

@ -70,9 +70,9 @@ import {
IDynamicWidgetComponent, IDynamicWidgetComponent,
ShowWidgetHeaderActionFunction, ShowWidgetHeaderActionFunction,
updateEntityParams, updateEntityParams,
WidgetContext, WidgetContext, widgetContextToken, widgetErrorMessagesToken,
WidgetHeaderAction, WidgetHeaderAction,
WidgetInfo, WidgetInfo, widgetTitlePanelToken,
WidgetTypeInstance WidgetTypeInstance
} from '@home/models/widget-component.models'; } from '@home/models/widget-component.models';
import { import {
@ -743,15 +743,15 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
{ {
providers: [ providers: [
{ {
provide: 'widgetContext', provide: widgetContextToken,
useValue: this.widgetContext useValue: this.widgetContext
}, },
{ {
provide: 'errorMessages', provide: widgetErrorMessagesToken,
useValue: this.errorMessages useValue: this.errorMessages
}, },
{ {
provide: 'widgetTitlePanel', provide: widgetTitlePanelToken,
useValue: this.widgetTitlePanel useValue: this.widgetTitlePanel
} }
], ],

8
ui-ngx/src/app/modules/home/models/datasource/attribute-datasource.ts

@ -25,7 +25,7 @@ import {
AttributeData, AttributeData,
AttributeScope, AttributeScope,
isClientSideTelemetryType, isClientSideTelemetryType,
TelemetrySubscriber, SharedTelemetrySubscriber,
TelemetryType TelemetryType
} from '@shared/models/telemetry/telemetry.models'; } from '@shared/models/telemetry/telemetry.models';
import { AttributeService } from '@core/http/attribute.service'; import { AttributeService } from '@core/http/attribute.service';
@ -42,7 +42,7 @@ export class AttributeDatasource implements DataSource<AttributeData> {
public selection = new SelectionModel<AttributeData>(true, []); public selection = new SelectionModel<AttributeData>(true, []);
private allAttributes: Observable<Array<AttributeData>>; private allAttributes: Observable<Array<AttributeData>>;
private telemetrySubscriber: TelemetrySubscriber; private telemetrySubscriber: SharedTelemetrySubscriber;
constructor(private attributeService: AttributeService, constructor(private attributeService: AttributeService,
private telemetryWsService: TelemetryWebsocketService, private telemetryWsService: TelemetryWebsocketService,
@ -99,10 +99,10 @@ export class AttributeDatasource implements DataSource<AttributeData> {
if (!this.allAttributes) { if (!this.allAttributes) {
let attributesObservable: Observable<Array<AttributeData>>; let attributesObservable: Observable<Array<AttributeData>>;
if (isClientSideTelemetryType.get(attributesScope)) { if (isClientSideTelemetryType.get(attributesScope)) {
this.telemetrySubscriber = TelemetrySubscriber.createEntityAttributesSubscription( this.telemetrySubscriber = SharedTelemetrySubscriber.createEntityAttributesSubscription(
this.telemetryWsService, entityId, attributesScope, this.zone); this.telemetryWsService, entityId, attributesScope, this.zone);
this.telemetrySubscriber.subscribe(); this.telemetrySubscriber.subscribe();
attributesObservable = this.telemetrySubscriber.attributeData$(); attributesObservable = this.telemetrySubscriber.attributeData$;
} else { } else {
attributesObservable = this.attributeService.getEntityAttributes(entityId, attributesScope as AttributeScope); attributesObservable = this.attributeService.getEntityAttributes(entityId, attributesScope as AttributeScope);
} }

12
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -47,7 +47,7 @@ import {
WidgetActionsApi, WidgetActionsApi,
WidgetSubscriptionApi WidgetSubscriptionApi
} from '@core/api/widget-api.models'; } from '@core/api/widget-api.models';
import { ChangeDetectorRef, Injector, NgZone, Type } from '@angular/core'; import { ChangeDetectorRef, InjectionToken, Injector, NgZone, TemplateRef, Type } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { RafService } from '@core/services/raf.service'; import { RafService } from '@core/services/raf.service';
import { WidgetTypeId } from '@shared/models/id/widget-type-id'; import { WidgetTypeId } from '@shared/models/id/widget-type-id';
@ -98,7 +98,9 @@ import * as RxJSOperators from 'rxjs/operators';
import { TbPopoverComponent } from '@shared/components/popover.component'; import { TbPopoverComponent } from '@shared/components/popover.component';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { AlarmQuery, AlarmSearchStatus, AlarmStatus } from '@app/shared/models/alarm.models'; import { AlarmQuery, AlarmSearchStatus, AlarmStatus } from '@app/shared/models/alarm.models';
import { ImagePipe, MillisecondsToTimeStringPipe, TelemetrySubscriber } from '@app/shared/public-api'; import { ImagePipe } from '@shared/pipe/image.pipe';
import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
import { SharedTelemetrySubscriber, TelemetrySubscriber } from '@shared/models/telemetry/telemetry.models';
import { UserId } from '@shared/models/id/user-id'; import { UserId } from '@shared/models/id/user-id';
import { UserSettingsService } from '@core/http/user-settings.service'; import { UserSettingsService } from '@core/http/user-settings.service';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
@ -204,7 +206,7 @@ export class WidgetContext {
userSettingsService: UserSettingsService; userSettingsService: UserSettingsService;
utilsService: UtilsService; utilsService: UtilsService;
telemetryWsService: TelemetryWebsocketService; telemetryWsService: TelemetryWebsocketService;
telemetrySubscribers?: TelemetrySubscriber[]; telemetrySubscribers?: Array<TelemetrySubscriber | SharedTelemetrySubscriber>;
date: DatePipe; date: DatePipe;
imagePipe: ImagePipe; imagePipe: ImagePipe;
milliSecondsToTimeString: MillisecondsToTimeStringPipe; milliSecondsToTimeString: MillisecondsToTimeStringPipe;
@ -536,6 +538,10 @@ export class LabelVariablePattern {
} }
} }
export const widgetContextToken = new InjectionToken<WidgetContext>('widgetContext');
export const widgetErrorMessagesToken = new InjectionToken<string[]>('errorMessages');
export const widgetTitlePanelToken = new InjectionToken<TemplateRef<any>>('widgetTitlePanel');
export interface IDynamicWidgetComponent { export interface IDynamicWidgetComponent {
readonly ctx: WidgetContext; readonly ctx: WidgetContext;
readonly errorMessages: string[]; readonly errorMessages: string[];

3
ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html

@ -230,9 +230,10 @@
<tb-icon class="tb-mat-18">mdi:microsoft-teams</tb-icon> <tb-icon class="tb-mat-18">mdi:microsoft-teams</tb-icon>
<div class="tb-form-panel-title" translate>notification.delivery-method.microsoft-teams-preview</div> <div class="tb-form-panel-title" translate>notification.delivery-method.microsoft-teams-preview</div>
</div> </div>
<div class="notification-content mini"> <div class="notification-content mini" [style.background-color]="preview.processedTemplates.MICROSOFT_TEAMS?.themeColor">
<div class="subject">{{ preview.processedTemplates.MICROSOFT_TEAMS.subject }}</div> <div class="subject">{{ preview.processedTemplates.MICROSOFT_TEAMS.subject }}</div>
{{ preview.processedTemplates.MICROSOFT_TEAMS.body }} {{ preview.processedTemplates.MICROSOFT_TEAMS.body }}
<button mat-raised-button>{{ preview.processedTemplates.MICROSOFT_TEAMS.button.text }}</button>
</div> </div>
</section> </section>
</div> </div>

2
ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss

@ -190,6 +190,8 @@
&.mini { &.mini {
font-size: 12px; font-size: 12px;
line-height: 1.25; line-height: 1.25;
justify-content: start;
gap: 5px;
.subject { .subject {
font-size: 14px; font-size: 14px;

3
ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts

@ -127,7 +127,8 @@ export class RuleChainsTableConfigResolver {
const columns: Array<EntityColumn<RuleChain>> = []; const columns: Array<EntityColumn<RuleChain>> = [];
columns.push( columns.push(
new DateEntityTableColumn<RuleChain>('createdTime', 'common.created-time', this.datePipe, '150px'), new DateEntityTableColumn<RuleChain>('createdTime', 'common.created-time', this.datePipe, '150px'),
new EntityTableColumn<RuleChain>('name', 'rulechain.name', '100%') new EntityTableColumn<RuleChain>('name', 'rulechain.name', '50%'),
new EntityTableColumn<RuleChain>('description', 'rulechain.description', '50%', entity => entity.additionalInfo?.description ?? '')
); );
if (ruleChainScope === 'tenant' || ruleChainScope === 'edge') { if (ruleChainScope === 'tenant' || ruleChainScope === 'edge') {
columns.push( columns.push(

2
ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.ts

@ -31,7 +31,7 @@ import { mergeMap, tap } from 'rxjs/operators';
import { ImportExportService } from '@shared/import-export/import-export.service'; import { ImportExportService } from '@shared/import-export/import-export.service';
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
import printTemplate from '!raw-loader!./backup-code-print-template.raw'; import printTemplate from './backup-code-print-template.raw';
@Component({ @Component({
selector: 'tb-backup-code-auth-dialog', selector: 'tb-backup-code-auth-dialog',

3
ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.ts

@ -28,6 +28,7 @@ import {
TwoFactorAuthProviderType TwoFactorAuthProviderType
} from '@shared/models/two-factor-auth.models'; } from '@shared/models/two-factor-auth.models';
import { MatStepper } from '@angular/material/stepper'; import { MatStepper } from '@angular/material/stepper';
import { unwrapModule } from '@core/utils';
@Component({ @Component({
selector: 'tb-totp-auth-dialog', selector: 'tb-totp-auth-dialog',
@ -56,7 +57,7 @@ export class TotpAuthDialogComponent extends DialogComponent<TotpAuthDialogCompo
this.totpAuthURL = this.authAccountConfig.authUrl; this.totpAuthURL = this.authAccountConfig.authUrl;
this.authAccountConfig.useByDefault = true; this.authAccountConfig.useByDefault = true;
import('qrcode').then((QRCode) => { import('qrcode').then((QRCode) => {
QRCode.toCanvas(this.canvasRef.nativeElement, this.totpAuthURL); unwrapModule(QRCode).toCanvas(this.canvasRef.nativeElement, this.totpAuthURL);
this.canvasRef.nativeElement.style.width = 'auto'; this.canvasRef.nativeElement.style.width = 'auto';
this.canvasRef.nativeElement.style.height = 'auto'; this.canvasRef.nativeElement.style.height = 'auto';
}); });

1
ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.html

@ -19,6 +19,7 @@
<mat-toolbar color="primary"> <mat-toolbar color="primary">
<h2 translate>widget.select-widget-type</h2> <h2 translate>widget.select-widget-type</h2>
<span class="flex-1"></span> <span class="flex-1"></span>
<div tb-help="widgetTypes"></div>
<button mat-icon-button <button mat-icon-button
(click)="cancel()" (click)="cancel()"
type="button"> type="button">

2
ui-ngx/src/app/shared/components/dialog/confirm-dialog.component.html

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<h2 mat-dialog-title>{{data.title}}</h2> <h2 mat-dialog-title tbTruncateWithTooltip>{{data.title}}</h2>
<div mat-dialog-content [innerHTML]="data.message | safe: 'html'"></div> <div mat-dialog-content [innerHTML]="data.message | safe: 'html'"></div>
<div mat-dialog-actions class="flex items-center justify-end"> <div mat-dialog-actions class="flex items-center justify-end">
<button mat-button color="primary" [mat-dialog-close]="false">{{data.cancel}}</button> <button mat-button color="primary" [mat-dialog-close]="false">{{data.cancel}}</button>

4
ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts

@ -20,13 +20,13 @@ import { SharedModule } from '@shared/shared.module';
import { ImportExportService } from '@shared/import-export/import-export.service'; import { ImportExportService } from '@shared/import-export/import-export.service';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { entityTypeTranslations } from '@shared/models/entity-type.models'; import { entityTypeTranslations } from '@shared/models/entity-type.models';
import { EntityInfoData } from '@shared/models/entity.models'; import { EntityInfoData, VersionedEntity } from '@shared/models/entity.models';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { RuleChainMetaData } from '@shared/models/rule-chain.models'; import { RuleChainMetaData } from '@shared/models/rule-chain.models';
interface EntityConflictDialogData { interface EntityConflictDialogData {
message: string; message: string;
entity: EntityInfoData | RuleChainMetaData; entity: VersionedEntity;
} }
@Component({ @Component({

2
ui-ngx/src/app/shared/components/entity/entity-select.component.ts

@ -139,7 +139,7 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte
}; };
} }
this.entitySelectFormGroup.get('entityType').patchValue(this.modelValue.entityType, {emitEvent: false}); this.entitySelectFormGroup.get('entityType').patchValue(this.modelValue.entityType, {emitEvent: false});
this.entitySelectFormGroup.get('entityId').patchValue(this.modelValue.id, {emitEvent: false}); this.entitySelectFormGroup.get('entityId').patchValue(this.modelValue, {emitEvent: false});
} }
updateView(entityType: EntityType | AliasEntityType | null, entityId: string | null) { updateView(entityType: EntityType | AliasEntityType | null, entityId: string | null) {

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save