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
[![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 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}",
"actions": null
},
{
"tag": "scale-background",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n}",
"actions": null
},
{
"tag": "top-layer",
"stateRenderFunction": "if (ctx.properties.transparent || !ctx.properties.scale) {\n element.hide();\n}",
@ -555,7 +560,7 @@
}
]
}]]></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"/>
</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"/>

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}",
"actions": null
},
{
"tag": "scale-background",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n}",
"actions": null
},
{
"tag": "top-layer",
"stateRenderFunction": "if (ctx.properties.transparent || !ctx.properties.scale) {\n element.hide();\n}",
@ -556,7 +561,7 @@
}
]
}]]></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"/>
</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"/>

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}",
"actions": null
},
{
"tag": "scale-background",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n}",
"actions": null
},
{
"tag": "top-layer",
"stateRenderFunction": "if (ctx.properties.transparent || !ctx.properties.scale) {\n element.hide();\n}",
@ -556,7 +561,7 @@
}
]
}]]></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"/>
</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"/>

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}",
"actions": null
},
{
"tag": "scale-background",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n}",
"actions": null
},
{
"tag": "top-layer",
"stateRenderFunction": "if (ctx.properties.transparent || !ctx.properties.scale) {\n element.hide();\n}",
@ -556,7 +561,7 @@
}
]
}]]></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"/>
</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"/>

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": [
"horizontal_pipe",
"long_horizontal_pipe",
"extra_long_horizontal_pipe",
"vertical_pipe",
"long_vertical_pipe",
"extra_long_vertical_pipe",
"left_bottom_elbow_pipe",
"bottom_right_elbow_pipe",
"top_right_elbow_pipe",
@ -28,6 +30,10 @@
"right_drain_pipe",
"short_left_drain_pipe",
"short_right_drain_pipe",
"horizontal_broken_pipe",
"vertical_broken_pipe",
"long_horizontal_broken_pipe",
"long_vertical_broken_pipe",
"top_flow_meter",
"right_flow_meter",
"bottom_flow_meter",
@ -58,6 +64,7 @@
"stand_vertical_tank",
"cylindrical_tank",
"stand_cylindrical_tank",
"small_cylindrical_tank",
"vertical_short_tank",
"stand_vertical_short_tank",
"large_cylindrical_tank",
@ -68,6 +75,8 @@
"stand_horizontal_tank",
"spherical_tank",
"small_spherical_tank",
"conical_tank",
"large_conical_tank",
"elevated_tank",
"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.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.id.TenantId;
@ -39,6 +40,7 @@ import java.util.stream.Stream;
@TbCoreComponent
@RequiredArgsConstructor
@Slf4j
@ConditionalOnProperty(value = "transport.gateway.dashboard.sync.enabled", havingValue = "true")
public class DashboardSyncService {
private final GitSyncService gitSyncService;
@ -46,8 +48,6 @@ public class DashboardSyncService {
private final WidgetsBundleService widgetsBundleService;
private final PartitionService partitionService;
@Value("${transport.gateway.dashboard.sync.enabled:true}")
private boolean enabled;
@Value("${transport.gateway.dashboard.sync.repository_url:}")
private String repoUrl;
@Value("${transport.gateway.dashboard.sync.branch:main}")
@ -60,9 +60,6 @@ public class DashboardSyncService {
@AfterStartUp(order = AfterStartUp.REGULAR_SERVICE)
public void init() throws Exception {
if (!enabled) {
return;
}
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 lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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.UncheckedIOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
@ -170,7 +169,6 @@ public class InstallScripts {
loadRuleChainsFromPath(tenantId, edgeChainsDir);
}
@SneakyThrows
private void loadRuleChainsFromPath(TenantId tenantId, Path ruleChainsPath) {
findRuleChainsFromPath(ruleChainsPath).forEach(path -> {
try {
@ -182,12 +180,10 @@ public class InstallScripts {
});
}
List<Path> findRuleChainsFromPath(Path ruleChainsPath) throws IOException {
List<Path> paths = new ArrayList<>();
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(ruleChainsPath, path -> path.toString().endsWith(InstallScripts.JSON_EXT))) {
dirStream.forEach(paths::add);
List<Path> findRuleChainsFromPath(Path ruleChainsPath) {
try (Stream<Path> files = listDir(ruleChainsPath).filter(path -> path.toString().endsWith(InstallScripts.JSON_EXT))) {
return files.toList();
}
return paths;
}
public RuleChain createDefaultRuleChain(TenantId tenantId, String ruleChainName) {
@ -211,11 +207,11 @@ public class InstallScripts {
return ruleChain;
}
public void loadSystemWidgets() throws Exception {
public void loadSystemWidgets() {
log.info("Loading system widgets");
Map<Path, JsonNode> widgetsBundlesMap = new HashMap<>();
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(
path -> {
JsonNode widgetsBundleDescriptorJson;
@ -247,12 +243,14 @@ public class InstallScripts {
}
Path widgetTypesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_TYPES_DIR);
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(
path -> {
try {
JsonNode widgetTypeJson = JacksonUtil.toJsonNode(path.toFile());
WidgetTypeDetails widgetTypeDetails = JacksonUtil.treeToValue(widgetTypeJson, WidgetTypeDetails.class);
String widgetTypeJson = Files.readString(path);
widgetTypeJson = resourceService.checkSystemResourcesUsage(widgetTypeJson, ResourceType.JS_MODULE);
WidgetTypeDetails widgetTypeDetails = JacksonUtil.fromString(widgetTypeJson, WidgetTypeDetails.class);
widgetTypeService.saveWidgetType(widgetTypeDetails);
} catch (Exception e) {
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");
Path scadaSymbolsDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, SCADA_SYMBOLS_DIR);
if (Files.exists(scadaSymbolsDir)) {
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(
path -> {
try {
@ -404,11 +402,10 @@ public class InstallScripts {
imagesUpdater.updateAssetProfilesImages();
}
@SneakyThrows
public void loadSystemImages() {
log.info("Loading system images...");
Stream<Path> dashboardsFiles = Stream.concat(Files.list(Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, DASHBOARDS_DIR)),
Files.list(Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, DASHBOARDS_DIR)));
Stream<Path> dashboardsFiles = Stream.concat(listDir(Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, DASHBOARDS_DIR)),
listDir(Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, DASHBOARDS_DIR)));
try (dashboardsFiles) {
dashboardsFiles.forEach(file -> {
try {
@ -431,25 +428,22 @@ public class InstallScripts {
loadDashboardsFromDir(tenantId, customerId, dashboardsDir);
}
@SneakyThrows
private void loadDashboardsFromDir(TenantId tenantId, CustomerId customerId, Path dashboardsDir) {
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(dashboardsDir, path -> path.toString().endsWith(JSON_EXT))) {
dirStream.forEach(
path -> {
try {
JsonNode dashboardJson = JacksonUtil.toJsonNode(path.toFile());
Dashboard dashboard = JacksonUtil.treeToValue(dashboardJson, Dashboard.class);
dashboard.setTenantId(tenantId);
Dashboard savedDashboard = dashboardService.saveDashboard(dashboard);
if (customerId != null && !customerId.isNullUid()) {
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);
}
try (Stream<Path> dashboards = listDir(dashboardsDir).filter(path -> path.toString().endsWith(JSON_EXT))) {
dashboards.forEach(path -> {
try {
JsonNode dashboardJson = JacksonUtil.toJsonNode(path.toFile());
Dashboard dashboard = JacksonUtil.treeToValue(dashboardJson, Dashboard.class);
dashboard.setTenantId(tenantId);
Dashboard savedDashboard = dashboardService.saveDashboard(dashboard);
if (customerId != null && !customerId.isNullUid()) {
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);
}
});
}
}
@ -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);
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(
path -> {
try {
@ -489,7 +483,7 @@ public class InstallScripts {
public void loadSystemLwm2mResources() {
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(
path -> {
try {
@ -539,9 +533,11 @@ public class InstallScripts {
}
}
private Stream<Path> listDir(Path resourcesDir) {
private Stream<Path> listDir(Path dir) {
try {
return Files.list(resourcesDir);
return Files.list(dir);
} catch (NoSuchFileException e) {
return Stream.empty();
} catch (IOException 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.NotificationRequestStatus;
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.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings;
@ -187,7 +188,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
}
@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();
target.setTenantId(tenantId);
PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig();
@ -198,6 +199,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
.tenantId(tenantId)
.template(template)
.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)
.build();
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:
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).
in_memory:
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;
import org.junit.After;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.controller.AbstractControllerTest;
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;
@ -35,6 +40,20 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
})
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
public void testGatewaysDashboardSync() throws Exception {
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.service.install.update.ImagesUpdater;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
@ -87,14 +86,14 @@ class InstallScriptsTest {
}
@Test
void testDefaultRuleChainsTemplates() throws IOException {
void testDefaultRuleChainsTemplates() {
Path dir = installScripts.getTenantRuleChainsDir();
installScripts.findRuleChainsFromPath(dir)
.forEach(this::validateRuleChainTemplate);
}
@Test
void testDefaultEdgeRuleChainsTemplates() throws IOException {
void testDefaultEdgeRuleChainsTemplates() {
Path dir = installScripts.getEdgeRuleChainsDir();
installScripts.findRuleChainsFromPath(dir)
.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.info.AlarmCommentNotificationInfo;
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.settings.MobileAppNotificationDeliveryMethodConfig;
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.security.Authority;
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.service.notification.channels.MicrosoftTeamsNotificationChannel;
import org.thingsboard.server.service.notification.channels.TeamsAdaptiveCard;
@ -746,14 +748,21 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
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(),
expectedNotification.toTemplate());
template, new GeneralNotificationInfo(Map.of(
"subjectVariable", "subject",
"bodyVariable", "body"
)));
getAnotherWsClient().waitForUpdate(true);
Notification notification = getAnotherWsClient().getLastDataUpdate().getUpdate();
assertThat(notification.getSubject()).isEqualTo(expectedNotification.getSubject());
assertThat(notification.getText()).isEqualTo(expectedNotification.getText());
assertThat(notification.getSubject()).isEqualTo("Testing subject");
assertThat(notification.getText()).isEqualTo("Testing body");
}
@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.Ignore;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.test.context.TestPropertySource;
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.server.common.data.id.TenantId;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
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.assertThatThrownBy;
import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST;
@DaoSqlTest
@TestPropertySource(properties = {
@ -52,10 +46,7 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_RE
"tbel.max_errors=2",
"tbel.compiled_scripts_cache_size=100"
})
class TbelInvokeServiceTest extends AbstractControllerTest {
@Autowired
private TbelInvokeService invokeService;
class TbelInvokeServiceTest extends AbstractTbelInvokeTest {
@Value("${tbel.max_errors}")
private int maxJsErrors;
@ -69,14 +60,14 @@ class TbelInvokeServiceTest extends AbstractControllerTest {
for (int i = 0; i < 100; i++) {
msg.put("temperature", i);
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);
}
long startTs = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
msg.put("temperature", i);
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);
}
long duration = System.currentTimeMillis() - startTs;
@ -101,7 +92,7 @@ class TbelInvokeServiceTest extends AbstractControllerTest {
for (int i = 0; i < maxJsErrors; i++) {
assertThatThrownBy(() -> {
invokeScript(scriptId, hugeMsg);
invokeScriptResultString(scriptId, hugeMsg);
}).hasMessageContaining("input arguments exceed maximum");
}
assertThatScriptIsBlocked(scriptId);
@ -114,7 +105,7 @@ class TbelInvokeServiceTest extends AbstractControllerTest {
for (int i = 0; i < maxJsErrors; i++) {
assertThatThrownBy(() -> {
invokeScript(scriptId, "{}");
invokeScriptResultString(scriptId, "{}");
}).hasMessageContaining("result exceeds maximum allowed size");
}
assertThatScriptIsBlocked(scriptId);
@ -182,7 +173,7 @@ class TbelInvokeServiceTest extends AbstractControllerTest {
scriptsIds.add(scriptId);
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);
assertThat(compiledScriptsCache.getIfPresent(scriptIdToHash.get(scriptRemovedFromCache))).isNull();
invokeScript(scriptRemovedFromCache, "{ \"temperature\": 12 }");
invokeScriptResultString(scriptRemovedFromCache, "{ \"temperature\": 12 }");
assertThat(compiledScriptsCache.getIfPresent(scriptIdToHash.get(scriptRemovedFromCache))).isNotNull();
}
private void assertThatScriptIsBlocked(UUID scriptId) {
assertThatThrownBy(() -> {
invokeScript(scriptId, "{}");
invokeScriptResultString(scriptId, "{}");
}).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.thingsboard.server.controller.AbstractWebTest" 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 -->
<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;
@Slf4j
@Deprecated(forRemoval = true, since = "3.9") // for removal in 4.0
public class TbServiceBusAdmin implements TbQueueAdmin {
private final String MAX_SIZE = "maxSizeInMb";
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;
@Slf4j
@Deprecated(forRemoval = true, since = "3.9") // for removal in 4.0
public class TbPubSubAdmin implements TbQueueAdmin {
private static final String ACK_DEADLINE = "ackDeadlineInSec";
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;
@Slf4j
@Deprecated(forRemoval = true, since = "3.9") // for removal in 4.0
public class TbRabbitMqAdmin implements TbQueueAdmin {
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;
@Slf4j
@Deprecated(forRemoval = true, since = "3.9") // for removal in 4.0
public class TbAwsSqsAdmin implements TbQueueAdmin {
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()) {
return -1;
}
return str.matches("-?\\d+(\\.\\d+)?") ? DEC_RADIX : -1;
return str.matches("[+-]?\\d+(\\.\\d+)?") ? DEC_RADIX : -1;
}
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(java.util.Optional.of(473).get(), TbUtils.parseInt("473"));
Assertions.assertEquals(java.util.Optional.of(-255).get(), TbUtils.parseInt("-0xFF"));
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) {
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()) {
log.trace("[{}][{}] Pushing to topic {} message {}", tenantId, entityId, tpi.getFullTopicName(), msg);
}
TransportTbQueueCallback transportTbQueueCallback = callback != null ?
new TransportTbQueueCallback(callback) : null;
tbCoreProducerStats.incrementTotal();

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

@ -372,6 +372,15 @@ public class DefaultNotifications {
.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 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.util.concurrent.ListenableFuture;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.stereotype.Service;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.RegexUtils;
import org.thingsboard.server.cache.resourceInfo.ResourceInfoCacheKey;
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.ResourceType;
import org.thingsboard.server.common.data.TbResource;
@ -54,7 +58,7 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
@Service("TbResourceDaoService")
@Slf4j
@AllArgsConstructor
@RequiredArgsConstructor
@Primary
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 TbResourceInfoDao resourceInfoDao;
protected final ResourceDataValidator resourceValidator;
@Autowired @Lazy
private ImageService imageService;
@Override
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) {
if (resourceType == ResourceType.DASHBOARD) {
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);

2
msa/js-executor/docker/Dockerfile

@ -14,7 +14,7 @@
# 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 DOCKER_MODE true

48
msa/js-executor/package.json

@ -6,26 +6,26 @@
"main": "server.ts",
"bin": "server.js",
"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",
"start": "nodemon --watch '.' --ext 'ts' --exec 'ts-node server.ts'",
"start-prod": "nodemon --watch '.' --ext 'ts' --exec 'NODE_ENV=production ts-node server.ts'",
"build": "tsc"
},
"dependencies": {
"@aws-sdk/client-sqs": "^3.121.0",
"@azure/service-bus": "^7.5.1",
"@google-cloud/pubsub": "^4.3.3",
"amqplib": "^0.10.0",
"config": "^3.3.7",
"express": "^4.18.1",
"@aws-sdk/client-sqs": "^3.682.0",
"@azure/service-bus": "^7.9.5",
"@google-cloud/pubsub": "^4.8.0",
"amqplib": "^0.10.4",
"config": "^3.3.12",
"express": "^4.21.1",
"js-yaml": "^4.1.0",
"kafkajs": "^2.1.0",
"long": "^5.2.0",
"kafkajs": "^2.2.4",
"long": "^5.2.3",
"uuid-parse": "^1.1.0",
"uuid-random": "^1.3.2",
"winston": "^3.7.2",
"winston-daily-rotate-file": "^4.7.1"
"winston": "^3.16.0",
"winston-daily-rotate-file": "^5.0.0"
},
"nyc": {
"exclude": [
@ -36,26 +36,20 @@
]
},
"devDependencies": {
"@types/amqplib": "^0.8.2",
"@types/config": "^0.0.41",
"@types/express": "^4.17.13",
"@types/node": "^18.0.1",
"@types/uuid-parse": "^1.0.0",
"fs-extra": "^10.1.0",
"nodemon": "^2.0.16",
"pkg": "^5.7.0",
"ts-node": "^10.8.1",
"typescript": "^4.7.4"
"@types/amqplib": "^0.10.5",
"@types/config": "^3.3.5",
"@types/express": "~4.17.21",
"@types/node": "~20.17.6",
"@types/uuid-parse": "^1.0.2",
"fs-extra": "^11.2.0",
"nodemon": "^3.1.7",
"pkg": "^5.8.1",
"ts-node": "^10.9.2",
"typescript": "5.5.4"
},
"pkg": {
"assets": [
"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>
</goals>
<configuration>
<nodeVersion>v16.20.2</nodeVersion>
<yarnVersion>v1.22.17</yarnVersion>
<nodeVersion>v20.18.0</nodeVersion>
<yarnVersion>v1.22.22</yarnVersion>
</configuration>
</execution>
<execution>

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

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

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

@ -57,7 +57,7 @@ export class ServiceBusTemplate implements IQueue {
this.parseQueueProperties();
const listQueues = await this.serviceBusService.listQueues();
const listQueues = this.serviceBusService.listQueues();
for await (const queue of listQueues) {
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:
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).
in_memory:
stats:

2
msa/web-ui/docker/Dockerfile

@ -14,7 +14,7 @@
# 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 DOCKER_MODE true

39
msa/web-ui/package.json

@ -6,22 +6,22 @@
"main": "server.ts",
"bin": "server.js",
"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",
"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'",
"build": "tsc"
},
"dependencies": {
"compression": "^1.7.4",
"config": "^3.3.7",
"compression": "^1.7.5",
"config": "^3.3.12",
"connect-history-api-fallback": "^1.6.0",
"express": "^4.18.1",
"express": "^4.21.1",
"http": "0.0.0",
"http-proxy": "^1.18.1",
"js-yaml": "^4.1.0",
"winston": "^3.7.2",
"winston-daily-rotate-file": "^4.7.1"
"winston": "^3.16.0",
"winston-daily-rotate-file": "^5.0.0"
},
"nyc": {
"exclude": [
@ -32,26 +32,21 @@
]
},
"devDependencies": {
"@types/compression": "^1.7.2",
"@types/config": "^0.0.41",
"@types/connect-history-api-fallback": "^1.3.5",
"@types/express": "^4.17.13",
"@types/http-proxy": "^1.17.9",
"@types/node": "^18.0.0",
"fs-extra": "^10.1.0",
"nodemon": "^2.0.16",
"pkg": "^5.7.0",
"ts-node": "^10.8.1",
"typescript": "^4.7.4"
"@types/compression": "^1.7.5",
"@types/config": "^3.3.5",
"@types/connect-history-api-fallback": "^1.5.4",
"@types/express": "~4.17.21",
"@types/http-proxy": "^1.17.15",
"@types/node": "~20.17.6",
"fs-extra": "^11.2.0",
"nodemon": "^3.1.7",
"pkg": "^5.8.1",
"ts-node": "^10.9.2",
"typescript": "5.5.4"
},
"pkg": {
"assets": [
"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>
</goals>
<configuration>
<nodeVersion>v16.20.2</nodeVersion>
<yarnVersion>v1.22.17</yarnVersion>
<nodeVersion>v20.18.0</nodeVersion>
<yarnVersion>v1.22.22</yarnVersion>
</configuration>
</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.NotificationRequest;
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.template.NotificationTemplate;
@ -32,7 +33,7 @@ public interface NotificationCenter {
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);

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,
name = "originator telemetry",
configClazz = TbGetTelemetryNodeConfiguration.class,
version = 1,
version = 2,
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 " +
"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 {
boolean hasChanges = false;
switch (fromVersion) {
case 0 -> {
case 0: {
if (oldConfiguration.hasNonNull("fetchMode")) {
String fetchMode = oldConfiguration.get("fetchMode").asText();
switch (fetchMode) {
case "FIRST":
case "FIRST" -> {
((ObjectNode) oldConfiguration).put("orderBy", Direction.ASC.name());
((ObjectNode) oldConfiguration).put("aggregation", Aggregation.NONE.name());
hasChanges = true;
break;
case "LAST":
}
case "LAST" -> {
((ObjectNode) oldConfiguration).put("orderBy", Direction.DESC.name());
((ObjectNode) oldConfiguration).put("aggregation", Aggregation.NONE.name());
hasChanges = true;
break;
case "ALL":
}
case "ALL" -> {
if (oldConfiguration.has("orderBy") &&
(oldConfiguration.get("orderBy").isNull() || oldConfiguration.get("orderBy").asText().isEmpty())) {
((ObjectNode) oldConfiguration).put("orderBy", Direction.ASC.name());
@ -257,16 +257,33 @@ public class TbGetTelemetryNode implements TbNode {
((ObjectNode) oldConfiguration).put("aggregation", Aggregation.NONE.name());
hasChanges = true;
}
break;
default:
}
default -> {
((ObjectNode) oldConfiguration).put("fetchMode", FetchMode.LAST.name());
((ObjectNode) oldConfiguration).put("orderBy", Direction.DESC.name());
((ObjectNode) oldConfiguration).put("aggregation", Aggregation.NONE.name());
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);
}

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,
"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:
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).
kafka:
# Kafka Bootstrap Servers

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

@ -202,7 +202,9 @@ transport:
# Queue configuration parameters
queue:
type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ)
# 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) .
kafka:
# Kafka Bootstrap Servers

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

@ -302,7 +302,9 @@ transport:
# Queue configuration properties
queue:
type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ)
# 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).
kafka:
# Kafka Bootstrap Servers

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

@ -235,7 +235,9 @@ transport:
# Queue configuration parameters
queue:
type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ)
# 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).
kafka:
# Kafka Bootstrap Servers

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

@ -181,7 +181,9 @@ transport:
# Queue configuration parameters
queue:
type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ)
# 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).
kafka:
# Kafka Bootstrap Servers

67
ui-ngx/angular.json

@ -18,12 +18,20 @@
},
"architect": {
"build": {
"builder": "@angular-builders/custom-webpack:browser",
"builder": "@angular-builders/custom-esbuild:application",
"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",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"browser": "src/main.ts",
"polyfills": ["src/polyfills.ts"],
"tsConfig": "src/tsconfig.app.json",
"aot": true,
"assets": [
@ -122,49 +130,8 @@
"node_modules/prismjs/components/prism-typescript.min.js",
"node_modules/prismjs/plugins/line-numbers/prism-line-numbers.min.js"
],
"customWebpackConfig": {
"path": "./extra-webpack.config.js"
},
"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": {
@ -187,8 +154,6 @@
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": false,
"budgets": [
{
"type": "initial",
@ -198,7 +163,6 @@
]
},
"development": {
"buildOptimizer": false,
"optimization": {
"scripts": false,
"styles": {
@ -207,7 +171,6 @@
},
"fonts": false
},
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
@ -215,7 +178,7 @@
}
},
"serve": {
"builder": "@angular-builders/custom-webpack:dev-server",
"builder": "@angular-builders/custom-esbuild:dev-server",
"options": {
"buildTarget": "thingsboard:build",
"proxyConfig": "proxy.conf.js"
@ -231,7 +194,7 @@
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"builder": "@angular/build:extract-i18n",
"options": {
"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 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 ngcPath = path.join('.', 'node_modules', '.bin', 'ngc');
const tailwindcss = path.join('.', 'node_modules', '.bin', 'tailwindcss');
const tsconfigPath = path.join('src', 'tsconfig.app.json');
console.log(`Remove directory: ${typeDir}`);
@ -31,12 +33,28 @@ try {
}
const cliCommand = `${ngcPath} --p ${tsconfigPath} --declaration --outDir ${srcDir}`;
console.log(cliCommand);
try {
child_process.execSync(cliCommand);
} catch (err) {
console.error("Build types", err);
process.exit(1);
executeCliCommand(cliCommand, 'Build types');
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}`);
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) {
@ -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",
"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: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:icon-metadata": "node generate-icon-metadata.js",
"lint": "ng lint",
@ -13,22 +13,22 @@
},
"private": true,
"dependencies": {
"@angular/animations": "18.2.6",
"@angular/cdk": "18.2.6",
"@angular/common": "18.2.6",
"@angular/compiler": "18.2.6",
"@angular/core": "18.2.6",
"@angular/animations": "18.2.10",
"@angular/cdk": "18.2.11",
"@angular/common": "18.2.10",
"@angular/compiler": "18.2.10",
"@angular/core": "18.2.10",
"@angular/flex-layout": "^15.0.0-beta.42",
"@angular/forms": "18.2.6",
"@angular/material": "18.2.6",
"@angular/platform-browser": "18.2.6",
"@angular/platform-browser-dynamic": "18.2.6",
"@angular/router": "18.2.6",
"@angular/forms": "18.2.10",
"@angular/material": "18.2.11",
"@angular/platform-browser": "18.2.10",
"@angular/platform-browser-dynamic": "18.2.10",
"@angular/router": "18.2.10",
"@auth0/angular-jwt": "^5.2.0",
"@emotion/react": "11.13.3",
"@emotion/styled": "11.13.0",
"@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",
"@geoman-io/leaflet-geoman-free": "2.17.0",
"@iplab/ngx-color-picker": "^18.0.1",
@ -41,9 +41,9 @@
"@mui/styles": "6.1.2",
"@mui/system": "6.1.2",
"@mui/x-date-pickers": "7.18.0",
"@ngrx/effects": "^18.0.2",
"@ngrx/store": "^18.0.2",
"@ngrx/store-devtools": "^18.0.2",
"@ngrx/effects": "^18.1.1",
"@ngrx/store": "^18.1.1",
"@ngrx/store-devtools": "^18.1.1",
"@ngx-translate/core": "^15.0.0",
"@svgdotjs/svg.filter.js": "^3.0.8",
"@svgdotjs/svg.js": "^3.2.4",
@ -105,26 +105,25 @@
"tinycolor2": "^1.6.0",
"tinymce": "~6.8.5",
"tooltipster": "^4.2.8",
"ts-transformer-keys": "^0.4.4",
"tslib": "^2.7.0",
"tv4": "^1.3.0",
"typeface-roboto": "^1.1.13",
"zone.js": "~0.14.10"
},
"devDependencies": {
"@angular-builders/custom-webpack": "~18.0.0",
"@angular-devkit/build-angular": "18.2.7",
"@angular-devkit/core": "18.2.7",
"@angular-devkit/schematics": "18.2.7",
"@angular-eslint/builder": "18.3.1",
"@angular-eslint/eslint-plugin": "18.3.1",
"@angular-eslint/eslint-plugin-template": "18.3.1",
"@angular-eslint/schematics": "18.3.1",
"@angular-eslint/template-parser": "18.3.1",
"@angular/cli": "18.2.7",
"@angular/compiler-cli": "18.2.6",
"@angular/language-service": "18.2.6",
"@ngtools/webpack": "18.2.7",
"@angular-builders/custom-esbuild": "18.0.0",
"@angular-devkit/build-angular": "18.2.11",
"@angular-devkit/core": "18.2.11",
"@angular-devkit/schematics": "18.2.11",
"@angular-eslint/builder": "18.4.0",
"@angular-eslint/eslint-plugin": "18.4.0",
"@angular-eslint/eslint-plugin-template": "18.4.0",
"@angular-eslint/schematics": "18.4.0",
"@angular-eslint/template-parser": "18.4.0",
"@angular/build": "18.2.11",
"@angular/cli": "18.2.11",
"@angular/compiler-cli": "18.2.10",
"@angular/language-service": "18.2.10",
"@types/ace-diff": "^2.1.4",
"@types/canvas-gauges": "^2.1.8",
"@types/flot": "^0.0.36",
@ -144,31 +143,31 @@
"@types/systemjs": "6.15.1",
"@types/tinycolor2": "^1.4.6",
"@types/tooltipster": "^0.0.35",
"@typescript-eslint/eslint-plugin": "^8.11.0",
"@typescript-eslint/parser": "^8.11.0",
"@typescript-eslint/utils": "^8.11.0",
"@typescript-eslint/eslint-plugin": "^8.12.2",
"@typescript-eslint/parser": "^8.12.2",
"@typescript-eslint/types": "^8.12.2",
"@typescript-eslint/utils": "^8.12.2",
"autoprefixer": "^10.4.20",
"compression-webpack-plugin": "^11.1.0",
"directory-tree": "^3.5.2",
"eslint": "~9.13.0",
"eslint-plugin-import": "latest",
"eslint-plugin-jsdoc": "latest",
"eslint-plugin-jsdoc": "^50.4.3",
"eslint-plugin-prefer-arrow": "latest",
"eslint-plugin-tailwindcss": "^3.17.5",
"ngrx-store-freeze": "^0.2.4",
"patch-package": "^8.0.0",
"postcss": "^8.4.47",
"postinstall-prepare": "^2.0.0",
"raw-loader": "^4.0.2",
"tailwindcss": "^3.4.13",
"tailwindcss": "^3.4.14",
"ts-node": "^10.9.2",
"typescript": "~5.5.4",
"webpack": "5.95.0"
"typescript": "~5.5.4"
},
"resolutions": {
"@types/react": "18.3.10",
"rc-virtual-list": "3.5.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
index 0fa881f..b844dfa 100755
index 6fe7ad6..291e4ed 100755
--- a/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)) {
matches || (matches = []);
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>
</goals>
<configuration>
<nodeVersion>v20.11.1</nodeVersion>
<nodeVersion>v20.18.0</nodeVersion>
<yarnVersion>v1.22.22</yarnVersion>
</configuration>
</execution>

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

@ -61,6 +61,9 @@ export class AliasController implements IAliasController {
resolvedAliases: { [aliasId: string]: AliasInfo } = {};
resolvedAliasesObservable: { [aliasId: string]: Observable<AliasInfo> } = {};
resolvedDevices: { [deviceId: string]: EntityInfo } = {};
resolvedDevicesObservable: { [deviceId: string]: Observable<EntityInfo> } = {};
resolvedAliasesToStateEntities: { [aliasId: string]: StateEntityInfo } = {};
constructor(private utils: UtilsService,
@ -261,9 +264,35 @@ export class AliasController implements IAliasController {
}
resolveSingleEntityInfoForDeviceId(deviceId: string): Observable<EntityInfo> {
const entityFilter = singleEntityFilterFromDeviceId(deviceId);
return this.entityService.findSingleEntityInfoByEntityFilter(entityFilter,
{ignoreLoading: true, ignoreErrors: true});
let entityInfo = this.resolvedDevices[deviceId];
if (entityInfo) {
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> {

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

@ -22,11 +22,11 @@ import { MatDialog } from '@angular/material/dialog';
import {
EntityConflictDialogComponent
} from '@shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component';
import { HasId } from '@shared/models/base-data';
import { HasVersion } from '@shared/models/entity.models';
import { EntityInfoData, VersionedEntity } from '@shared/models/entity.models';
import { getInterceptorConfig } from './interceptor.util';
import { isDefined } from '@core/utils';
import { InterceptorConfig } from '@core/interceptors/interceptor-config';
import { RuleChainMetaData } from '@shared/models/rule-chain.models';
@Injectable()
export class EntityConflictInterceptor implements HttpInterceptor {
@ -35,7 +35,7 @@ export class EntityConflictInterceptor implements HttpInterceptor {
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/')) {
return next.handle(request);
}
@ -52,11 +52,11 @@ export class EntityConflictInterceptor implements HttpInterceptor {
}
private handleConflictError(
request: HttpRequest<unknown & HasId & HasVersion>,
request: HttpRequest<VersionedEntity>,
next: HttpHandler,
error: HttpErrorResponse
): Observable<HttpEvent<unknown>> {
if (getInterceptorConfig(request).ignoreVersionConflict) {
if (getInterceptorConfig(request).ignoreVersionConflict || !this.isVersionedEntity(request.body)) {
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 };
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, {
disableClose: true,
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',
type: 'link',
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 { customTranslationsPrefix, i18nPrefix } from '@app/shared/models/constants';
import { DataKey, Datasource, DatasourceType, KeyInfo } from '@shared/models/widget.models';
import { DataKeyType } from '@app/shared/models/telemetry/telemetry.models';
import {
alarmFields,
alarmSeverityTranslations,
alarmStatusTranslations
} from '@shared/models/alarm.models';
import { DataKeyType, SharedTelemetrySubscriber } from '@app/shared/models/telemetry/telemetry.models';
import { alarmFields, alarmSeverityTranslations, alarmStatusTranslations } from '@shared/models/alarm.models';
import { materialColors } from '@app/shared/models/material.models';
import { WidgetInfo } from '@home/models/widget-component.models';
import jsonSchemaDefaults from 'json-schema-defaults';
import { Observable } from 'rxjs';
import { publishReplay, refCount } from 'rxjs/operators';
import { WidgetContext } from '@app/modules/home/models/widget-component.models';
import {
AttributeData,
LatestTelemetry,
TelemetrySubscriber,
TelemetryType
} from '@shared/models/telemetry/telemetry.models';
import { AttributeData, LatestTelemetry, TelemetryType } from '@shared/models/telemetry/telemetry.models';
import { EntityId } from '@shared/models/id/entity-id';
import { DatePipe, DOCUMENT } from '@angular/common';
import { entityTypeTranslations } from '@shared/models/entity-type.models';
@ -483,13 +474,13 @@ export class UtilsService {
if (!entityId && ctx.datasources.length > 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) {
ctx.telemetrySubscribers = [];
}
ctx.telemetrySubscribers.push(subscription);
subscription.subscribe();
return subscription.attributeData$().pipe(
return subscription.attributeData$.pipe(
publishReplay(1),
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> => {
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 enumerable from '@shared/decorators/enumerable';
import * as TbInject from '@shared/decorators/tb-inject';
import * as FooterComponent from '@shared/components/footer.component';
import * as LogoComponent from '@shared/components/logo.component';
@ -435,7 +434,6 @@ class ModulesMap implements IModulesMap {
'@shared/decorators/coercion': coercion,
'@shared/decorators/enumerable': enumerable,
'@shared/decorators/tb-inject': TbInject,
'@shared/import-export/import-export.service': ImportExportService,
'@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
type="button"
matSuffix
*ngIf="getAlarmCommentFormControl().value"
*ngIf="getAlarmCommentValue()"
(click)="saveComment()">
<mat-icon color="primary">
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 {
const commentInputValue: string = this.getAlarmCommentFormControl().value;
const commentInputValue: string = this.getAlarmCommentValue();
if (commentInputValue) {
const comment: AlarmComment = {
alarmId: {
@ -174,7 +174,7 @@ export class AlarmCommentComponent implements OnInit {
}
saveEditedComment(commentId: string): void {
const commentEditInputValue: string = this.getAlarmCommentEditFormControl().value;
const commentEditInputValue: string = this.getAlarmCommentEditValue();
if (commentEditInputValue) {
const editedComment: AlarmComment = this.getAlarmCommentById(commentId);
editedComment.comment.text = commentEditInputValue;
@ -277,6 +277,14 @@ export class AlarmCommentComponent implements OnInit {
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 {
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();
}
setTimeout(() => {
this.differ = new aceDiff.default(
this.differ = new aceDiff(
{
element: this.diffViewerElmRef.nativeElement,
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 { Directive, InjectionToken } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Directive, inject, InjectionToken } from '@angular/core';
import { Router } from '@angular/router';
import { PageComponent } from '@shared/components/page.component';
import { CustomDialogContainerComponent } from './custom-dialog-container.component';
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 {
controller: (instance: CustomDialogComponent) => void;
@ -37,12 +34,13 @@ export class CustomDialogComponent extends PageComponent {
[key: string]: any;
constructor(@TbInject(Store) protected store: Store<AppState>,
@TbInject(Router) protected router: Router,
@TbInject(MatDialogRef) public dialogRef: MatDialogRef<CustomDialogContainerComponent>,
@TbInject(UntypedFormBuilder) public fb: UntypedFormBuilder,
@TbInject(CUSTOM_DIALOG_DATA) public data: CustomDialogData) {
super(store);
protected router = inject(Router);
public dialogRef = inject(MatDialogRef<CustomDialogContainerComponent>);
public data = inject(CUSTOM_DIALOG_DATA);
public fb = inject(UntypedFormBuilder);
constructor() {
super();
// @ts-ignore
this.validators = Validators;
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 { Directive, Injector, OnDestroy, OnInit, TemplateRef } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { IDynamicWidgetComponent, WidgetContext } from '@home/models/widget-component.models';
import { Directive, inject, Injector, OnDestroy, OnInit } from '@angular/core';
import {
IDynamicWidgetComponent,
widgetContextToken,
widgetErrorMessagesToken,
widgetTitlePanelToken
} from '@home/models/widget-component.models';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { RafService } from '@core/services/raf.service';
import {
@ -45,7 +48,6 @@ import { DatePipe } from '@angular/common';
import { TranslateService } from '@ngx-translate/core';
import { DomSanitizer } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { TbInject } from '@shared/decorators/tb-inject';
import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
import { UserSettingsService } from '@core/http/user-settings.service';
import { ImagePipe } from '@shared/pipe/image.pipe';
@ -64,38 +66,39 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid
validators = Validators;
constructor(@TbInject(RafService) public raf: RafService,
@TbInject(Store) protected store: Store<AppState>,
@TbInject(UntypedFormBuilder) public fb: UntypedFormBuilder,
@TbInject(Injector) public readonly $injector: Injector,
@TbInject('widgetContext') public readonly ctx: WidgetContext,
@TbInject('errorMessages') public readonly errorMessages: string[],
@TbInject('widgetTitlePanel') public readonly widgetTitlePanel: TemplateRef<any>) {
super(store);
this.ctx.$injector = $injector;
this.ctx.deviceService = $injector.get(DeviceService);
this.ctx.assetService = $injector.get(AssetService);
this.ctx.entityViewService = $injector.get(EntityViewService);
this.ctx.customerService = $injector.get(CustomerService);
this.ctx.dashboardService = $injector.get(DashboardService);
this.ctx.userService = $injector.get(UserService);
this.ctx.attributeService = $injector.get(AttributeService);
this.ctx.entityRelationService = $injector.get(EntityRelationService);
this.ctx.entityService = $injector.get(EntityService);
this.ctx.authService = $injector.get(AuthService);
this.ctx.dialogs = $injector.get(DialogService);
this.ctx.customDialog = $injector.get(CustomDialogService);
this.ctx.resourceService = $injector.get(ResourceService);
this.ctx.userSettingsService = $injector.get(UserSettingsService);
this.ctx.utilsService = $injector.get(UtilsService);
this.ctx.telemetryWsService = $injector.get(TelemetryWebsocketService);
this.ctx.date = $injector.get(DatePipe);
this.ctx.imagePipe = $injector.get(ImagePipe);
this.ctx.milliSecondsToTimeString = $injector.get(MillisecondsToTimeStringPipe);
this.ctx.translate = $injector.get(TranslateService);
this.ctx.http = $injector.get(HttpClient);
this.ctx.sanitizer = $injector.get(DomSanitizer);
this.ctx.router = $injector.get(Router);
public raf = inject(RafService);
public fb = inject(UntypedFormBuilder);
public readonly $injector = inject(Injector);
public readonly ctx = inject(widgetContextToken);
public readonly errorMessages = inject(widgetErrorMessagesToken);
public readonly widgetTitlePanel = inject(widgetTitlePanelToken);
constructor() {
super();
this.ctx.$injector = this.$injector;
this.ctx.deviceService = this.$injector.get(DeviceService);
this.ctx.assetService = this.$injector.get(AssetService);
this.ctx.entityViewService = this.$injector.get(EntityViewService);
this.ctx.customerService = this.$injector.get(CustomerService);
this.ctx.dashboardService = this.$injector.get(DashboardService);
this.ctx.userService = this.$injector.get(UserService);
this.ctx.attributeService = this.$injector.get(AttributeService);
this.ctx.entityRelationService = this.$injector.get(EntityRelationService);
this.ctx.entityService = this.$injector.get(EntityService);
this.ctx.authService = this.$injector.get(AuthService);
this.ctx.dialogs = this.$injector.get(DialogService);
this.ctx.customDialog = this.$injector.get(CustomDialogService);
this.ctx.resourceService = this.$injector.get(ResourceService);
this.ctx.userSettingsService = this.$injector.get(UserSettingsService);
this.ctx.utilsService = this.$injector.get(UtilsService);
this.ctx.telemetryWsService = this.$injector.get(TelemetryWebsocketService);
this.ctx.date = this.$injector.get(DatePipe);
this.ctx.imagePipe = this.$injector.get(ImagePipe);
this.ctx.milliSecondsToTimeString = this.$injector.get(MillisecondsToTimeStringPipe);
this.ctx.translate = this.$injector.get(TranslateService);
this.ctx.http = this.$injector.get(HttpClient);
this.ctx.sanitizer = this.$injector.get(DomSanitizer);
this.ctx.router = this.$injector.get(Router);
this.ctx.$scope = this;
if (this.ctx.defaultSubscription) {

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

@ -17,8 +17,7 @@
import {
AttributeData,
AttributeScope,
LatestTelemetry,
TelemetrySubscriber,
LatestTelemetry, SharedTelemetrySubscriber,
TelemetryType,
telemetryTypeTranslationsShort
} 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> {
protected targetEntityId: EntityId;
private telemetrySubscriber: TelemetrySubscriber;
private telemetrySubscriber: SharedTelemetrySubscriber;
protected constructor(protected ctx: WidgetContext,
protected settings: GetValueSettings<V>,
@ -470,10 +469,10 @@ export abstract class TelemetryValueGetter<V, S extends TelemetryValueSettings>
private subscribeForTelemetryValue(): Observable<V> {
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.telemetrySubscriber.subscribe();
return this.telemetrySubscriber.attributeData$().pipe(
return this.telemetrySubscriber.attributeData$.pipe(
map((data) => {
let value: V = null;
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 { Observable, Subject } from 'rxjs';
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 { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
@ -157,7 +157,7 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI
private updateQRCode(link: string) {
import('qrcode').then((QRCode) => {
QRCode.toString(link, (err, string) => {
unwrapModule(QRCode).toString(link, (err, string) => {
this.qrCodeSVG = string;
this.cd.markForCheck();
})

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

@ -28,7 +28,7 @@ import {
isNumber,
isObject,
parseFunction,
safeExecute
safeExecute, unwrapModule
} from '@core/utils';
interface QrCodeWidgetSettings {
@ -124,7 +124,7 @@ export class QrCodeWidgetComponent extends PageComponent implements OnInit, Afte
private updateCanvas() {
if (this.viewInited) {
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.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 { CustomActionDescriptor, WidgetAction } from '@shared/models/widget.models';
import { deepClone, isDefined, isUndefined } from '@core/utils';
import customSampleJs from '!raw-loader!./custom-sample-js.raw';
import customSampleCss from '!raw-loader!./custom-sample-css.raw';
import customSampleHtml from '!raw-loader!./custom-sample-html.raw';
import customSampleJs from './custom-sample-js.raw';
import customSampleCss from './custom-sample-css.raw';
import customSampleHtml from './custom-sample-html.raw';
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 { TranslateService } from '@ngx-translate/core';
import {
defaultGoogleMapProviderSettings,
defaultHereMapProviderSettings,
defaultImageMapProviderSettings,
defaultOpenStreetMapProviderSettings, defaultTencentMapProviderSettings,
GoogleMapProviderSettings,
HereMapProviderSettings,
ImageMapProviderSettings,
@ -39,7 +43,6 @@ import {
TencentMapProviderSettings
} from '@home/components/widget/lib/maps/map-models';
import { extractType } from '@core/utils';
import { keys } from 'ts-transformer-keys';
import { IAliasController } from '@core/api/widget-api.models';
@Component({
@ -128,11 +131,11 @@ export class MapProviderSettingsComponent extends PageComponent implements OnIni
writeValue(value: MapProviderSettings): void {
this.modelValue = value;
const provider = value?.provider;
const googleProviderSettings = extractType<GoogleMapProviderSettings>(value, keys<GoogleMapProviderSettings>());
const openstreetProviderSettings = extractType<OpenStreetMapProviderSettings>(value, keys<OpenStreetMapProviderSettings>());
const hereProviderSettings = extractType<HereMapProviderSettings>(value, keys<HereMapProviderSettings>());
const imageMapProviderSettings = extractType<ImageMapProviderSettings>(value, keys<ImageMapProviderSettings>());
const tencentMapProviderSettings = extractType<TencentMapProviderSettings>(value, keys<TencentMapProviderSettings>());
const googleProviderSettings = extractType<GoogleMapProviderSettings>(value, Object.keys(defaultGoogleMapProviderSettings) as (keyof GoogleMapProviderSettings)[]);
const openstreetProviderSettings = extractType<OpenStreetMapProviderSettings>(value, Object.keys(defaultOpenStreetMapProviderSettings) as (keyof OpenStreetMapProviderSettings)[]);
const hereProviderSettings = extractType<HereMapProviderSettings>(value, Object.keys(defaultHereMapProviderSettings) as (keyof HereMapProviderSettings)[]);
const imageMapProviderSettings = extractType<ImageMapProviderSettings>(value, Object.keys(defaultImageMapProviderSettings) as (keyof ImageMapProviderSettings)[]);
const tencentMapProviderSettings = extractType<TencentMapProviderSettings>(value, Object.keys(defaultTencentMapProviderSettings) as (keyof TencentMapProviderSettings)[]);
this.providerSettingsFormGroup.patchValue(
{
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 {
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
Validator
} from '@angular/forms';
import { PageComponent } from '@shared/components/page.component';
@ -31,6 +31,13 @@ import { TranslateService } from '@ngx-translate/core';
import {
CircleSettings,
CommonMapSettings,
defaultCircleSettings,
defaultCommonMapSettings,
defaultMapEditorSettings,
defaultMapProviderSettings,
defaultMarkerClusteringSettings,
defaultMarkersSettings,
defaultPolygonSettings,
MapEditorSettings,
MapProviders,
MapProviderSettings,
@ -41,7 +48,6 @@ import {
UnitedMapSettings
} from '@home/components/widget/lib/maps/map-models';
import { extractType } from '@core/utils';
import { keys } from 'ts-transformer-keys';
import { IAliasController } from '@core/api/widget-api.models';
import { Widget } from '@shared/models/widget.models';
@ -140,12 +146,12 @@ export class MapSettingsComponent extends PageComponent implements OnInit, Contr
writeValue(value: UnitedMapSettings): void {
this.modelValue = value;
const mapProviderSettings = extractType<MapProviderSettings>(value, keys<MapProviderSettings>());
const commonMapSettings = extractType<CommonMapSettings>(value, keys<CommonMapSettings>());
const markersSettings = extractType<MarkersSettings>(value, keys<MarkersSettings>());
const polygonSettings = extractType<PolygonSettings>(value, keys<PolygonSettings>());
const circleSettings = extractType<CircleSettings>(value, keys<CircleSettings>());
const mapEditorSettings = extractType<MapEditorSettings>(value, keys<MapEditorSettings>());
const mapProviderSettings = extractType<MapProviderSettings>(value, Object.keys(defaultMapProviderSettings) as (keyof MapProviderSettings)[]);
const commonMapSettings = extractType<CommonMapSettings>(value, Object.keys(defaultCommonMapSettings) as (keyof CommonMapSettings)[]);
const markersSettings = extractType<MarkersSettings>(value, Object.keys(defaultMarkersSettings) as (keyof MarkersSettings)[]);
const polygonSettings = extractType<PolygonSettings>(value, Object.keys(defaultPolygonSettings) as (keyof PolygonSettings)[]);
const circleSettings = extractType<CircleSettings>(value, Object.keys(defaultCircleSettings) as (keyof CircleSettings)[]);
const mapEditorSettings = extractType<MapEditorSettings>(value, Object.keys(defaultMapEditorSettings) as (keyof MapEditorSettings)[]);
const formValue = {
mapProviderSettings,
commonMapSettings,
@ -157,7 +163,7 @@ export class MapSettingsComponent extends PageComponent implements OnInit, Contr
if (this.routeMap) {
formValue.routeMapSettings = extractType<PolylineSettings>(value, ['strokeWeight', 'strokeOpacity']);
} 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.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 {
CircleSettings,
CommonMapSettings,
defaultCircleSettings,
defaultCommonMapSettings,
defaultMapProviderSettings,
defaultMarkersSettings,
defaultPolygonSettings,
defaultTripAnimationPathSettings,
defaultTripAnimationPointSettings,
defaultTripAnimationSettings,
MapProviderSettings,
MarkersSettings,
PointsSettings,
PolygonSettings,
PolylineSettings,
TripAnimationCommonSettings,
TripAnimationMarkerSettings
PolylineSettings
} from 'src/app/modules/home/components/widget/lib/maps/map-models';
import { extractType } from '@core/utils';
import { keys } from 'ts-transformer-keys';
@Component({
selector: 'tb-trip-animation-widget-settings',
@ -69,13 +75,13 @@ export class TripAnimationWidgetSettingsComponent extends WidgetSettingsComponen
}
protected prepareInputSettings(settings: WidgetSettings): WidgetSettings {
const mapProviderSettings = extractType<MapProviderSettings>(settings, keys<MapProviderSettings>());
const commonMapSettings = extractType<TripAnimationCommonSettings>(settings, keys<TripAnimationCommonSettings>());
const markersSettings = extractType<TripAnimationMarkerSettings>(settings, keys<TripAnimationMarkerSettings>());
const pathSettings = extractType<PolylineSettings>(settings, keys<PolylineSettings>());
const pointSettings = extractType<PointsSettings>(settings, keys<PointsSettings>());
const polygonSettings = extractType<PolygonSettings>(settings, keys<PolygonSettings>());
const circleSettings = extractType<CircleSettings>(settings, keys<CircleSettings>());
const mapProviderSettings = extractType<MapProviderSettings>(settings, Object.keys(defaultMapProviderSettings) as (keyof MapProviderSettings)[]);
const commonMapSettings = extractType<CommonMapSettings>(settings, Object.keys(defaultCommonMapSettings) as (keyof CommonMapSettings)[]);
const markersSettings = extractType<MarkersSettings>(settings, Object.keys(defaultMarkersSettings) as (keyof MarkersSettings)[]);
const pathSettings = extractType<PolylineSettings>(settings, Object.keys(defaultTripAnimationPathSettings) as (keyof PolylineSettings)[]);
const pointSettings = extractType<PointsSettings>(settings, Object.keys(defaultTripAnimationPointSettings) as (keyof PointsSettings)[]);
const polygonSettings = extractType<PolygonSettings>(settings, Object.keys(defaultPolygonSettings) as (keyof PolygonSettings)[]);
const circleSettings = extractType<CircleSettings>(settings, Object.keys(defaultCircleSettings) as (keyof CircleSettings)[]);
return {
mapProviderSettings,
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.cssjs = cssjs;
w.moment = moment;
w.$ = $;
w.jQuery = $;
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(
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))}"
(mousedown)="$event.stopPropagation()">
<button mat-icon-button *ngFor="let action of widget.customHeaderActions"
type="button"
[class.!hidden]="isEdit"
(click)="action.onAction($event)"
matTooltip="{{ action.displayName }}"
@ -45,6 +46,7 @@
<tb-icon>{{ action.icon }}</tb-icon>
</button>
<button mat-icon-button *ngFor="let action of widget.widgetActions"
type="button"
[class.!hidden]="isEdit || !action.show"
(click)="action.onAction($event)"
matTooltip="{{ action.name | translate }}"
@ -52,6 +54,7 @@
<tb-icon>{{ action.icon }}</tb-icon>
</button>
<button mat-icon-button
type="button"
[class.!hidden]="isEdit || !widget.enableFullscreen"
(click)="$event.stopPropagation(); widget.isFullscreen = !widget.isFullscreen; updateEditWidgetActionsTooltipState()"
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,
ShowWidgetHeaderActionFunction,
updateEntityParams,
WidgetContext,
WidgetContext, widgetContextToken, widgetErrorMessagesToken,
WidgetHeaderAction,
WidgetInfo,
WidgetInfo, widgetTitlePanelToken,
WidgetTypeInstance
} from '@home/models/widget-component.models';
import {
@ -743,15 +743,15 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
{
providers: [
{
provide: 'widgetContext',
provide: widgetContextToken,
useValue: this.widgetContext
},
{
provide: 'errorMessages',
provide: widgetErrorMessagesToken,
useValue: this.errorMessages
},
{
provide: 'widgetTitlePanel',
provide: widgetTitlePanelToken,
useValue: this.widgetTitlePanel
}
],

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

@ -25,7 +25,7 @@ import {
AttributeData,
AttributeScope,
isClientSideTelemetryType,
TelemetrySubscriber,
SharedTelemetrySubscriber,
TelemetryType
} from '@shared/models/telemetry/telemetry.models';
import { AttributeService } from '@core/http/attribute.service';
@ -42,7 +42,7 @@ export class AttributeDatasource implements DataSource<AttributeData> {
public selection = new SelectionModel<AttributeData>(true, []);
private allAttributes: Observable<Array<AttributeData>>;
private telemetrySubscriber: TelemetrySubscriber;
private telemetrySubscriber: SharedTelemetrySubscriber;
constructor(private attributeService: AttributeService,
private telemetryWsService: TelemetryWebsocketService,
@ -99,10 +99,10 @@ export class AttributeDatasource implements DataSource<AttributeData> {
if (!this.allAttributes) {
let attributesObservable: Observable<Array<AttributeData>>;
if (isClientSideTelemetryType.get(attributesScope)) {
this.telemetrySubscriber = TelemetrySubscriber.createEntityAttributesSubscription(
this.telemetrySubscriber = SharedTelemetrySubscriber.createEntityAttributesSubscription(
this.telemetryWsService, entityId, attributesScope, this.zone);
this.telemetrySubscriber.subscribe();
attributesObservable = this.telemetrySubscriber.attributeData$();
attributesObservable = this.telemetrySubscriber.attributeData$;
} else {
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,
WidgetSubscriptionApi
} 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 { RafService } from '@core/services/raf.service';
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 { EntityId } from '@shared/models/id/entity-id';
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 { UserSettingsService } from '@core/http/user-settings.service';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
@ -204,7 +206,7 @@ export class WidgetContext {
userSettingsService: UserSettingsService;
utilsService: UtilsService;
telemetryWsService: TelemetryWebsocketService;
telemetrySubscribers?: TelemetrySubscriber[];
telemetrySubscribers?: Array<TelemetrySubscriber | SharedTelemetrySubscriber>;
date: DatePipe;
imagePipe: ImagePipe;
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 {
readonly ctx: WidgetContext;
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>
<div class="tb-form-panel-title" translate>notification.delivery-method.microsoft-teams-preview</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>
{{ preview.processedTemplates.MICROSOFT_TEAMS.body }}
<button mat-raised-button>{{ preview.processedTemplates.MICROSOFT_TEAMS.button.text }}</button>
</div>
</section>
</div>

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

@ -190,6 +190,8 @@
&.mini {
font-size: 12px;
line-height: 1.25;
justify-content: start;
gap: 5px;
.subject {
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>> = [];
columns.push(
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') {
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 { deepClone } from '@core/utils';
import printTemplate from '!raw-loader!./backup-code-print-template.raw';
import printTemplate from './backup-code-print-template.raw';
@Component({
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
} from '@shared/models/two-factor-auth.models';
import { MatStepper } from '@angular/material/stepper';
import { unwrapModule } from '@core/utils';
@Component({
selector: 'tb-totp-auth-dialog',
@ -56,7 +57,7 @@ export class TotpAuthDialogComponent extends DialogComponent<TotpAuthDialogCompo
this.totpAuthURL = this.authAccountConfig.authUrl;
this.authAccountConfig.useByDefault = true;
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.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">
<h2 translate>widget.select-widget-type</h2>
<span class="flex-1"></span>
<div tb-help="widgetTypes"></div>
<button mat-icon-button
(click)="cancel()"
type="button">

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

@ -15,7 +15,7 @@
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-actions class="flex items-center justify-end">
<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 { CommonModule } from '@angular/common';
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 { RuleChainMetaData } from '@shared/models/rule-chain.models';
interface EntityConflictDialogData {
message: string;
entity: EntityInfoData | RuleChainMetaData;
entity: VersionedEntity;
}
@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('entityId').patchValue(this.modelValue.id, {emitEvent: false});
this.entitySelectFormGroup.get('entityId').patchValue(this.modelValue, {emitEvent: false});
}
updateView(entityType: EntityType | AliasEntityType | null, entityId: string | null) {

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

Loading…
Cancel
Save