Browse Source

Merge branch 'develop/3.6' into device-state-service/fix-flaky-tests

pull/9155/head
Dmytro Skarzhynets 3 years ago
parent
commit
ba67ca5e09
  1. 6772
      application/src/main/data/json/demo/dashboards/gateway_list.json
  2. 9
      application/src/main/data/json/system/widget_bundles/gateway_widgets.json
  3. 2
      application/src/main/data/json/system/widget_types/compass.json
  4. 19
      application/src/main/data/json/system/widget_types/gateway_configuration2.json
  5. 19
      application/src/main/data/json/system/widget_types/gateway_connector.json
  6. 23
      application/src/main/data/json/system/widget_types/gateway_general_statistics.json
  7. 20
      application/src/main/data/json/system/widget_types/gateway_logs.json
  8. 23
      application/src/main/data/json/system/widget_types/gateway_statistics.json
  9. 2
      application/src/main/data/json/system/widget_types/radial_gauge.json
  10. 20
      application/src/main/data/json/system/widget_types/service_rpc.json
  11. 2
      application/src/main/data/json/system/widget_types/speed_gauge.json
  12. 2
      application/src/main/data/json/system/widget_types/temperature_radial_gauge.json
  13. 2
      application/src/main/data/json/system/widget_types/thermometer_scale.json
  14. 2
      application/src/main/data/upgrade/3.5.1/schema_update.sql
  15. 36
      application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java
  16. 17
      application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
  17. 5
      application/src/main/java/org/thingsboard/server/service/component/RuleNodeClassInfo.java
  18. 7
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java
  19. 13
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  20. 48
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  21. 4
      application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java
  22. 4
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java
  23. 5
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java
  24. 6
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java
  25. 4
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java
  26. 2
      application/src/main/resources/logback.xml
  27. 13
      application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java
  28. 3
      application/src/test/java/org/thingsboard/server/edge/WidgetEdgeTest.java
  29. 53
      application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java
  30. 25
      build_proto.sh
  31. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityService.java
  32. 1
      common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
  33. 14
      common/edge-api/src/main/proto/edge.proto
  34. 4
      common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java
  35. 19
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DateTimeFormatOptions.java
  36. 17
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java
  37. 2
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java
  38. 21
      common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java
  39. 35
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java
  40. 2
      dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeDao.java
  41. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java
  42. 13
      dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java
  43. 49
      dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java
  44. 5
      dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java
  45. 158
      dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDaoTest.java
  46. 2
      pom.xml
  47. 16
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNode.java
  48. 35
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbVersionedNode.java
  49. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java
  50. 30
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java
  51. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractNodeWithFetchTo.java
  52. 8
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java
  53. 150
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java
  54. 6
      ui-ngx/src/app/core/http/device.service.ts
  55. 7
      ui-ngx/src/app/core/utils.ts
  56. 8
      ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html
  57. 10
      ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts
  58. 2
      ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.html
  59. 5
      ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.ts
  60. 10
      ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-config.ts
  61. 75
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.html
  62. 37
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.scss
  63. 98
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.ts
  64. 833
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html
  65. 143
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.scss
  66. 721
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts
  67. 198
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html
  68. 114
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.scss
  69. 517
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts
  70. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.html
  71. 1
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.ts
  72. 61
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.html
  73. 56
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.scss
  74. 237
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.ts
  75. 57
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-remote-configuration-dialog.html
  76. 61
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-remote-configuration-dialog.ts
  77. 53
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html
  78. 55
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss
  79. 130
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts
  80. 81
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html
  81. 86
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.scss
  82. 299
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.ts
  83. 6
      ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html
  84. 4
      ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.scss
  85. 7
      ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts
  86. 4
      ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html
  87. 16
      ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts
  88. 26
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-logs-settings.component.html
  89. 54
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-logs-settings.component.ts
  90. 22
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component.html
  91. 52
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component.ts
  92. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  93. 23
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  94. 1
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  95. 1
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts
  96. 11
      ui-ngx/src/app/modules/home/pages/home-pages.models.ts
  97. 2
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.html
  98. 3
      ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html
  99. 3
      ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts
  100. 37
      ui-ngx/src/app/shared/components/directives/tb-json-to-string.directive.ts

6772
application/src/main/data/json/demo/dashboards/gateway_list.json

File diff suppressed because it is too large

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

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

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

@ -15,7 +15,7 @@
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-analogue-compass-widget-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"minorTicks\":22,\"needleCircleSize\":15,\"showBorder\":true,\"borderOuterWidth\":10,\"colorPlate\":\"#222\",\"colorMajorTicks\":\"#f5f5f5\",\"colorMinorTicks\":\"#ddd\",\"colorNeedle\":\"#f08080\",\"colorNeedleCircle\":\"#e8e8e8\",\"colorBorder\":\"#ccc\",\"majorTickFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#ccc\"},\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\",\"animationTarget\":\"needle\",\"majorTicks\":[\"N\",\"NE\",\"E\",\"SE\",\"S\",\"SW\",\"W\",\"NW\"]},\"title\":\"Compass\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"minorTicks\":22,\"needleCircleSize\":15,\"showBorder\":true,\"borderOuterWidth\":10,\"colorPlate\":\"#222\",\"colorMajorTicks\":\"#f5f5f5\",\"colorMinorTicks\":\"#ddd\",\"colorNeedle\":\"#f08080\",\"colorNeedleCircle\":\"#e8e8e8\",\"colorBorder\":\"#ccc\",\"majorTickFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#ccc\"},\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\",\"animationTarget\":\"needle\",\"majorTicks\":[\"N\",\"NE\",\"E\",\"SE\",\"S\",\"SW\",\"W\",\"NW\"]},\"title\":\"Compass\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\"}"
},
"externalId": null
}

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

@ -15,7 +15,7 @@
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-analogue-radial-gauge-widget-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nif (value < -100) {\\n\\tvalue = -100;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":100,\"startAngle\":45,\"ticksAngle\":270,\"showBorder\":true,\"defaultColor\":\"#e65100\",\"needleCircleSize\":10,\"highlights\":[],\"showUnitTitle\":true,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":10,\"valueInt\":3,\"valueDec\":0,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"numbersFont\":{\"family\":\"Roboto\",\"size\":18,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"size\":36,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\"},\"minValue\":-100,\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\"},\"title\":\"Radial gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}"
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nif (value < -100) {\\n\\tvalue = -100;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":100,\"startAngle\":45,\"ticksAngle\":270,\"showBorder\":true,\"defaultColor\":\"#e65100\",\"needleCircleSize\":10,\"highlights\":[],\"showUnitTitle\":true,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":10,\"valueInt\":3,\"valueDec\":0,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"numbersFont\":{\"family\":\"Roboto\",\"size\":18,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"size\":36,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\"},\"minValue\":-100,\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\"},\"title\":\"Radial gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\"}"
},
"externalId": null
}

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

File diff suppressed because one or more lines are too long

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

@ -15,7 +15,7 @@
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-analogue-radial-gauge-widget-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 220) {\\n\\tvalue = 220;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":180,\"startAngle\":45,\"ticksAngle\":270,\"showBorder\":false,\"defaultColor\":\"#e65100\",\"needleCircleSize\":7,\"highlights\":[{\"from\":80,\"to\":120,\"color\":\"#fdd835\"},{\"color\":\"#e57373\",\"from\":120,\"to\":180}],\"showUnitTitle\":false,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":2,\"valueInt\":3,\"minValue\":0,\"valueDec\":0,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":1500,\"animationRule\":\"linear\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"units\":\"MPH\",\"majorTicksCount\":9,\"numbersFont\":{\"family\":\"Roboto\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"size\":32,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\",\"family\":\"Segment7Standard\"},\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\"},\"title\":\"Speed gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}"
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 220) {\\n\\tvalue = 220;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":180,\"startAngle\":45,\"ticksAngle\":270,\"showBorder\":false,\"defaultColor\":\"#e65100\",\"needleCircleSize\":7,\"highlights\":[{\"from\":80,\"to\":120,\"color\":\"#fdd835\"},{\"color\":\"#e57373\",\"from\":120,\"to\":180}],\"showUnitTitle\":false,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":2,\"valueInt\":3,\"minValue\":0,\"valueDec\":0,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":1500,\"animationRule\":\"linear\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"units\":\"MPH\",\"majorTicksCount\":9,\"numbersFont\":{\"family\":\"Roboto\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"size\":32,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\",\"family\":\"Segment7Standard\"},\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\"},\"title\":\"Speed gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\"}"
},
"externalId": null
}

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

@ -15,7 +15,7 @@
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-analogue-radial-gauge-widget-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":60,\"startAngle\":67.5,\"ticksAngle\":225,\"showBorder\":true,\"defaultColor\":\"#e65100\",\"needleCircleSize\":7,\"highlights\":[{\"from\":-60,\"to\":-50,\"color\":\"#42a5f5\"},{\"from\":-50,\"to\":-40,\"color\":\"rgba(66, 165, 245, 0.83)\"},{\"from\":-40,\"to\":-30,\"color\":\"rgba(66, 165, 245, 0.66)\"},{\"from\":-30,\"to\":-20,\"color\":\"rgba(66, 165, 245, 0.5)\"},{\"from\":-20,\"to\":-10,\"color\":\"rgba(66, 165, 245, 0.33)\"},{\"from\":-10,\"to\":0,\"color\":\"rgba(66, 165, 245, 0.16)\"},{\"from\":0,\"to\":10,\"color\":\"rgba(229, 115, 115, 0.16)\"},{\"from\":10,\"to\":20,\"color\":\"rgba(229, 115, 115, 0.33)\"},{\"from\":20,\"to\":30,\"color\":\"rgba(229, 115, 115, 0.5)\"},{\"from\":30,\"to\":40,\"color\":\"rgba(229, 115, 115, 0.66)\"},{\"from\":40,\"to\":50,\"color\":\"rgba(229, 115, 115, 0.83)\"},{\"from\":50,\"to\":60,\"color\":\"#e57373\"}],\"showUnitTitle\":true,\"colorPlate\":\"#cfd8dc\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":2,\"valueInt\":3,\"valueDec\":1,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":1000,\"animationRule\":\"bounce\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"units\":\"°C\",\"majorTicksCount\":12,\"numbersFont\":{\"family\":\"Roboto\",\"size\":20,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"size\":30,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\"},\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"unitTitle\":\"Temperature\",\"minValue\":-60},\"title\":\"Temperature radial gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}"
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":60,\"startAngle\":67.5,\"ticksAngle\":225,\"showBorder\":true,\"defaultColor\":\"#e65100\",\"needleCircleSize\":7,\"highlights\":[{\"from\":-60,\"to\":-50,\"color\":\"#42a5f5\"},{\"from\":-50,\"to\":-40,\"color\":\"rgba(66, 165, 245, 0.83)\"},{\"from\":-40,\"to\":-30,\"color\":\"rgba(66, 165, 245, 0.66)\"},{\"from\":-30,\"to\":-20,\"color\":\"rgba(66, 165, 245, 0.5)\"},{\"from\":-20,\"to\":-10,\"color\":\"rgba(66, 165, 245, 0.33)\"},{\"from\":-10,\"to\":0,\"color\":\"rgba(66, 165, 245, 0.16)\"},{\"from\":0,\"to\":10,\"color\":\"rgba(229, 115, 115, 0.16)\"},{\"from\":10,\"to\":20,\"color\":\"rgba(229, 115, 115, 0.33)\"},{\"from\":20,\"to\":30,\"color\":\"rgba(229, 115, 115, 0.5)\"},{\"from\":30,\"to\":40,\"color\":\"rgba(229, 115, 115, 0.66)\"},{\"from\":40,\"to\":50,\"color\":\"rgba(229, 115, 115, 0.83)\"},{\"from\":50,\"to\":60,\"color\":\"#e57373\"}],\"showUnitTitle\":true,\"colorPlate\":\"#cfd8dc\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":2,\"valueInt\":3,\"valueDec\":1,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":1000,\"animationRule\":\"bounce\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"units\":\"°C\",\"majorTicksCount\":12,\"numbersFont\":{\"family\":\"Roboto\",\"size\":20,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"size\":30,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\"},\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"unitTitle\":\"Temperature\",\"minValue\":-60},\"title\":\"Temperature radial gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\"}"
},
"externalId": null
}

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

@ -15,7 +15,7 @@
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-analogue-linear-gauge-widget-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 30 - 15;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":100,\"defaultColor\":\"#e64a19\",\"barStrokeWidth\":2.5,\"colorBar\":\"rgba(255, 255, 255, 0.4)\",\"colorBarEnd\":\"rgba(221, 221, 221, 0.38)\",\"showUnitTitle\":true,\"minorTicks\":2,\"valueBox\":true,\"valueInt\":3,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"colorNeedleShadowUp\":\"rgba(2,255,255,0.2)\",\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"highlightsWidth\":10,\"animation\":true,\"animationDuration\":1500,\"animationRule\":\"linear\",\"showBorder\":false,\"majorTicksCount\":8,\"numbersFont\":{\"family\":\"Arial\",\"size\":18,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#78909c\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":26,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#37474f\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":40,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#444\",\"shadowColor\":\"rgba(0,0,0,0.3)\"},\"minValue\":-60,\"highlights\":[{\"from\":-60,\"to\":-40,\"color\":\"#90caf9\"},{\"from\":-40,\"to\":-20,\"color\":\"rgba(144, 202, 249, 0.66)\"},{\"from\":-20,\"to\":0,\"color\":\"rgba(144, 202, 249, 0.33)\"},{\"from\":0,\"to\":20,\"color\":\"rgba(244, 67, 54, 0.2)\"},{\"from\":20,\"to\":40,\"color\":\"rgba(244, 67, 54, 0.4)\"},{\"from\":40,\"to\":60,\"color\":\"rgba(244, 67, 54, 0.6)\"},{\"from\":60,\"to\":80,\"color\":\"rgba(244, 67, 54, 0.8)\"},{\"from\":80,\"to\":100,\"color\":\"#f44336\"}],\"unitTitle\":\"Temperature\",\"units\":\"°C\",\"colorBarProgress\":\"#90caf9\",\"colorBarProgressEnd\":\"#f44336\",\"colorBarStroke\":\"#b0bec5\",\"valueDec\":1},\"title\":\"Thermometer scale\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}"
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 30 - 15;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":100,\"defaultColor\":\"#e64a19\",\"barStrokeWidth\":2.5,\"colorBar\":\"rgba(255, 255, 255, 0.4)\",\"colorBarEnd\":\"rgba(221, 221, 221, 0.38)\",\"showUnitTitle\":true,\"minorTicks\":2,\"valueBox\":true,\"valueInt\":3,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"colorNeedleShadowUp\":\"rgba(2,255,255,0.2)\",\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"highlightsWidth\":10,\"animation\":true,\"animationDuration\":1500,\"animationRule\":\"linear\",\"showBorder\":false,\"majorTicksCount\":8,\"numbersFont\":{\"family\":\"Arial\",\"size\":18,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#78909c\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":26,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#37474f\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":40,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#444\",\"shadowColor\":\"rgba(0,0,0,0.3)\"},\"minValue\":-60,\"highlights\":[{\"from\":-60,\"to\":-40,\"color\":\"#90caf9\"},{\"from\":-40,\"to\":-20,\"color\":\"rgba(144, 202, 249, 0.66)\"},{\"from\":-20,\"to\":0,\"color\":\"rgba(144, 202, 249, 0.33)\"},{\"from\":0,\"to\":20,\"color\":\"rgba(244, 67, 54, 0.2)\"},{\"from\":20,\"to\":40,\"color\":\"rgba(244, 67, 54, 0.4)\"},{\"from\":40,\"to\":60,\"color\":\"rgba(244, 67, 54, 0.6)\"},{\"from\":60,\"to\":80,\"color\":\"rgba(244, 67, 54, 0.8)\"},{\"from\":80,\"to\":100,\"color\":\"#f44336\"}],\"unitTitle\":\"Temperature\",\"units\":\"°C\",\"colorBarProgress\":\"#90caf9\",\"colorBarProgressEnd\":\"#f44336\",\"colorBarStroke\":\"#b0bec5\",\"valueDec\":1},\"title\":\"Thermometer scale\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\"}"
},
"externalId": null
}

2
application/src/main/data/upgrade/3.5.1/schema_update.sql

@ -127,6 +127,8 @@ UPDATE resource
ALTER TABLE notification_request ALTER COLUMN info SET DATA TYPE varchar(1000000);
DELETE FROM alarm WHERE tenant_id NOT IN (SELECT id FROM tenant);
CREATE TABLE IF NOT EXISTS alarm_types (
tenant_id uuid NOT NULL,
type varchar(255) NOT NULL,

36
application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java

@ -31,7 +31,9 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.dao.device.DeviceConnectivityService;
@ -87,6 +89,36 @@ public class DeviceConnectivityController extends BaseController {
return deviceConnectivityService.findDevicePublishTelemetryCommands(baseUrl, device);
}
@ApiOperation(value = "Get commands to launch gateway (getGatewayLaunchCommands)",
notes = "Fetch the list of commands for different operation systems to launch a gateway using docker." +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@ApiResponses(value = {
@ApiResponse(code = 200, message = "OK",
examples = @io.swagger.annotations.Example(
value = {
@io.swagger.annotations.ExampleProperty(
mediaType = "application/json",
value = "{\"mqtt\": {\n" +
" \"linux\": \"docker run --rm -it -v ~/.tb-gateway/logs:/thingsboard_gateway/logs -v ~/.tb-gateway/extensions:/thingsboard_gateway/extensions -v ~/.tb-gateway/config:/thingsboard_gateway/config --name tbGateway127001 -e host=localhost -e port=1883 -e accessToken=qTe5oDBHPJf0KCSKO8J3 --restart always thingsboard/tb-gateway\",\n" +
" \"windows\": \"docker run --rm -it -v %HOMEPATH%/tb-gateway/logs:/thingsboard_gateway/logs -v %HOMEPATH%/tb-gateway/extensions:/thingsboard_gateway/extensions -v %HOMEPATH%/tb-gateway/config:/thingsboard_gateway/config --name tbGateway127001 -e host=localhost -e port=1883 -e accessToken=qTe5oDBHPJf0KCSKO8J3 --restart always thingsboard/tb-gateway\"}\n" +
"}")}))})
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/device-connectivity/gateway-launch/{deviceId}", method = RequestMethod.GET)
@ResponseBody
public JsonNode getGatewayLaunchCommands(@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION)
@PathVariable(DEVICE_ID) String strDeviceId, HttpServletRequest request) throws ThingsboardException, URISyntaxException {
checkParameter(DEVICE_ID, strDeviceId);
DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
Device device = checkDeviceId(deviceId, Operation.READ_CREDENTIALS);
if (!checkIsGateway(device)) {
throw new ThingsboardException("The device must be a gateway!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
String baseUrl = systemSecurityService.getBaseUrl(getTenantId(), getCurrentUser().getCustomerId(), request);
return deviceConnectivityService.findGatewayLaunchCommands(baseUrl, device);
}
@ApiOperation(value = "Download server certificate using file path defined in device.connectivity properties (downloadServerCertificate)", notes = "Download server certificate.")
@RequestMapping(value = "/device-connectivity/{protocol}/certificate/download", method = RequestMethod.GET)
@ResponseBody
@ -104,4 +136,8 @@ public class DeviceConnectivityController extends BaseController {
.body(pemCert);
}
private static boolean checkIsGateway(Device device) {
return device.getAdditionalInfo().has(DataConstants.GATEWAY_PARAMETER) &&
device.getAdditionalInfo().get(DataConstants.GATEWAY_PARAMETER).asBoolean();
}
}

17
application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java

@ -30,13 +30,12 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.rule.engine.api.NodeDefinition;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.rule.engine.api.TbVersionedNode;
import org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode;
import org.thingsboard.rule.engine.filter.TbOriginatorTypeSwitchNode;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.rule.RuleChainType;
@ -89,15 +88,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
try {
var clazz = Class.forName(clazzName);
RuleNode annotation = clazz.getAnnotation(RuleNode.class);
boolean versioned = false;
if (annotation.version() > 0) { // No need to process nodes that has version = 0;
if (TbVersionedNode.class.isAssignableFrom(clazz)) {
versioned = true;
} else {
log.error("RuleNode [{}] has version {} but does not implement TbVersionedNode interface! Any update procedures for this rule node will be skipped!", clazzName, annotation.version());
}
}
ruleNodeClasses.put(clazzName, new RuleNodeClassInfo(clazz, annotation, versioned));
ruleNodeClasses.put(clazzName, new RuleNodeClassInfo(clazz, annotation));
} catch (Exception e) {
log.warn("Failed to create instance of rule node type: {} due to: ", clazzName, e);
}
@ -194,7 +185,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
scannedComponent.setType(type);
Class<?> clazz = def.getClazz();
RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class);
scannedComponent.setConfigurationVersion(def.isVersioned() ? def.getCurrentVersion() : 0);
scannedComponent.setConfigurationVersion(def.getCurrentVersion());
scannedComponent.setName(ruleNodeAnnotation.name());
scannedComponent.setScope(ruleNodeAnnotation.scope());
scannedComponent.setClusteringMode(ruleNodeAnnotation.clusteringMode());

5
application/src/main/java/org/thingsboard/server/service/component/RuleNodeClassInfo.java

@ -23,7 +23,6 @@ public class RuleNodeClassInfo {
private final Class<?> clazz;
private final RuleNode annotation;
private final boolean versioned;
public String getClassName(){
return clazz.getName();
@ -37,4 +36,8 @@ public class RuleNodeClassInfo {
return annotation.version();
}
public boolean isVersioned() {
return annotation.version() > 0;
}
}

7
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java

@ -35,6 +35,13 @@ public class WidgetTypeMsgConstructor {
.setIdLSB(widgetTypeDetails.getId().getId().getLeastSignificantBits());
if (widgetTypeDetails.getFqn() != null) {
builder.setFqn(widgetTypeDetails.getFqn());
if (widgetTypeDetails.getFqn().contains(".")) {
String[] aliases = widgetTypeDetails.getFqn().split("\\.", 2);
if (aliases.length == 2) {
builder.setBundleAlias(aliases[0]);
builder.setAlias(aliases[1]);
}
}
}
if (widgetTypeDetails.getName() != null) {
builder.setName(widgetTypeDetails.getName());

13
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java

@ -731,8 +731,8 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
break;
case "3.5.1":
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
log.info("Updating schema ...");
if (isOldSchema(conn, 3005001)) {
log.info("Updating schema ...");
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.5.1", SCHEMA_UPDATE_SQL);
loadSql(schemaUpdateFile, conn);
@ -756,14 +756,23 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_rule_node_type_configuration_version ON rule_node(type, configuration_version);");
} catch (Exception e) {
}
try {
conn.createStatement().execute("UPDATE rule_node SET " +
"configuration = (configuration::jsonb || '{\"updateAttributesOnlyOnValueChange\": \"false\"}'::jsonb)::varchar, " +
"configuration_version = 1 " +
"WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode' AND configuration_version < 1;");
} catch (Exception e) {
}
try {
conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_unread ON notification(recipient_id) WHERE status <> 'READ';");
} catch (Exception e) {
}
conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3005002;");
log.info("Schema updated to version 3.5.2.");
} else {
log.info("Skip schema re-update to version 3.5.2. Use env flag 'SKIP_SCHEMA_VERSION_CHECK' to force the re-update.");
}
log.info("Schema updated.");
} catch (Exception e) {
log.error("Failed updating schema!!!", e);
}

48
application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java

@ -27,7 +27,7 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbVersionedNode;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.flow.TbRuleChainInputNode;
import org.thingsboard.rule.engine.flow.TbRuleChainInputNodeConfiguration;
import org.thingsboard.rule.engine.profile.TbDeviceProfileNode;
@ -78,6 +78,7 @@ import org.thingsboard.server.dao.model.sql.DeviceProfileEntity;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.sql.JpaExecutorService;
import org.thingsboard.server.dao.sql.device.DeviceProfileRepository;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
@ -88,9 +89,7 @@ import org.thingsboard.server.service.install.SystemDataLoaderService;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
@ -103,6 +102,8 @@ import static org.thingsboard.server.common.data.StringUtils.isBlank;
@Slf4j
public class DefaultDataUpdateService implements DataUpdateService {
private static final int MAX_PENDING_SAVE_RULE_NODE_FUTURES = 100;
@Autowired
private TenantService tenantService;
@ -155,6 +156,9 @@ public class DefaultDataUpdateService implements DataUpdateService {
@Autowired
private EdgeEventDao edgeEventDao;
@Autowired
JpaExecutorService jpaExecutorService;
@Override
public void updateData(String fromVersion) throws Exception {
switch (fromVersion) {
@ -227,13 +231,15 @@ public class DefaultDataUpdateService implements DataUpdateService {
@Override
public void upgradeRuleNodes() {
try {
var futures = new ArrayList<ListenableFuture<?>>(100);
int totalRuleNodesUpgraded = 0;
log.info("Starting rule nodes upgrade ...");
var nodeClassToVersionMap = componentDiscoveryService.getVersionedNodes();
log.debug("Found {} versioned nodes to check for upgrade!", nodeClassToVersionMap.size());
nodeClassToVersionMap.forEach(clazz -> {
var ruleNodeType = clazz.getClassName();
var ruleNodeTypeForLogs = clazz.getSimpleName();
var toVersion = clazz.getCurrentVersion();
for (var ruleNodeClassInfo : nodeClassToVersionMap) {
var ruleNodeType = ruleNodeClassInfo.getClassName();
var ruleNodeTypeForLogs = ruleNodeClassInfo.getSimpleName();
var toVersion = ruleNodeClassInfo.getCurrentVersion();
log.debug("Going to check for nodes with type: {} to upgrade to version: {}.", ruleNodeTypeForLogs, toVersion);
var ruleNodesToUpdate = new PageDataIterable<>(
pageLink -> ruleChainService.findAllRuleNodesByTypeAndVersionLessThan(ruleNodeType, toVersion, pageLink), 1024
@ -248,28 +254,44 @@ public class DefaultDataUpdateService implements DataUpdateService {
log.debug("Going to upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {}",
ruleNodeId, ruleNodeTypeForLogs, fromVersion, toVersion);
try {
var tbVersionedNode = (TbVersionedNode) clazz.getClazz().getDeclaredConstructor().newInstance();
var tbVersionedNode = (TbNode) ruleNodeClassInfo.getClazz().getDeclaredConstructor().newInstance();
TbPair<Boolean, JsonNode> upgradeRuleNodeConfigurationResult = tbVersionedNode.upgrade(fromVersion, oldConfiguration);
if (upgradeRuleNodeConfigurationResult.getFirst()) {
ruleNode.setConfiguration(upgradeRuleNodeConfigurationResult.getSecond());
}
ruleNode.setConfigurationVersion(toVersion);
ruleChainService.saveRuleNode(TenantId.SYS_TENANT_ID, ruleNode);
log.debug("Successfully upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {}",
ruleNodeId, ruleNodeTypeForLogs, fromVersion, toVersion);
futures.add(jpaExecutorService.submit(() -> {
ruleChainService.saveRuleNode(TenantId.SYS_TENANT_ID, ruleNode);
log.debug("Successfully upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {}",
ruleNodeId, ruleNodeTypeForLogs, fromVersion, toVersion);
}));
if (futures.size() >= MAX_PENDING_SAVE_RULE_NODE_FUTURES) {
log.info("{} upgraded rule nodes so far ...",
totalRuleNodesUpgraded += awaitFuturesToCompleteAndGetCount(futures));
futures.clear();
}
} catch (Exception e) {
log.warn("Failed to upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {} due to: ",
ruleNodeId, ruleNodeTypeForLogs, fromVersion, toVersion, e);
}
}
}
});
log.info("Finished rule nodes upgrade!");
}
log.info("Finished rule nodes upgrade. Upgraded rule nodes count: {}",
totalRuleNodesUpgraded + awaitFuturesToCompleteAndGetCount(futures));
} catch (Exception e) {
log.error("Unexpected error during rule nodes upgrade: ", e);
}
}
private int awaitFuturesToCompleteAndGetCount(List<ListenableFuture<?>> futures) {
try {
return Futures.allAsList(futures).get().size();
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException("Failed to process save rule nodes requests due to: ", e);
}
}
private final PaginatedUpdater<String, DeviceProfileEntity> deviceProfileEntityDynamicConditionsUpdater =
new PaginatedUpdater<>() {

4
application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java

@ -20,8 +20,8 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.TbVersionedNode;
import org.thingsboard.rule.engine.flow.TbRuleChainInputNode;
import org.thingsboard.rule.engine.flow.TbRuleChainInputNodeConfiguration;
import org.thingsboard.rule.engine.flow.TbRuleChainOutputNode;
@ -402,7 +402,7 @@ public class DefaultTbRuleChainService extends AbstractTbEntityService implement
var ruleNodeClass = componentDiscoveryService.getRuleNodeInfo(ruleNodeType)
.orElseThrow(() -> new RuntimeException("Rule node " + ruleNodeType + " is not supported!"));
if (ruleNodeClass.isVersioned()) {
TbVersionedNode tbVersionedNode = (TbVersionedNode) ruleNodeClass.getClazz().getDeclaredConstructor().newInstance();
TbNode tbVersionedNode = (TbNode) ruleNodeClass.getClazz().getDeclaredConstructor().newInstance();
int fromVersion = node.getConfigurationVersion();
int toVersion = ruleNodeClass.getCurrentVersion();
if (fromVersion < toVersion) {

4
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java

@ -48,8 +48,8 @@ public abstract class BaseEntityExportService<I extends EntityId, E extends Expo
public abstract Set<EntityType> getSupportedEntityTypes();
protected void replaceUuidsRecursively(EntitiesExportCtx<?> ctx, JsonNode node, Set<String> skipFieldsSet, Pattern includedFieldsPattern) {
JacksonUtil.replaceUuidsRecursively(node, skipFieldsSet, includedFieldsPattern, uuid -> getExternalIdOrElseInternalByUuid(ctx, uuid));
protected void replaceUuidsRecursively(EntitiesExportCtx<?> ctx, JsonNode node, Set<String> skippedRootFields, Pattern includedFieldsPattern) {
JacksonUtil.replaceUuidsRecursively(node, skippedRootFields, includedFieldsPattern, uuid -> getExternalIdOrElseInternalByUuid(ctx, uuid), true);
}
protected Stream<UUID> toExternalIds(Collection<UUID> internalIds, Function<UUID, EntityId> entityIdCreator,

5
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java

@ -26,8 +26,11 @@ import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
import java.util.Collections;
import java.util.Set;
import static org.thingsboard.server.service.sync.ie.importing.impl.DashboardImportService.WIDGET_CONFIG_PROCESSED_FIELDS_PATTERN;
@Service
@TbCoreComponent
public class DashboardExportService extends BaseEntityExportService<DashboardId, Dashboard, EntityExportData<Dashboard>> {
@ -43,7 +46,7 @@ public class DashboardExportService extends BaseEntityExportService<DashboardId,
replaceUuidsRecursively(ctx, entityAlias, Set.of("id"), null);
}
for (JsonNode widgetConfig : dashboard.getWidgetsConfig()) {
replaceUuidsRecursively(ctx, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Set.of("id"), null);
replaceUuidsRecursively(ctx, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Collections.emptySet(), WIDGET_CONFIG_PROCESSED_FIELDS_PATTERN);
}
}

6
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java

@ -388,11 +388,11 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
}
protected void replaceIdsRecursively(EntitiesImportCtx ctx, IdProvider idProvider, JsonNode json,
Set<String> skipFieldsSet, Pattern includedFieldsPattern,
Set<String> skippedRootFields, Pattern includedFieldsPattern,
LinkedHashSet<EntityType> hints) {
JacksonUtil.replaceUuidsRecursively(json, skipFieldsSet, includedFieldsPattern,
JacksonUtil.replaceUuidsRecursively(json, skippedRootFields, includedFieldsPattern,
uuid -> idProvider.getInternalIdByUuid(uuid, ctx.isFinalImportAttempt(), hints)
.map(EntityId::getId).orElse(uuid));
.map(EntityId::getId).orElse(uuid), true);
}
}

4
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java

@ -36,6 +36,7 @@ import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@Service
@ -44,6 +45,7 @@ import java.util.stream.Collectors;
public class DashboardImportService extends BaseEntityImportService<DashboardId, Dashboard, EntityExportData<Dashboard>> {
private static final LinkedHashSet<EntityType> HINTS = new LinkedHashSet<>(Arrays.asList(EntityType.DASHBOARD, EntityType.DEVICE, EntityType.ASSET));
public static final Pattern WIDGET_CONFIG_PROCESSED_FIELDS_PATTERN = Pattern.compile(".*Id.*");
private final DashboardService dashboardService;
@ -68,7 +70,7 @@ public class DashboardImportService extends BaseEntityImportService<DashboardId,
replaceIdsRecursively(ctx, idProvider, entityAlias, Set.of("id"), null, HINTS);
}
for (JsonNode widgetConfig : dashboard.getWidgetsConfig()) {
replaceIdsRecursively(ctx, idProvider, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Set.of("id"), null, HINTS);
replaceIdsRecursively(ctx, idProvider, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Collections.emptySet(), WIDGET_CONFIG_PROCESSED_FIELDS_PATTERN, HINTS);
}
return dashboard;
}

2
application/src/main/resources/logback.xml

@ -30,6 +30,8 @@
<logger name="org.apache.kafka.clients" level="WARN"/>
<!-- To enable the logging of scanned rule engine components-->
<!-- <logger name="org.thingsboard.server.service.component.AnnotationComponentDiscoveryService" level="DEBUG" />-->
<!-- To enable the debug logging of rule node upgrade -->
<!-- <logger name="org.thingsboard.server.service.install.update.DefaultDataUpdateService" level="DEBUG"/>-->
<!-- Other useful logs -->
<!-- <logger name="org.springframework.jdbc.core" level="TRACE" />-->
<!-- <logger name="org.hibernate.SQL" level="DEBUG" />-->

13
application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java

@ -149,6 +149,19 @@ public class TelemetryControllerTest extends AbstractControllerTest {
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody, String.class, status().isBadRequest());
}
@Test
public void testEmptyKeyIsProhibited() throws Exception {
loginTenantAdmin();
Device device = createDevice();
String invalidRequestBody = "{\"\": \"value\"}";
doPostAsync("/api/plugins/telemetry/" + device.getId() + "/SHARED_SCOPE", invalidRequestBody, String.class, status().isBadRequest());
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody, String.class, status().isBadRequest());
String invalidRequestBody2 = "{\" \": \"value\"}";
doPostAsync("/api/plugins/telemetry/" + device.getId() + "/SHARED_SCOPE", invalidRequestBody2, String.class, status().isBadRequest());
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody2, String.class, status().isBadRequest());
}
private Device createDevice() throws Exception {
String testToken = "TEST_TOKEN";

3
application/src/test/java/org/thingsboard/server/edge/WidgetEdgeTest.java

@ -58,6 +58,7 @@ public class WidgetEdgeTest extends AbstractEdgeTest {
descriptor.put("key", "value");
widgetType.setDescriptor(descriptor);
widgetType.setDeprecated(true);
widgetType.setFqn("bundle_alias.type_alias");
WidgetType savedWidgetType = doPost("/api/widgetType", widgetType, WidgetType.class);
Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
@ -69,6 +70,8 @@ public class WidgetEdgeTest extends AbstractEdgeTest {
Assert.assertEquals(savedWidgetType.getFqn(), widgetTypeUpdateMsg.getFqn());
Assert.assertEquals(savedWidgetType.getName(), widgetTypeUpdateMsg.getName());
Assert.assertTrue(widgetTypeUpdateMsg.getDeprecated());
Assert.assertEquals("bundle_alias", widgetTypeUpdateMsg.getBundleAlias());
Assert.assertEquals("type_alias", widgetTypeUpdateMsg.getAlias());
Assert.assertEquals(JacksonUtil.toJsonNode(widgetTypeUpdateMsg.getDescriptorJson()), savedWidgetType.getDescriptor());
// update widget bundle

53
application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java

@ -257,6 +257,7 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
Asset asset1 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1");
Asset asset2 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 2");
Dashboard dashboard = createDashboard(tenantId1, null, "Dashboard 1");
Dashboard otherDashboard = createDashboard(tenantId1, null, "Dashboard 2");
DeviceProfile existingDeviceProfile = createDeviceProfile(tenantId2, null, null, "Existing");
String aliasId = "23c4185d-1497-9457-30b2-6d91e69a5b2c";
@ -265,20 +266,43 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
"\"" + aliasId + "\": {\n" +
"\"alias\": \"assets\",\n" +
"\"filter\": {\n" +
"\"entityList\": [\n" +
"\"" + asset1.getId().toString() + "\",\n" +
"\"" + asset2.getId().toString() + "\",\n" +
"\"" + tenantId1.getId().toString() + "\",\n" +
"\"" + existingDeviceProfile.getId().toString() + "\",\n" +
"\"" + unknownUuid + "\"\n" +
"],\n" +
"\"resolveMultiple\": true\n" +
" \"entityList\": [\n" +
" \"" + asset1.getId() + "\",\n" +
" \"" + asset2.getId() + "\",\n" +
" \"" + tenantId1.getId() + "\",\n" +
" \"" + existingDeviceProfile.getId() + "\",\n" +
" \"" + unknownUuid + "\"\n" +
" ],\n" +
" \"id\":\"" + asset1.getId() + "\",\n" +
" \"resolveMultiple\": true\n" +
"},\n" +
"\"id\": \"" + aliasId + "\"\n" +
"}\n" +
"}";
String widgetId = "ea8f34a0-264a-f11f-cde3-05201bb4ff4b";
String actionId = "4a8e6efa-3e68-fa59-7feb-d83366130cae";
String widgets = "{\n" +
" \"" + widgetId + "\": {\n" +
" \"config\": {\n" +
" \"actions\": {\n" +
" \"rowClick\": [\n" +
" {\n" +
" \"name\": \"go to dashboard\",\n" +
" \"targetDashboardId\": \"" + otherDashboard.getId() + "\",\n" +
" \"id\": \"" + actionId + "\"\n" +
" }\n" +
" ]\n" +
" }\n" +
" },\n" +
" \"row\": 0,\n" +
" \"col\": 0,\n" +
" \"id\": \"" + widgetId + "\"\n" +
" }\n" +
"}";
ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode();
dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases));
dashboardConfiguration.set("widgets", JacksonUtil.toJsonNode(widgets));
dashboardConfiguration.set("description", new TextNode("hallo"));
dashboard.setConfiguration(dashboardConfiguration);
dashboard = dashboardService.saveDashboard(dashboard);
@ -288,10 +312,12 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
EntityExportData<Asset> asset1ExportData = exportEntity(tenantAdmin1, asset1.getId());
EntityExportData<Asset> asset2ExportData = exportEntity(tenantAdmin1, asset2.getId());
EntityExportData<Dashboard> dashboardExportData = exportEntity(tenantAdmin1, dashboard.getId());
EntityExportData<Dashboard> otherDashboardExportData = exportEntity(tenantAdmin1, otherDashboard.getId());
AssetProfile importedProfile = importEntity(tenantAdmin2, profileExportData).getSavedEntity();
Asset importedAsset1 = importEntity(tenantAdmin2, asset1ExportData).getSavedEntity();
Asset importedAsset2 = importEntity(tenantAdmin2, asset2ExportData).getSavedEntity();
Dashboard importedOtherDashboard = importEntity(tenantAdmin2, otherDashboardExportData).getSavedEntity();
Dashboard importedDashboard = importEntity(tenantAdmin2, dashboardExportData).getSavedEntity();
Map.Entry<String, JsonNode> entityAlias = importedDashboard.getConfiguration().get("entityAliases").fields().next();
@ -311,6 +337,17 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
.isEqualTo(existingDeviceProfile.getId().toString());
assertThat(aliasEntitiesIds).element(4).as("unresolved uuid was replaced with tenant id")
.isEqualTo(tenantId2.toString());
assertThat(entityAlias.getValue().get("filter").get("id").asText()).as("external asset 1 was replaced with imported one")
.isEqualTo(importedAsset1.getId().toString());
ObjectNode widgetConfig = importedDashboard.getWidgetsConfig().get(0);
assertThat(widgetConfig.get("id").asText()).as("widget id is not replaced")
.isEqualTo(widgetId);
JsonNode actionConfig = widgetConfig.get("config").get("actions").get("rowClick").get(0);
assertThat(actionConfig.get("id").asText()).as("action id is not replaced")
.isEqualTo(actionId);
assertThat(actionConfig.get("targetDashboardId").asText()).as("dashboard id is replaced with imported one")
.isEqualTo(importedOtherDashboard.getId().toString());
}

25
build_proto.sh

@ -0,0 +1,25 @@
#!/bin/bash
#
# Copyright © 2016-2023 The Thingsboard Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
echo "Building ThingsBoard protobuf-containing packages..."
MAVEN_OPTS="-Xmx1024m" NODE_OPTIONS="--max_old_space_size=3072" \
mvn clean compile -T4 --also-make --projects='
common/cluster-api,
common/edge-api,
common/transport/coap,
common/transport/mqtt,
common/transport/transport-api'

2
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityService.java

@ -25,5 +25,7 @@ public interface DeviceConnectivityService {
JsonNode findDevicePublishTelemetryCommands(String baseUrl, Device device) throws URISyntaxException;
JsonNode findGatewayLaunchCommands(String baseUrl, Device device) throws URISyntaxException;
Resource getPemCertFile(String protocol);
}

1
common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java

@ -43,6 +43,7 @@ public class DataConstants {
public static final String RETRIES = "retries";
public static final String EDGE_ID = "edgeId";
public static final String DEVICE_ID = "deviceId";
public static final String GATEWAY_PARAMETER = "gateway";
public static final String COAP_TRANSPORT_NAME = "COAP";
public static final String LWM2M_TRANSPORT_NAME = "LWM2M";
public static final String MQTT_TRANSPORT_NAME = "MQTT";

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

@ -86,7 +86,7 @@ message ConnectResponseMsg {
}
message SyncRequestMsg {
bool syncRequired = 1; // deprecated
bool syncRequired = 1 [deprecated = true];
optional bool fullSync = 2;
}
@ -114,7 +114,7 @@ enum UpdateMsgType {
ENTITY_DELETED_RPC_MESSAGE = 2;
ALARM_ACK_RPC_MESSAGE = 3;
ALARM_CLEAR_RPC_MESSAGE = 4;
ENTITY_MERGE_RPC_MESSAGE = 5; // deprecated
ENTITY_MERGE_RPC_MESSAGE = 5 [deprecated = true];
}
message EntityDataProto {
@ -205,7 +205,7 @@ message DeviceUpdateMsg {
string type = 9;
optional string label = 10;
optional string additionalInfo = 11;
optional string conflictName = 12; // deprecated
optional string conflictName = 12 [deprecated = true];
optional int64 firmwareIdMSB = 13;
optional int64 firmwareIdLSB = 14;
optional bytes deviceDataBytes = 15;
@ -366,8 +366,8 @@ message WidgetTypeUpdateMsg {
UpdateMsgType msgType = 1;
int64 idMSB = 2;
int64 idLSB = 3;
optional string bundleAlias = 4; // deprecated
optional string alias = 5; // deprecated
optional string bundleAlias = 4 [deprecated = true];
optional string alias = 5 [deprecated = true];
optional string name = 6;
optional string descriptorJson = 7;
bool isSystem = 8;
@ -448,8 +448,8 @@ message DeviceCredentialsRequestMsg {
int64 deviceIdLSB = 2;
}
// deprecated
message DeviceProfileDevicesRequestMsg {
option deprecated = true;
int64 deviceProfileIdMSB = 1;
int64 deviceProfileIdLSB = 2;
}
@ -563,7 +563,7 @@ message UplinkMsg {
repeated UserCredentialsRequestMsg userCredentialsRequestMsg = 10;
repeated DeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 11;
repeated DeviceRpcCallMsg deviceRpcCallMsg = 12;
repeated DeviceProfileDevicesRequestMsg deviceProfileDevicesRequestMsg = 13; // deprecated
repeated DeviceProfileDevicesRequestMsg deviceProfileDevicesRequestMsg = 13 [deprecated = true];
repeated WidgetBundleTypesRequestMsg widgetBundleTypesRequestMsg = 14;
repeated EntityViewsRequestMsg entityViewsRequestMsg = 15;
repeated AssetUpdateMsg assetUpdateMsg = 16;

4
common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java

@ -150,7 +150,7 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
}
apiUsageReportClient.ifPresent(client -> client.report(tenantId, customerId, ApiUsageRecordKey.JS_EXEC_COUNT, 1));
pushedMsgs.incrementAndGet();
log.trace("InvokeScript uuid {} with timeout {}ms", scriptId, getMaxInvokeRequestsTimeout());
log.trace("[{}] InvokeScript uuid {} with timeout {}ms", tenantId, scriptId, getMaxInvokeRequestsTimeout());
var task = doInvokeFunction(scriptId, args);
var resultFuture = Futures.transformAsync(task.getResultFuture(), output -> {
@ -167,7 +167,7 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
} else {
String message = "Script invocation is blocked due to maximum error count "
+ getMaxErrors() + ", scriptId " + scriptId + "!";
log.warn(message);
log.warn("[{}] " + message, tenantId);
return error(message);
}
} else {

19
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DateFormattingOptions.java → common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DateTimeFormatOptions.java

@ -1,3 +1,18 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.script.api.tbel;
import lombok.Data;
@ -10,7 +25,7 @@ import java.util.TimeZone;
@NoArgsConstructor
@Data
class DateFormattingOptions {
class DateTimeFormatOptions {
private static final TimeZone DEFAULT_TZ = TimeZone.getDefault();
private String timeZone;
@ -19,7 +34,7 @@ class DateFormattingOptions {
@Getter
private String pattern;
public DateFormattingOptions(String timeZone) {
public DateTimeFormatOptions(String timeZone) {
this.timeZone = timeZone;
}

17
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java

@ -5,7 +5,7 @@
* 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
* 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,
@ -34,7 +34,6 @@ import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.function.BiFunction;
import java.util.function.Function;
public class TbDate extends Date {
@ -128,9 +127,9 @@ public class TbDate extends Date {
});
}
public String toLocaleString(String localeStr, String optionsStr, BiFunction<Locale, DateFormattingOptions, DateTimeFormatter> formatterBuilder) {
public String toLocaleString(String localeStr, String optionsStr, BiFunction<Locale, DateTimeFormatOptions, DateTimeFormatter> formatterBuilder) {
Locale locale = StringUtils.isNotEmpty(localeStr) ? Locale.forLanguageTag(localeStr) : Locale.getDefault();
DateFormattingOptions options = getDateFormattingOptions(optionsStr);
DateTimeFormatOptions options = getDateFormattingOptions(optionsStr);
ZonedDateTime zdt = this.toInstant().atZone(options.getTimeZone().toZoneId());
DateTimeFormatter formatter;
if (StringUtils.isNotEmpty(options.getPattern())) {
@ -141,17 +140,17 @@ public class TbDate extends Date {
return formatter.format(zdt);
}
private static DateFormattingOptions getDateFormattingOptions(String options) {
DateFormattingOptions opt = null;
private static DateTimeFormatOptions getDateFormattingOptions(String options) {
DateTimeFormatOptions opt = null;
if (StringUtils.isNotEmpty(options)) {
try {
opt = JacksonUtil.fromString(options, DateFormattingOptions.class);
opt = JacksonUtil.fromString(options, DateTimeFormatOptions.class);
} catch (IllegalArgumentException iae) {
opt = new DateFormattingOptions(options);
opt = new DateTimeFormatOptions(options);
}
}
if (opt == null) {
opt = new DateFormattingOptions();
opt = new DateTimeFormatOptions();
}
return opt;
}

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

@ -36,6 +36,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -63,6 +64,7 @@ public class TbUtils {
ExecutionContext.class, String.class)));
parserConfig.addImport("stringToBytes", new MethodStub(TbUtils.class.getMethod("stringToBytes",
ExecutionContext.class, String.class, String.class)));
parserConfig.registerNonConvertableMethods(TbUtils.class, Collections.singleton("stringToBytes"));
parserConfig.addImport("parseInt", new MethodStub(TbUtils.class.getMethod("parseInt",
String.class)));
parserConfig.addImport("parseInt", new MethodStub(TbUtils.class.getMethod("parseInt",

21
common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java

@ -28,6 +28,7 @@ import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.google.common.collect.Lists;
import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.common.data.kv.KvEntry;
@ -35,7 +36,6 @@ import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
@ -228,27 +228,24 @@ public class JacksonUtil {
return node;
}
public static void replaceUuidsRecursively(JsonNode node, Set<String> skipFieldsSet, Pattern includedFieldsPattern, UnaryOperator<UUID> replacer) {
public static void replaceUuidsRecursively(JsonNode node, Set<String> skippedRootFields, Pattern includedFieldsPattern, UnaryOperator<UUID> replacer, boolean root) {
if (node == null) {
return;
}
if (node.isObject()) {
ObjectNode objectNode = (ObjectNode) node;
List<String> fieldNames = new ArrayList<>(objectNode.size());
objectNode.fieldNames().forEachRemaining(fieldNames::add);
List<String> fieldNames = Lists.newArrayList(objectNode.fieldNames());
for (String fieldName : fieldNames) {
if (skipFieldsSet.contains(fieldName)) {
if (root && skippedRootFields.contains(fieldName)) {
continue;
}
if (includedFieldsPattern != null) {
if (!RegexUtils.matches(fieldName, includedFieldsPattern)) {
continue;
}
}
var child = objectNode.get(fieldName);
if (child.isObject() || child.isArray()) {
replaceUuidsRecursively(child, skipFieldsSet, includedFieldsPattern, replacer);
replaceUuidsRecursively(child, skippedRootFields, includedFieldsPattern, replacer, false);
} else if (child.isTextual()) {
if (includedFieldsPattern != null && !RegexUtils.matches(fieldName, includedFieldsPattern)) {
continue;
}
String text = child.asText();
String newText = RegexUtils.replace(text, RegexUtils.UUID_PATTERN, uuid -> replacer.apply(UUID.fromString(uuid)).toString());
if (!text.equals(newText)) {
@ -261,7 +258,7 @@ public class JacksonUtil {
for (int i = 0; i < array.size(); i++) {
JsonNode arrayElement = array.get(i);
if (arrayElement.isObject() || arrayElement.isArray()) {
replaceUuidsRecursively(arrayElement, skipFieldsSet, includedFieldsPattern, replacer);
replaceUuidsRecursively(arrayElement, skippedRootFields, includedFieldsPattern, replacer, false);
} else if (arrayElement.isTextual()) {
String text = arrayElement.asText();
String newText = RegexUtils.replace(text, RegexUtils.UUID_PATTERN, uuid -> replacer.apply(UUID.fromString(uuid)).toString());

35
dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java

@ -57,8 +57,10 @@ import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAPS;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.DOCKER;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTP;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTPS;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.LINUX;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTT;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTTS;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.WINDOWS;
@Service("DeviceConnectivityDaoService")
@Slf4j
@ -138,6 +140,26 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
return commands;
}
@Override
public JsonNode findGatewayLaunchCommands(String baseUrl, Device device) throws URISyntaxException {
DeviceId deviceId = device.getId();
log.trace("Executing findDevicePublishTelemetryCommands [{}]", deviceId);
validateId(deviceId, INCORRECT_DEVICE_ID + deviceId);
DeviceCredentials creds = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), deviceId);
ObjectNode commands = JacksonUtil.newObjectNode();
if (deviceConnectivityConfiguration.isEnabled(MQTT)) {
Optional.ofNullable(getGatewayDockerCommands(baseUrl, creds, MQTT))
.ifPresent(v -> commands.set(MQTT, v));
}
if (deviceConnectivityConfiguration.isEnabled(MQTTS)) {
Optional.ofNullable(getGatewayDockerCommands(baseUrl, creds, MQTTS))
.ifPresent(v -> commands.set(MQTTS, v));
}
return commands;
}
@Override
public Resource getPemCertFile(String protocol) {
return certs.computeIfAbsent(protocol, key -> {
@ -272,6 +294,18 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
return null;
}
private JsonNode getGatewayDockerCommands(String baseUrl, DeviceCredentials deviceCredentials, String mqttType) throws URISyntaxException {
ObjectNode dockerLaunchCommands = JacksonUtil.newObjectNode();
DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity().get(mqttType);
String mqttHost = getHost(baseUrl, properties);
String mqttPort = properties.getPort().isEmpty() ? null : properties.getPort();
Optional.ofNullable(DeviceConnectivityUtil.getGatewayLaunchCommand(LINUX, mqttHost, mqttPort, deviceCredentials))
.ifPresent(v -> dockerLaunchCommands.put(LINUX, v));
Optional.ofNullable(DeviceConnectivityUtil.getGatewayLaunchCommand(WINDOWS, mqttHost, mqttPort, deviceCredentials))
.ifPresent(v -> dockerLaunchCommands.put(WINDOWS, v));
return dockerLaunchCommands.isEmpty() ? null : dockerLaunchCommands;
}
private String getDockerMqttPublishCommand(String protocol, String baseUrl, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) throws URISyntaxException {
DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity(protocol);
String mqttHost = getHost(baseUrl, properties);
@ -329,4 +363,5 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
private String getHost(String baseUrl, DeviceConnectivityInfo properties) throws URISyntaxException {
return properties.getHost().isEmpty() ? new URI(baseUrl).getHost() : properties.getHost();
}
}

2
dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeDao.java

@ -30,7 +30,7 @@ import java.util.List;
*/
public interface RuleNodeDao extends Dao<RuleNode> {
List<RuleNode> findRuleNodesByTenantIdAndType(TenantId tenantId, String type, String search);
List<RuleNode> findRuleNodesByTenantIdAndType(TenantId tenantId, String type, String configurationSearch);
PageData<RuleNode> findAllRuleNodesByType(String type, PageLink pageLink);

4
dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java

@ -56,8 +56,8 @@ public class JpaRuleNodeDao extends JpaAbstractDao<RuleNodeEntity, RuleNode> imp
}
@Override
public List<RuleNode> findRuleNodesByTenantIdAndType(TenantId tenantId, String type, String search) {
return DaoUtil.convertDataList(ruleNodeRepository.findRuleNodesByTenantIdAndType(tenantId.getId(), type, search));
public List<RuleNode> findRuleNodesByTenantIdAndType(TenantId tenantId, String type, String configurationSearch) {
return DaoUtil.convertDataList(ruleNodeRepository.findRuleNodesByTenantIdAndType(tenantId.getId(), type, configurationSearch));
}
@Override

13
dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java

@ -29,19 +29,22 @@ import java.util.UUID;
public interface RuleNodeRepository extends JpaRepository<RuleNodeEntity, UUID> {
@Query("SELECT r FROM RuleNodeEntity r WHERE r.ruleChainId in " +
"(select id from RuleChainEntity rc WHERE rc.tenantId = :tenantId) " +
"AND r.type = :ruleType AND LOWER(r.configuration) LIKE LOWER(CONCAT('%', :searchText, '%')) ")
@Query(nativeQuery = true, value = "SELECT * FROM rule_node r WHERE r.rule_chain_id in " +
"(select id from rule_chain rc WHERE rc.tenant_id = :tenantId) AND r.type = :ruleType " +
" AND (:searchText IS NULL OR r.configuration ILIKE CONCAT('%', :searchText, '%'))")
List<RuleNodeEntity> findRuleNodesByTenantIdAndType(@Param("tenantId") UUID tenantId,
@Param("ruleType") String ruleType,
@Param("searchText") String searchText);
@Query("SELECT r FROM RuleNodeEntity r WHERE r.type = :ruleType AND LOWER(r.configuration) LIKE LOWER(CONCAT('%', :searchText, '%')) ")
@Query(nativeQuery = true, value = "SELECT * FROM rule_node r WHERE r.type = :ruleType " +
" AND (:searchText IS NULL OR r.configuration ILIKE CONCAT('%', :searchText, '%'))")
Page<RuleNodeEntity> findAllRuleNodesByType(@Param("ruleType") String ruleType,
@Param("searchText") String searchText,
Pageable pageable);
@Query("SELECT r FROM RuleNodeEntity r WHERE r.type = :ruleType AND r.configurationVersion < :version AND LOWER(r.configuration) LIKE LOWER(CONCAT('%', :searchText, '%')) ")
@Query(nativeQuery = true, value = "SELECT * FROM rule_node r WHERE r.type = :ruleType " +
" AND configuration_version < :version " +
" AND (:searchText IS NULL OR r.configuration ILIKE CONCAT('%', :searchText, '%'))")
Page<RuleNodeEntity> findAllRuleNodesByTypeAndVersionLessThan(@Param("ruleType") String ruleType,
@Param("version") int version,
@Param("searchText") String searchText,

49
dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.util;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentials;
@ -34,6 +35,7 @@ public class DeviceConnectivityUtil {
public static final String CHECK_DOCUMENTATION = "Check documentation";
public static final String JSON_EXAMPLE_PAYLOAD = "\"{temperature:25}\"";
public static final String DOCKER_RUN = "docker run --rm -it ";
public static final String GATEWAY_DOCKER_RUN = "docker run -it ";
public static final String MQTT_IMAGE = "thingsboard/mosquitto-clients ";
public static final String COAP_IMAGE = "thingsboard/coap-clients ";
@ -78,6 +80,53 @@ public class DeviceConnectivityUtil {
return command.toString();
}
public static String getGatewayLaunchCommand(String os, String host, String port, DeviceCredentials deviceCredentials) {
String gatewayVolumePathPrefix = "~/.tb-gateway";
if (WINDOWS.equals(os)) {
gatewayVolumePathPrefix = "%HOMEPATH%/tb-gateway";
}
String gatewayContainerName = "tbGateway" + StringUtils.capitalize(host.replace(".", ""));
StringBuilder command = new StringBuilder(GATEWAY_DOCKER_RUN);
command.append("-v {gatewayVolumePathPrefix}/logs:/thingsboard_gateway/logs".replace("{gatewayVolumePathPrefix}", gatewayVolumePathPrefix));
command.append(" -v {gatewayVolumePathPrefix}/extensions:/thingsboard_gateway/extensions".replace("{gatewayVolumePathPrefix}", gatewayVolumePathPrefix));
command.append(" -v {gatewayVolumePathPrefix}/config:/thingsboard_gateway/config".replace("{gatewayVolumePathPrefix}", gatewayVolumePathPrefix));
command.append(" --name ").append(gatewayContainerName);
command.append(" -e host=").append(host);
command.append(" -e port=").append(port);
switch(deviceCredentials.getCredentialsType()) {
case ACCESS_TOKEN:
command.append(" -e accessToken=").append(deviceCredentials.getCredentialsId());
break;
case MQTT_BASIC:
BasicMqttCredentials credentials = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(),
BasicMqttCredentials.class);
if (credentials != null) {
if (credentials.getClientId() != null) {
command.append(" -e clientId=").append(credentials.getClientId());
}
if (credentials.getUserName() != null) {
command.append(" -e username=").append(credentials.getUserName());
}
if (credentials.getPassword() != null) {
command.append(" -e password=").append(credentials.getPassword());
}
} else {
return null;
}
break;
default:
return null;
}
command.append(" --restart always");
command.append(" thingsboard/tb-gateway");
return command.toString();
}
public static String getDockerMqttPublishCommand(String protocol, String baseUrl, String host, String port, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) {
String mqttCommand = getMqttPublishCommand(protocol, host, port, deviceTelemetryTopic, deviceCredentials);

5
dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java

@ -18,6 +18,7 @@ package org.thingsboard.server.dao.util;
import com.fasterxml.jackson.databind.JsonNode;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
@ -48,8 +49,8 @@ public class KvUtils {
String key = tsKvEntry.getKey();
if (key == null) {
throw new DataValidationException("Key can't be null");
if (StringUtils.isBlank(key)) {
throw new DataValidationException("Key can't be null or empty");
}
if (key.length() > 255) {

158
dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDaoTest.java

@ -0,0 +1,158 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.sql.rule;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.google.common.util.concurrent.ListeningExecutorService;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.dao.AbstractJpaDaoTest;
import org.thingsboard.server.dao.rule.RuleChainDao;
import org.thingsboard.server.dao.rule.RuleNodeDao;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class JpaRuleNodeDaoTest extends AbstractJpaDaoTest {
public static final int COUNT = 40;
public static final String PREFIX_FOR_RULE_NODE_NAME = "SEARCH_TEXT_";
List<UUID> ruleNodeIds;
TenantId tenantId1;
TenantId tenantId2;
RuleChainId ruleChainId1;
RuleChainId ruleChainId2;
@Autowired
private RuleChainDao ruleChainDao;
@Autowired
private RuleNodeDao ruleNodeDao;
ListeningExecutorService executor;
@Before
public void setUp() {
tenantId1 = TenantId.fromUUID(Uuids.timeBased());
ruleChainId1 = new RuleChainId(UUID.randomUUID());
tenantId2 = TenantId.fromUUID(Uuids.timeBased());
ruleChainId2 = new RuleChainId(UUID.randomUUID());
ruleNodeIds = createRuleNodes(tenantId1, tenantId2, ruleChainId1, ruleChainId2, COUNT);
}
@After
public void tearDown() throws Exception {
ruleNodeDao.removeAllByIds(ruleNodeIds);
if (executor != null) {
executor.shutdownNow();
}
}
@Test
public void testSaveRuleName0x00_thenSomeDatabaseException() {
RuleNode ruleNode = getRuleNode(ruleChainId1, "T", "\u0000");
assertThatThrownBy(() -> ruleNodeIds.add(ruleNodeDao.save(tenantId1, ruleNode).getUuidId()));
}
@Test
public void testFindRuleNodesByTenantIdAndType() {
List<RuleNode> ruleNodes1 = ruleNodeDao.findRuleNodesByTenantIdAndType(tenantId1, "A", PREFIX_FOR_RULE_NODE_NAME);
assertEquals(20, ruleNodes1.size());
List<RuleNode> ruleNodes2 = ruleNodeDao.findRuleNodesByTenantIdAndType(tenantId2, "B", PREFIX_FOR_RULE_NODE_NAME);
assertEquals(20, ruleNodes2.size());
ruleNodes1 = ruleNodeDao.findRuleNodesByTenantIdAndType(tenantId1, "A", null);
assertEquals(20, ruleNodes1.size());
ruleNodes2 = ruleNodeDao.findRuleNodesByTenantIdAndType(tenantId2, "B", null);
assertEquals(20, ruleNodes2.size());
}
@Test
public void testFindRuleNodesByType() {
PageData<RuleNode> ruleNodes = ruleNodeDao.findAllRuleNodesByType( "A", new PageLink(10, 0, PREFIX_FOR_RULE_NODE_NAME));
assertEquals(20, ruleNodes.getTotalElements());
assertEquals(2, ruleNodes.getTotalPages());
assertEquals(10, ruleNodes.getData().size());
ruleNodes = ruleNodeDao.findAllRuleNodesByType( "A", new PageLink(10, 0));
assertEquals(20, ruleNodes.getTotalElements());
assertEquals(2, ruleNodes.getTotalPages());
assertEquals(10, ruleNodes.getData().size());
}
@Test
public void testFindRuleNodesByTypeAndVersionLessThan() {
PageData<RuleNode> ruleNodes = ruleNodeDao.findAllRuleNodesByTypeAndVersionLessThan( "A", 1, new PageLink(10, 0, PREFIX_FOR_RULE_NODE_NAME));
assertEquals(20, ruleNodes.getTotalElements());
assertEquals(2, ruleNodes.getTotalPages());
assertEquals(10, ruleNodes.getData().size());
ruleNodes = ruleNodeDao.findAllRuleNodesByTypeAndVersionLessThan( "A", 1, new PageLink(10, 0));
assertEquals(20, ruleNodes.getTotalElements());
assertEquals(2, ruleNodes.getTotalPages());
assertEquals(10, ruleNodes.getData().size());
}
private List<UUID> createRuleNodes(TenantId tenantId1, TenantId tenantId2, RuleChainId ruleChainId1, RuleChainId ruleChainId2, int count) {
var chain1 = new RuleChain(ruleChainId1);
chain1.setTenantId(tenantId1);
chain1.setName(ruleChainId1.toString());
ruleChainDao.save(tenantId1, chain1);
var chain2 = new RuleChain(ruleChainId2);
chain2.setTenantId(tenantId2);
chain2.setName(ruleChainId2.toString());
ruleChainDao.save(tenantId2, chain2);
List<UUID> savedRuleNodeIds = new ArrayList<>();
for (int i = 0; i < count / 2; i++) {
savedRuleNodeIds.add(ruleNodeDao.save(tenantId1, getRuleNode(ruleChainId1, "A", Integer.toString(i))).getUuidId());
savedRuleNodeIds.add(ruleNodeDao.save(tenantId2, getRuleNode(ruleChainId2, "B", Integer.toString(i + count / 2))).getUuidId());
}
return savedRuleNodeIds;
}
private RuleNode getRuleNode(RuleChainId ruleChainId, String type, String nameSuffix) {
return getRuleNode(ruleChainId, Uuids.timeBased(), type, nameSuffix);
}
private RuleNode getRuleNode(RuleChainId ruleChainId, UUID ruleNodeId, String type, String nameSuffix) {
RuleNode ruleNode = new RuleNode();
ruleNode.setId(new RuleNodeId(ruleNodeId));
ruleNode.setRuleChainId(ruleChainId);
ruleNode.setName(nameSuffix);
ruleNode.setType(type);
ruleNode.setConfiguration(JacksonUtil.newObjectNode().put("searchHint", PREFIX_FOR_RULE_NODE_NAME + nameSuffix));
ruleNode.setConfigurationVersion(0);
return ruleNode;
}
}

2
pom.xml

@ -78,7 +78,7 @@
<zookeeper.version>3.8.1</zookeeper.version>
<protobuf.version>3.21.9</protobuf.version>
<grpc.version>1.42.1</grpc.version>
<tbel.version>1.1.0</tbel.version>
<tbel.version>1.1.1</tbel.version>
<lombok.version>1.18.18</lombok.version>
<paho.client.version>1.2.4</paho.client.version>
<paho.mqttv5.client.version>1.2.5</paho.mqttv5.client.version>

16
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNode.java

@ -15,6 +15,8 @@
*/
package org.thingsboard.rule.engine.api;
import com.fasterxml.jackson.databind.JsonNode;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
@ -35,4 +37,18 @@ public interface TbNode {
default void onPartitionChangeMsg(TbContext ctx, PartitionChangeMsg msg) {
}
/**
* Upgrades the configuration from a specific version to the current version specified in the
* {@link RuleNode} annotation for the instance of {@link TbNode}.
*
* @param fromVersion The version from which the configuration needs to be upgraded.
* @param oldConfiguration The old configuration to be upgraded.
* @return A pair consisting of a Boolean flag indicating the success of the upgrade
* and a JsonNode representing the upgraded configuration.
* @throws TbNodeException If an error occurs during the upgrade process.
*/
default TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
return new TbPair<>(false, oldConfiguration);
}
}

35
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbVersionedNode.java

@ -1,35 +0,0 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.api;
import com.fasterxml.jackson.databind.JsonNode;
import org.thingsboard.server.common.data.util.TbPair;
public interface TbVersionedNode extends TbNode {
/**
* Upgrades the configuration from a specific version to the current version specified in the
* {@link RuleNode} annotation for the instance of {@link TbVersionedNode}.
*
* @param fromVersion The version from which the configuration needs to be upgraded.
* @param oldConfiguration The old configuration to be upgraded.
* @return A pair consisting of a Boolean flag indicating the success of the upgrade
* and a JsonNode representing the upgraded configuration.
* @throws TbNodeException If an error occurs during the upgrade process.
*/
TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException;
}

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java

@ -22,9 +22,9 @@ import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.TbVersionedNode;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.EntityId;
@ -58,7 +58,7 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
"Output connections: <code>True</code>, <code>False</code>, <code>Failure</code>",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbFilterNodeCheckRelationConfig")
public class TbCheckRelationNode implements TbVersionedNode {
public class TbCheckRelationNode implements TbNode {
private static final String DIRECTION_PROPERTY_NAME = "direction";

30
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java

@ -84,7 +84,7 @@ import static org.thingsboard.rule.engine.math.TbMathArgumentType.CONSTANT;
)
public class TbMathNode implements TbNode {
private static final ConcurrentMap<EntityId, SemaphoreWithQueue<TbMsgTbContext>> locks = new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK);
private static final ConcurrentMap<EntityId, SemaphoreWithQueue<TbMsgTbContextBiFunction>> locks = new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK);
private final ThreadLocal<Expression> customExpression = new ThreadLocal<>();
private TbMathNodeConfiguration config;
private boolean msgBodyToJsonConversionRequired;
@ -111,21 +111,21 @@ public class TbMathNode implements TbNode {
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
var semaphoreWithQueue = locks.computeIfAbsent(msg.getOriginator(), SemaphoreWithQueue::new);
semaphoreWithQueue.getQueue().add(new TbMsgTbContext(msg, ctx));
semaphoreWithQueue.getQueue().add(new TbMsgTbContextBiFunction(msg, ctx, this::processMsgAsync));
tryProcessQueue(semaphoreWithQueue);
}
void tryProcessQueue(SemaphoreWithQueue<TbMsgTbContext> lockAndQueue) {
void tryProcessQueue(SemaphoreWithQueue<TbMsgTbContextBiFunction> lockAndQueue) {
final Semaphore semaphore = lockAndQueue.getSemaphore();
final Queue<TbMsgTbContext> queue = lockAndQueue.getQueue();
final Queue<TbMsgTbContextBiFunction> queue = lockAndQueue.getQueue();
while (!queue.isEmpty()) {
// The semaphore have to be acquired before EACH poll and released before NEXT poll.
// Otherwise, some message will remain unprocessed in queue
if (!semaphore.tryAcquire()) {
return;
}
TbMsgTbContext tbMsgTbContext = null;
TbMsgTbContextBiFunction tbMsgTbContext = null;
try {
tbMsgTbContext = queue.poll();
if (tbMsgTbContext == null) {
@ -140,7 +140,7 @@ public class TbMathNode implements TbNode {
}
//DO PROCESSING
final TbContext ctx = tbMsgTbContext.getCtx();
final ListenableFuture<TbMsg> resultMsgFuture = processMsgAsync(ctx, msg);
final ListenableFuture<TbMsg> resultMsgFuture = tbMsgTbContext.getBiFunction().apply(ctx, msg);
DonAsynchron.withCallback(resultMsgFuture, resultMsg -> {
try {
ctx.tellSuccess(resultMsg);
@ -156,10 +156,17 @@ public class TbMathNode implements TbNode {
tryProcessQueue(lockAndQueue);
}
}, ctx.getDbCallbackExecutor());
} catch (Throwable e) {
} catch (Throwable t) {
semaphore.release();
log.warn("[{}] Failed to process message: {}", lockAndQueue.getEntityId(), tbMsgTbContext == null ? null : tbMsgTbContext.getMsg(), e);
throw e;
if (tbMsgTbContext == null) { // if no message polled, the loop become infinite, will throw exception
log.error("[{}] Failed to process TbMsgTbContext queue", lockAndQueue.getEntityId(), t);
throw t;
}
TbMsg msg = tbMsgTbContext.getMsg();
TbContext ctx = tbMsgTbContext.getCtx();
log.warn("[{}] Failed to process message: {}", lockAndQueue.getEntityId(), msg, t);
ctx.tellFailure(msg, t); // you are not allowed to throw here, because queue will remain unprocessed
continue; // We are probably the last who process the queue. We have to continue poll until get successful callback or queue is empty
}
break; //submitted async exact one task. next poll will try on callback
}
@ -367,7 +374,7 @@ public class TbMathNode implements TbNode {
return function.apply(arg1.getValue(), arg2.getValue());
}
private ListenableFuture<TbMathArgumentValue> resolveArguments(TbContext ctx, TbMsg msg, Optional<ObjectNode> msgBodyOpt, TbMathArgument arg) {
ListenableFuture<TbMathArgumentValue> resolveArguments(TbContext ctx, TbMsg msg, Optional<ObjectNode> msgBodyOpt, TbMathArgument arg) {
String argKey = getKeyFromTemplate(msg, arg.getType(), arg.getKey());
switch (arg.getType()) {
case CONSTANT:
@ -433,9 +440,10 @@ public class TbMathNode implements TbNode {
@Data
@RequiredArgsConstructor
static public class TbMsgTbContext {
static public class TbMsgTbContextBiFunction {
final TbMsg msg;
final TbContext ctx;
final BiFunction<TbContext, TbMsg, ListenableFuture<TbMsg>> biFunction;
}
}

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractNodeWithFetchTo.java

@ -22,9 +22,9 @@ import com.google.common.util.concurrent.Futures;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.TbVersionedNode;
import org.thingsboard.rule.engine.util.TbMsgSource;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.KvEntry;
@ -35,7 +35,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.util.NoSuchElementException;
@Slf4j
public abstract class TbAbstractNodeWithFetchTo<C extends TbAbstractFetchToNodeConfiguration> implements TbVersionedNode {
public abstract class TbAbstractNodeWithFetchTo<C extends TbAbstractFetchToNodeConfiguration> implements TbNode {
protected final static String FETCH_TO_PROPERTY_NAME = "fetchTo";

8
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java

@ -24,9 +24,9 @@ import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.TbVersionedNode;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
@ -43,9 +43,7 @@ import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.DataConstants.CLIENT_SCOPE;
import static org.thingsboard.server.common.data.DataConstants.NOTIFY_DEVICE_METADATA_KEY;
import static org.thingsboard.server.common.data.DataConstants.SCOPE;
import static org.thingsboard.server.common.data.DataConstants.*;
import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST;
@Slf4j
@ -64,7 +62,7 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_R
configDirective = "tbActionNodeAttributesConfig",
icon = "file_upload"
)
public class TbMsgAttributesNode implements TbVersionedNode {
public class TbMsgAttributesNode implements TbNode {
static final String UPDATE_ATTRIBUTES_ONLY_ON_VALUE_CHANGE_KEY = "updateAttributesOnlyOnValueChange";
private TbMsgAttributesNodeConfiguration config;

150
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java

@ -18,6 +18,9 @@ package org.thingsboard.rule.engine.math;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Triple;
import org.assertj.core.api.SoftAssertions;
import org.junit.Assert;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -27,7 +30,9 @@ import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.verification.Timeout;
import org.thingsboard.common.util.AbstractListeningExecutor;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
@ -72,6 +77,10 @@ import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.timeout;
@ -128,7 +137,15 @@ public class TbMathNodeTest {
return initNode(TbRuleNodeMathFunctionType.CUSTOM, expression, result, arguments);
}
private TbMathNode initNodeWithCustomFunction(TbContext ctx, String expression, TbMathResult result, TbMathArgument... arguments) {
return initNode(ctx, TbRuleNodeMathFunctionType.CUSTOM, expression, result, arguments);
}
private TbMathNode initNode(TbRuleNodeMathFunctionType operation, String expression, TbMathResult result, TbMathArgument... arguments) {
return initNode(this.ctx, operation, expression, result, arguments);
}
private TbMathNode initNode(TbContext ctx, TbRuleNodeMathFunctionType operation, String expression, TbMathResult result, TbMathArgument... arguments) {
try {
TbMathNodeConfiguration configuration = new TbMathNodeConfiguration();
configuration.setOperation(operation);
@ -521,10 +538,11 @@ public class TbMathNodeTest {
new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "TestKey")
);
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 10).toString());
Throwable thrown = assertThrows(RuntimeException.class, () -> {
node.onMsg(ctx, msg);
});
assertNotNull(thrown.getMessage());
node.onMsg(ctx, msg);
ArgumentCaptor<Throwable> tCaptor = ArgumentCaptor.forClass(Throwable.class);
Mockito.verify(ctx, Mockito.timeout(5000)).tellFailure(eq(msg), tCaptor.capture());
Assert.assertNotNull(tCaptor.getValue().getMessage());
}
@Test
@ -534,11 +552,12 @@ public class TbMathNodeTest {
new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a")
);
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY);
Throwable thrown = assertThrows(RuntimeException.class, () -> {
node.onMsg(ctx, msg);
});
assertNotNull(thrown.getMessage());
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY);
node.onMsg(ctx, msg);
ArgumentCaptor<Throwable> tCaptor = ArgumentCaptor.forClass(Throwable.class);
Mockito.verify(ctx, Mockito.timeout(5000)).tellFailure(eq(msg), tCaptor.capture());
Assert.assertNotNull(tCaptor.getValue().getMessage());
}
@Test
@ -553,10 +572,10 @@ public class TbMathNodeTest {
CountDownLatch slowProcessingLatch = new CountDownLatch(1);
List<TbMsg> slowMsgList = IntStream.range(0, 5)
.mapToObj(x -> TbMsg.newMsg("TEST", originatorSlow, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString()))
.mapToObj(x -> TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originatorSlow, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString()))
.collect(Collectors.toList());
List<TbMsg> fastMsgList = IntStream.range(0, 2)
.mapToObj(x -> TbMsg.newMsg("TEST", originatorFast, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString()))
.mapToObj(x -> TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originatorFast, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString()))
.collect(Collectors.toList());
assertThat(slowMsgList.size()).as("slow msgs >= rule-dispatcher pool size").isGreaterThanOrEqualTo(RULE_DISPATCHER_POOL_SIZE);
@ -609,6 +628,115 @@ public class TbMathNodeTest {
verify(ctx, never()).tellFailure(any(), any());
}
@Test
public void testExp4j_concurrentBySingleOriginator_processMsgAsyncException() {
TbMathNode node = spy(initNodeWithCustomFunction("2a+3b",
new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "result", 2, false, false, null),
new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a"),
new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "b")
));
willThrow(new RuntimeException("Message body has no 'delta'")).given(node).resolveArguments(any(), any(), any(), any());
EntityId originatorSlow = DeviceId.fromString("7f01170d-6bba-419c-b95c-2b4c3ba32f30");
CountDownLatch slowProcessingLatch = new CountDownLatch(1);
List<TbMsg> slowMsgList = IntStream.range(0, 5)
.mapToObj(x -> TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originatorSlow, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString()))
.collect(Collectors.toList());
assertThat(slowMsgList.size()).as("slow msgs >= rule-dispatcher pool size").isGreaterThanOrEqualTo(RULE_DISPATCHER_POOL_SIZE);
log.debug("rule-dispatcher [{}], db-callback [{}], slowMsg [{}]", RULE_DISPATCHER_POOL_SIZE, DB_CALLBACK_POOL_SIZE, slowMsgList.size());
willAnswer(invocation -> {
TbMsg msg = invocation.getArgument(1);
if (slowProcessingLatch.getCount() > 0) {
log.debug("Await on slowProcessingLatch before processMsgAsync");
try {
assertThat(slowProcessingLatch.await(30, TimeUnit.SECONDS)).as("await on slowProcessingLatch").isTrue();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("\uD83D\uDC0C processMsgAsync with exception [{}][{}]", msg.getOriginator(), msg);
return invocation.callRealMethod();
}).given(node).processMsgAsync(eq(ctx), argThat(slowMsgList::contains));
willAnswer(invocation -> {
TbMsg msg = invocation.getArgument(1);
log.debug("submit slow originator onMsg [{}][{}]", msg.getOriginator(), msg);
return invocation.callRealMethod();
}).given(node).onMsg(eq(ctx), argThat(slowMsgList::contains));
// submit slow msg may block all rule engine dispatcher threads
slowMsgList.forEach(msg -> ruleEngineDispatcherExecutor.executeAsync(() -> node.onMsg(ctx, msg)));
// wait until dispatcher threads started with all slowMsg
verify(node, new Timeout(TimeUnit.SECONDS.toMillis(5), times(slowMsgList.size()))).onMsg(eq(ctx), argThat(slowMsgList::contains));
slowProcessingLatch.countDown();
verify(ctx, new Timeout(TimeUnit.SECONDS.toMillis(5), times(slowMsgList.size()))).tellFailure(any(), any());
verify(ctx, never()).tellSuccess(any());
}
@Test
public void testExp4j_concurrentBySingleOriginator_SingleMsg_manyNodesWithDifferentOutput() {
assertThat(RULE_DISPATCHER_POOL_SIZE).as("dispatcher pool size have to be > 1").isGreaterThan(1);
CountDownLatch processingLatch = new CountDownLatch(1);
List<Triple<TbContext, String, TbMathNode>> ctxNodes = IntStream.range(0, RULE_DISPATCHER_POOL_SIZE * 2)
.mapToObj(x -> {
final TbContext ctx = mock(TbContext.class); // many rule nodes - many contexts
willReturn(dbCallbackExecutor).given(ctx).getDbCallbackExecutor();
final String resultKey = "result" + x;
final TbMathNode node = spy(initNodeWithCustomFunction(ctx, "2a+3b",
new TbMathResult(TbMathArgumentType.MESSAGE_METADATA, resultKey, 1, false, true, null),
new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a"),
new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "b")));
willAnswer(invocation -> {
if (processingLatch.getCount() > 0) {
log.debug("Await on processingLatch before processMsgAsync");
try {
assertThat(processingLatch.await(30, TimeUnit.SECONDS)).as("await on processingLatch").isTrue();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("\uD83D\uDC0C processMsgAsync on node with expected resultKey [{}]", resultKey);
return invocation.callRealMethod();
}).given(node).processMsgAsync(any(), any());
willAnswer(invocation -> {
TbMsg msg = invocation.getArgument(1);
log.debug("submit originator onMsg [{}][{}]", msg.getOriginator(), msg);
return invocation.callRealMethod();
}).given(node).onMsg(any(), any());
return Triple.of(ctx, resultKey, node);
})
.collect(Collectors.toList());
ctxNodes.forEach(ctxNode -> ruleEngineDispatcherExecutor.executeAsync(() -> ctxNode.getRight()
.onMsg(ctxNode.getLeft(), TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, "{\"a\":2,\"b\":2}"))));
ctxNodes.forEach(ctxNode -> verify(ctxNode.getRight(), timeout(5000)).onMsg(eq(ctxNode.getLeft()), any()));
processingLatch.countDown();
SoftAssertions softly = new SoftAssertions();
ctxNodes.forEach(ctxNode -> {
final TbContext ctx = ctxNode.getLeft();
final String resultKey = ctxNode.getMiddle();
ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctx, timeout(5000)).tellSuccess(msgCaptor.capture());
TbMsg resultMsg = msgCaptor.getValue();
assertThat(resultMsg).as("result msg non null for result key " + resultKey).isNotNull();
log.debug("asserting result key [{}] in metadata [{}]", resultKey, resultMsg.getMetaData().getData());
softly.assertThat(resultMsg.getMetaData().getValue(resultKey)).as("asserting result key " + resultKey)
.isEqualTo("10.0");
});
softly.assertAll();
verify(ctx, never()).tellFailure(any(), any());
}
static class RuleDispatcherExecutor extends AbstractListeningExecutor {
@Override
protected int getThreadPollSize() {

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

@ -27,7 +27,7 @@ import {
DeviceCredentials,
DeviceInfo,
DeviceInfoQuery,
DeviceSearchQuery,
DeviceSearchQuery, PublishLaunchCommand,
PublishTelemetryCommand
} from '@app/shared/models/device.models';
import { EntitySubtype } from '@app/shared/models/entity-type.models';
@ -214,4 +214,8 @@ export class DeviceService {
return this.http.get<PublishTelemetryCommand>(`/api/device-connectivity/${deviceId}`, defaultHttpOptionsFromConfig(config));
}
public getDevicePublishLaunchCommands(deviceId: string, config?: RequestConfig): Observable<PublishLaunchCommand> {
return this.http.get<PublishLaunchCommand>(`/api/device-connectivity/gateway-launch/${deviceId}`, defaultHttpOptionsFromConfig(config));
}
}

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

@ -202,6 +202,13 @@ export function base64toObj(b64Encoded: string): any {
return JSON.parse(json);
}
export function stringToBase64(value: string): string {
return btoa(encodeURIComponent(value).replace(/%([0-9A-F]{2})/g,
function toSolidBytes(match, p1) {
return String.fromCharCode(Number('0x' + p1));
}));
}
const scrollRegex = /(auto|scroll)/;
function parentNodes(node: Node, nodes: Node[]): Node[] {

8
ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html

@ -17,7 +17,7 @@
-->
<form [formGroup]="attributeFormGroup" (ngSubmit)="add()" style="min-width: 400px;">
<mat-toolbar color="primary">
<h2>{{ title | translate }}</h2>
<h2>{{ (isTelemetry ? 'attribute.add-telemetry' : 'attribute.add') | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
@ -34,7 +34,7 @@
<mat-label translate>attribute.key</mat-label>
<input matInput formControlName="key" required>
<mat-error *ngIf="attributeFormGroup.get('key').hasError('required')">
{{ 'attribute.key-required' | translate }}
{{ (isTelemetry ? 'attribute.telemetry-key-required' : 'attribute.key-required') | translate }}
</mat-error>
<mat-error *ngIf="attributeFormGroup.get('key').hasError('maxlength')">
{{ 'attribute.key-max-length' | translate }}
@ -42,7 +42,7 @@
</mat-form-field>
<tb-value-input
formControlName="value"
requiredText="attribute.value-required">
[requiredText]="isTelemetry ? 'attribute.telemetry-value-required' : 'attribute.value-required'">
</tb-value-input>
</fieldset>
</div>
@ -55,7 +55,7 @@
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || attributeFormGroup.invalid || !attributeFormGroup.dirty">
[disabled]="(isLoading$ | async) || invalid() || !attributeFormGroup.dirty">
{{ 'action.add' | translate }}
</button>
</div>

10
ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts

@ -45,7 +45,7 @@ export class AddAttributeDialogComponent extends DialogComponent<AddAttributeDia
submitted = false;
title = '';
isTelemetry = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@ -62,8 +62,7 @@ export class AddAttributeDialogComponent extends DialogComponent<AddAttributeDia
key: ['', [Validators.required, Validators.maxLength(255)]],
value: [null, [Validators.required]]
});
this.title = this.data.attributeScope === LatestTelemetry.LATEST_TELEMETRY ?
'attribute.add-telemetry' : 'attribute.add'
this.isTelemetry = this.data.attributeScope === LatestTelemetry.LATEST_TELEMETRY;
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
@ -72,6 +71,11 @@ export class AddAttributeDialogComponent extends DialogComponent<AddAttributeDia
return originalErrorState || customErrorState;
}
invalid(): boolean {
const value = this.attributeFormGroup.get('value').value;
return !Array.isArray(value) && this.attributeFormGroup.invalid;
}
cancel(): void {
this.dialogRef.close(false);
}

2
ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.html

@ -33,7 +33,7 @@
</button>
<button mat-button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || attributeFormGroup.invalid || !attributeFormGroup.dirty">
[disabled]="(isLoading$ | async) || invalid() || !attributeFormGroup.dirty">
{{ 'action.update' | translate }}
</button>
</div>

5
ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.ts

@ -62,6 +62,11 @@ export class EditAttributeValuePanelComponent extends PageComponent implements O
return originalErrorState || customErrorState;
}
invalid(): boolean {
const value = this.attributeFormGroup.get('value').value;
return !Array.isArray(value) && this.attributeFormGroup.invalid;
}
cancel(): void {
this.overlayRef.dispose();
}

10
ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-config.ts

@ -107,9 +107,15 @@ export class EdgeDownlinkTableConfig extends EntityTableConfig<EdgeEvent, TimePa
this.columns.push(
new DateEntityTableColumn<EdgeEvent>('createdTime', 'event.event-time', this.datePipe, '120px'),
new EntityTableColumn<EdgeEvent>('type', 'event.type', '25%',
entity => this.translate.instant(edgeEventTypeTranslations.get(entity.type)), entity => ({}), false),
entity => {
let key = edgeEventTypeTranslations.get(entity.type);
return key ? this.translate.instant(key) : entity.type;
}, entity => ({}), false),
new EntityTableColumn<EdgeEvent>('action', 'edge.event-action', '25%',
entity => this.translate.instant(edgeEventActionTypeTranslations.get(entity.action)), entity => ({}), false),
entity => {
let key = edgeEventActionTypeTranslations.get(entity.action);
return key ? this.translate.instant(key) : entity.action;
}, entity => ({}), false),
new EntityTableColumn<EdgeEvent>('entityId', 'edge.entity-id', '40%',
(entity) => entity.entityId ? entity.entityId : '', () => ({}), false),
new EntityTableColumn<EdgeEvent>('status', 'event.status', '10%',

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

@ -0,0 +1,75 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div mat-dialog-content tb-toast fxLayout="column" toastTarget="dockerCommandDialogContent" style="padding-top: 5px">
<div fxLayout="row" fxLayoutAlign="space-between center">
<span class="tb-no-data-text">{{ 'gateway.docker-label' | translate }}</span>
<div fxFlexAlign="end" class="tb-help" [tb-help]="helpLink"></div>
</div>
<div fxFlex fxLayout="row" style="justify-content: space-between; flex-grow: 0; align-items: center">
</div>
<mat-tab-group [(selectedIndex)]="tabIndex">
<mat-tab>
<ng-template mat-tab-label>
<mat-icon class="tabs-icon" svgIcon="windows"></mat-icon>
Windows
</ng-template>
<ng-template matTabContent>
<div class="tb-form-panel no-padding no-border tb-tab-body">
<tb-markdown usePlainMarkdown containerClass="start-code" data="
```bash
{{windowsCode}}
{:copy-code}
```
"></tb-markdown>
</div>
</ng-template>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<mat-icon class="tabs-icon" svgIcon="linux"></mat-icon>
Linux
</ng-template>
<ng-template matTabContent>
<div class="tb-form-panel no-padding no-border tb-tab-body">
<tb-markdown usePlainMarkdown containerClass="start-code" data="
```bash
{{linuxCode}}
{:copy-code}
```
"></tb-markdown>
</div>
</ng-template>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<mat-icon class="tabs-icon" svgIcon="macos"></mat-icon>
MacOS
</ng-template>
<ng-template matTabContent>
<div class="tb-form-panel no-padding no-border tb-tab-body">
<tb-markdown usePlainMarkdown containerClass="start-code" data="
```bash
{{linuxCode}}
{:copy-code}
```
"></tb-markdown>
</div>
</ng-template>
</mat-tab>
</mat-tab-group>
</div>

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

@ -0,0 +1,37 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
width: 100%;
height: 100%;
display: block;
}
:host ::ng-deep {
.tb-markdown-view {
.start-code {
code[class*="language-"], pre[class*="language-"] {
overflow: hidden;
white-space: break-spaces;
word-break: break-all;
}
.code-wrapper {
padding: 0;
}
}
}
}

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

@ -0,0 +1,98 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { DeviceService } from '@core/http/device.service';
import { helpBaseUrl } from '@shared/models/constants';
import { getOS } from '@core/utils';
@Component({
selector: 'tb-gateway-command',
templateUrl: './device-gateway-command.component.html',
styleUrls: ['./device-gateway-command.component.scss']
})
export class DeviceGatewayCommandComponent implements OnInit {
@Input()
token: string;
@Input()
deviceId: string;
linuxCode: string;
windowsCode: string;
helpLink: string = helpBaseUrl + '/docs/iot-gateway/install/docker-installation/';
tabIndex = 0;
constructor(protected router: Router,
protected store: Store<AppState>,
private translate: TranslateService,
private cd: ChangeDetectorRef,
private deviceService: DeviceService) {
}
ngOnInit(): void {
if (this.deviceId) {
this.deviceService.getDevicePublishLaunchCommands(this.deviceId).subscribe(commands => {
this.createRunCode(commands.mqtt);
this.cd.detectChanges();
});
}
const currentOS = getOS();
switch (currentOS) {
case 'linux':
case 'android':
this.tabIndex = 1;
break;
case 'macos':
case 'ios':
this.tabIndex = 2;
break;
case 'windows':
this.tabIndex = 0;
break;
default:
this.tabIndex = 1;
}
}
createRunCode(commands) {
this.linuxCode = commands.linux;
this.windowsCode = commands.windows;
}
onDockerCodeCopied() {
this.store.dispatch(new ActionNotificationShow(
{
message: this.translate.instant('gateway.command-copied-message'),
type: 'success',
target: 'dockerCommandDialogContent',
duration: 1200,
verticalPosition: 'bottom',
horizontalPosition: 'left'
}));
}
}

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

@ -0,0 +1,833 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form [formGroup]="gatewayConfigGroup" style="height: 100%" fxLayout="column">
<mat-toolbar color="primary">
<h2 translate>gateway.gateway-configuration</h2>
<span fxFlex></span>
<button mat-icon-button
type="button"
(click)="cancel()"
*ngIf="dialogRef">
<mat-icon class="material-icons" style="color:#000;">close</mat-icon>
</button>
</mat-toolbar>
<mat-tab-group style="max-height: calc(100% - 117px); flex-grow: 1">
<mat-tab label="{{'gateway.thingsboard-general' | translate}}">
<div formGroupName="thingsboard" fxLayout="column" class="mat-content mat-padding">
<div style="padding-top: 20px" class="tb-form-panel">
<div fxLayout="row wrap">
<mat-slide-toggle color="primary" fxFlex="100" formControlName="remoteConfiguration">
{{ 'gateway.remote-configuration' | translate }}
<mat-icon class="material-icons-outlined suffix-icon slider-icon " aria-hidden="false" aria-label="help-icon"
style="cursor:pointer;"
matTooltip="{{'gateway.hints.remote-configuration' | translate }}">info
</mat-icon>
</mat-slide-toggle>
<mat-slide-toggle color="primary" fxFlex="100" formControlName="remoteShell">
{{ 'gateway.remote-shell' | translate }}
<mat-icon class="material-icons-outlined suffix-icon slider-icon " aria-hidden="false" aria-label="help-icon"
style="cursor:pointer;"
matTooltip="{{'gateway.hints.remote-shell' | translate }}">info
</mat-icon>
</mat-slide-toggle>
<mat-form-field appearance="outline" fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.thingsboard-host</mat-label>
<input matInput formControlName="host"/>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.host' | translate }}">info_outlined
</mat-icon>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.host').hasError('required')">
{{ 'gateway.thingsboard-host-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.thingsboard-port</mat-label>
<input matInput formControlName="port" type="number" min="0"/>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.port').hasError('required')">
{{'gateway.thingsboard-port-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.port').hasError('min')">
{{'gateway.thingsboard-port-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.port').hasError('max')">
{{'gateway.thingsboard-port-max' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.port').hasError('pattern')">
{{'gateway.thingsboard-port-pattern' | translate }}
</mat-error>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" aria-hidden="false"
aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.port' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-panel">
<div translate class="expansion-panel-header tb-form-panel-title tb-form-panel-title">security.security</div>
<div fxLayout="column" formGroupName="security">
<tb-toggle-header fxFlex="100" class="security-toggle-group"
[value]="gatewayConfigGroup.get('thingsboard.security.type').value"
name="securityTypeToggle" (valueChange)="updateSecurityValidators($event)"
useSelectOnMdLg="false" ignoreMdLgSize="true">
<tb-toggle-option *ngFor="let securityType of securityTypes | keyvalue"
[value]="securityType.key">{{ securityType.value | translate }}</tb-toggle-option>
</tb-toggle-header>
<div fxLayout="row wrap">
<mat-form-field appearance="outline"
*ngIf="gatewayConfigGroup.get('thingsboard.security.type').value.toLowerCase().includes('accesstoken')"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>security.access-token</mat-label>
<input matInput formControlName="accessToken"/>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.accessToken').hasError('required')">
{{'security.access-token-required' | translate}}
</mat-error>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" aria-hidden="false"
aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.token' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline" *ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'usernamePassword'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>security.clientId</mat-label>
<input matInput formControlName="clientId"/>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.clientId').hasError('required')">
{{'security.clientId-required' | translate}}
</mat-error>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" aria-hidden="false"
aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.client-id' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline" *ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'usernamePassword'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>security.username</mat-label>
<input matInput formControlName="username"/>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.username').hasError('required')">
{{'security.username-required' | translate}}
</mat-error>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" aria-hidden="false"
aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.username' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline" *ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'usernamePassword'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>security.password</mat-label>
<input matInput formControlName="password"/>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" aria-hidden="false"
aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.password' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<tb-error style="margin-top: -12px; display: block;" fxFlex="100"
[error]="gatewayConfigGroup.get('thingsboard.security').hasError('atLeastOne') ?
('device.client-id-or-user-name-necessary' | translate) : ''"></tb-error>
<tb-file-input
fxFlex="100"
hint="{{'gateway.hints.ca-cert' | translate}}"
*ngIf="gatewayConfigGroup.get('thingsboard.security.type').value.toLowerCase().includes('tls')"
formControlName="caCert"
label="{{'security.ca-cert' | translate}}"
[allowedExtensions]="'pem, cert, key'"
[accept]="'.pem, application/pem,.cert, application/cert, .key,application/key'"
dropLabel="{{'gateway.drop-file' | translate}}">
</tb-file-input>
<tb-file-input
fxFlex="100"
hint="{{'gateway.hints.cert' | translate}}"
*ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'tlsPrivateKey'"
formControlName="cert"
label="{{'security.cert' | translate}}"
[allowedExtensions]="'pem, cert, key'"
[accept]="'.pem, application/pem,.cert, application/cert, .key,application/key'"
dropLabel="{{'gateway.drop-file' | translate}}">
</tb-file-input>
<tb-file-input
fxFlex="100"
hint="{{'gateway.hints.private-key' | translate}}"
*ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'tlsPrivateKey'"
formControlName="privateKey"
label="{{'security.private-key' | translate}}"
[allowedExtensions]="'pem, cert, key'"
[accept]="'.pem, application/pem,.cert, application/cert, .key,application/key'"
dropLabel="{{'gateway.drop-file' | translate}}">
</tb-file-input>
</div>
</div>
</div>
</div>
</mat-tab>
<mat-tab label="{{'gateway.logs.logs' | translate}}">
<div formGroupName="logs" fxLayout="column" class="mat-content mat-padding">
<div class="tb-form-panel" style="padding-top: 20px">
<div fxLayout="row wrap">
<mat-form-field appearance="outline" fxFlex="100" class="mat-block tb-value-type">
<mat-label translate>gateway.logs.date-format</mat-label>
<input matInput formControlName="dateFormat"/>
<mat-error *ngIf="gatewayConfigGroup.get('logs.dateFormat').hasError('required')">
{{'gateway.logs.date-format-required' | translate }}
</mat-error>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" aria-hidden="false"
aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.date-form' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline" fxFlex="100" class="mat-block tb-value-type">
<mat-label translate>gateway.logs.log-format</mat-label>
<textarea matInput formControlName="logFormat" rows="2"></textarea>
<mat-error *ngIf="gatewayConfigGroup.get('logs.logFormat').hasError('required')">
{{'gateway.logs.log-format-required' | translate }}
</mat-error>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" aria-hidden="false"
aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.log-format' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-panel" formGroupName="remote">
<div translate class="expansion-panel-header tb-form-panel-title">gateway.logs.remote</div>
<mat-slide-toggle color="primary" formControlName="enabled">
{{ 'gateway.logs.remote-logs' | translate }}
<mat-icon class="mat-form-field-infix pointer-event suffix-icon slider-icon " aria-hidden="false"
aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.remote-log' | translate }}">info
</mat-icon>
</mat-slide-toggle>
<mat-form-field appearance="outline" class="mat-block tb-value-type">
<mat-label translate>gateway.logs.level</mat-label>
<mat-select formControlName="logLevel">
<mat-option *ngFor="let logLevel of gatewayLogLevel" [value]="logLevel">{{logLevel}}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-panel" formGroupName="local">
<div translate class="expansion-panel-header tb-form-panel-title">gateway.logs.local</div>
<div fxLayout="column">
<tb-toggle-header fxFlex="100" class="security-toggle-group"
[value]="logSelector.value" name="logTypeToggle" (valueChange)="updateLogType($event)"
useSelectOnMdLg="false" ignoreMdLgSize="true">
<tb-toggle-option *ngFor="let logConfig of localLogsConfigLabels| keyvalue" [value]="logConfig.key"
class="first-capital">{{ logConfig.value }}</tb-toggle-option>
</tb-toggle-header>
<div fxFlex="100" fxLayout="row wrap" fxLayout.lt-sm="column"
[formGroup]="getLogFormGroup(logSelector.value)">
<mat-form-field appearance="outline" class="mat-block tb-value-type" fxFlex="calc(50%-15px)">
<mat-label translate>gateway.logs.level</mat-label>
<mat-select formControlName="logLevel">
<mat-option *ngFor="let logLevel of gatewayLogLevel" [value]="logLevel">{{logLevel}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="mat-block tb-value-type" fxFlex="calc(50%-15px)">
<mat-label translate>gateway.logs.file-path</mat-label>
<input matInput formControlName="filePath"/>
<mat-error
*ngIf="gatewayConfigGroup.get('logs.local.' + logSelector.value + '.filePath').hasError('required')">
{{'gateway.logs.file-path-required' | translate }}
</mat-error>
</mat-form-field>
<div fxLayout="row" fxFlex="50" fxLayoutGap="15px">
<mat-form-field appearance="outline" class="mat-block tb-value-type" fxFlex>
<mat-label translate>gateway.logs.saving-period</mat-label>
<input matInput formControlName="savingTime" type="number" min="0"/>
<mat-error
*ngIf="gatewayConfigGroup.get('logs.local.' + logSelector.value + '.savingTime').hasError('required')">
{{'gateway.logs.saving-period-required' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('logs.local.' + logSelector.value + '.savingTime').hasError('min')">
{{'gateway.logs.saving-period-min' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="mat-block tb-value-type" style="min-width: 110px; width: 30%">
<mat-label translate></mat-label>
<mat-select formControlName="savingPeriod">
<mat-option *ngFor="let period of logSavingPeriods | keyvalue" [value]="period.key">
{{period.value | translate}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<mat-form-field appearance="outline" class="mat-block tb-value-type" fxFlex="calc(50%-30px)">
<mat-label translate>gateway.logs.backup-count</mat-label>
<input matInput formControlName="backupCount" type="number" min="0"/>
<mat-error
*ngIf="gatewayConfigGroup.get('logs.local.' + logSelector.value + '.backupCount').hasError('required')">
{{'gateway.logs.backup-count-required' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('logs.local.' + logSelector.value + '.backupCount').hasError('min')">
{{'gateway.logs.backup-count-min' | translate }}
</mat-error>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" aria-hidden="false"
aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.backup-count' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
</div>
</div>
</div>
</div>
</mat-tab>
<mat-tab label="{{'gateway.storage' | translate}}">
<div formGroupName="storage" fxLayout="column" class="mat-content mat-padding">
<div class="tb-form-panel">
<div translate class="expansion-panel-header tb-form-panel-title">gateway.storage
<span class="tb-hint">{{'gateway.hints.storage' | translate}}</span>
</div>
<div fxLayout="column">
<tb-toggle-header fxFlex="100" class="security-toggle-group"
[value]="gatewayConfigGroup.get('storage.type').value" name="storageType"
(valueChange)="updateStorageType($event)"
useSelectOnMdLg="false" ignoreMdLgSize="true">
<tb-toggle-option *ngFor="let storageType of storageTypes | keyvalue" [value]="storageType.key">
{{ storageType.value | translate }}</tb-toggle-option>
</tb-toggle-header>
<div class="line-break "></div>
<span class="tb-hint" style="padding-left: 0">
{{'gateway.hints.' + gatewayConfigGroup.get('storage.type').value | translate}}</span>
<div fxLayout="row wrap" fxLayout.lt-sm="column">
<mat-form-field appearance="outline" *ngIf="gatewayConfigGroup.get('storage.type').value === 'memory'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.storage-read-record-count</mat-label>
<input type="number" matInput formControlName="read_records_count"/>
<mat-error *ngIf="gatewayConfigGroup.get('storage.read_records_count').hasError('required')">
{{'gateway.storage-read-record-count-required' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.read_records_count').hasError('min')">
{{'gateway.storage-read-record-count-min' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.read_records_count').hasError('pattern')">
{{'gateway.storage-read-record-count-pattern' | translate}}
</mat-error>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" aria-hidden="false"
aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.read-record-count' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline" *ngIf="gatewayConfigGroup.get('storage.type').value === 'memory'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.storage-max-records</mat-label>
<input type="number" matInput formControlName="max_records_count"/>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_records_count').hasError('required')">
{{'gateway.storage-max-records-required' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_records_count').hasError('min')">
{{'gateway.storage-max-records-min' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_records_count').hasError('pattern')">
{{'gateway.storage-max-records-pattern' | translate}}
</mat-error>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" aria-hidden="false"
aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.max-records-count' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline" *ngIf="gatewayConfigGroup.get('storage.type').value === 'file'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.storage-data-folder-path</mat-label>
<input matInput formControlName="data_folder_path"/>
<mat-error *ngIf="gatewayConfigGroup.get('storage.data_folder_path').hasError('required')">
{{'gateway.storage-data-folder-path-required' | translate}}
</mat-error>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" aria-hidden="false"
aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.data-folder' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline" *ngIf="gatewayConfigGroup.get('storage.type').value === 'file'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.storage-max-files</mat-label>
<input matInput type="number" formControlName="max_file_count"/>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_file_count').hasError('required')">
{{'gateway.storage-max-files-required' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_file_count').hasError('min')">
{{'gateway.storage-max-files-min' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_file_count').hasError('pattern')">
{{'gateway.storage-max-files-pattern' | translate}}
</mat-error>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" aria-hidden="false"
aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.max-file-count' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline" *ngIf="gatewayConfigGroup.get('storage.type').value === 'file'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.storage-max-read-record-count</mat-label>
<input matInput type="number" formControlName="max_read_records_count"/>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_read_records_count').hasError('required')">
{{'gateway.storage-max-read-record-count-required' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_read_records_count').hasError('min')">
{{'gateway.storage-max-read-record-count-min' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_read_records_count').hasError('pattern')">
{{'gateway.storage-max-read-record-count-pattern' | translate}}
</mat-error>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" aria-hidden="false"
aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.max-read-count' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline" *ngIf="gatewayConfigGroup.get('storage.type').value === 'file'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.storage-max-file-records</mat-label>
<input matInput type="number" formControlName="max_records_per_file"/>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_records_per_file').hasError('required')">
{{'gateway.storage-max-records-required' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_records_per_file').hasError('min')">
{{'gateway.storage-max-records-min' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_records_per_file').hasError('pattern')">
{{'gateway.storage-max-records-pattern' | translate}}
</mat-error>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" aria-hidden="false"
aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.max-records' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline" *ngIf="gatewayConfigGroup.get('storage.type').value === 'sqlite'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.storage-path</mat-label>
<input matInput formControlName="data_file_path"/>
<mat-error *ngIf="gatewayConfigGroup.get('storage.data_file_path').hasError('required')">
{{'gateway.storage-path-required' | translate}}
</mat-error>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" aria-hidden="false"
aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.data-folder' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline" *ngIf="gatewayConfigGroup.get('storage.type').value === 'sqlite'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.messages-ttl-check-in-hours</mat-label>
<input matInput type="number" formControlName="messages_ttl_check_in_hours"/>
<mat-error *ngIf="gatewayConfigGroup.get('storage.messages_ttl_check_in_hours').hasError('required')">
{{'gateway.messages-ttl-check-in-hours-required' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.messages_ttl_check_in_hours').hasError('min')">
{{'gateway.messages-ttl-check-in-hours-min' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.messages_ttl_check_in_hours').hasError('pattern')">
{{'gateway.messages-ttl-check-in-hours-pattern' | translate}}
</mat-error>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" aria-hidden="false"
aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.ttl-check-hour' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline" *ngIf="gatewayConfigGroup.get('storage.type').value === 'sqlite'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type" style="flex-grow: 0">
<mat-label translate>gateway.messages-ttl-in-days</mat-label>
<input matInput type="number" formControlName="messages_ttl_in_days"/>
<mat-error *ngIf="gatewayConfigGroup.get('storage.messages_ttl_in_days').hasError('required')">
{{'gateway.messages-ttl-in-days-required' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.messages_ttl_in_days').hasError('min')">
{{'gateway.messages-ttl-in-days-min' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.messages_ttl_in_days').hasError('pattern')">
{{'gateway.messages-ttl-in-days-pattern' | translate}}
</mat-error>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" aria-hidden="false"
aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.ttl-messages-day' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
</div>
</div>
</div>
</div>
</mat-tab>
<mat-tab label="{{'gateway.grpc' | translate}}">
<div formGroupName="grpc" fxLayout="column" class="mat-content mat-padding">
<div class="tb-form-panel" style="padding-top: 20px">
<div fxLayout="row wrap" fxLayout.lt-md="column">
<mat-slide-toggle color="primary" fxFlex="100" formControlName="enabled">
{{ 'gateway.grpc' | translate }}
</mat-slide-toggle>
<mat-slide-toggle color="primary" fxFlex="100" formControlName="keepalivePermitWithoutCalls">
{{ 'gateway.permit-without-calls' | translate }}
<mat-icon class="material-icons-outlined suffix-icon slider-icon " aria-hidden="false" aria-label="help-icon"
style="cursor:pointer;"
matTooltip="{{'gateway.hints.permit-without-calls' | translate }}">info
</mat-icon>
</mat-slide-toggle>
<mat-form-field appearance="outline" fxFlex="calc(50%-15px)" fxFlex.lt-md="100" class="mat-block tb-value-type">
<mat-label translate>gateway.server-port</mat-label>
<input matInput formControlName="serverPort" type="number" min="0"/>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.server-port' | translate }}">info_outlined
</mat-icon>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.serverPort').hasError('required')">
{{'gateway.thingsboard-port-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.serverPort').hasError('min')">
{{'gateway.thingsboard-port-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.serverPort').hasError('max')">
{{'gateway.thingsboard-port-max' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.serverPort').hasError('pattern')">
{{'gateway.thingsboard-port-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" fxFlex="calc(50%-15px)" fxFlex.lt-md="100" class="mat-block tb-value-type">
<mat-label translate>gateway.grpc-keep-alive-timeout</mat-label>
<input matInput formControlName="keepAliveTimeoutMs" type="number" min="0"/>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.grpc-keep-alive-timeout' | translate }}">info_outlined
</mat-icon>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.keepAliveTimeoutMs').hasError('required')">
{{'gateway.grpc-keep-alive-timeout-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.keepAliveTimeoutMs').hasError('min')">
{{'gateway.grpc-keep-alive-timeout-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.keepAliveTimeoutMs').hasError('pattern')">
{{'gateway.grpc-keep-alive-timeout-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" fxFlex="calc(50%-15px)" fxFlex.lt-md="100" class="mat-block tb-value-type">
<mat-label translate>gateway.grpc-keep-alive</mat-label>
<input matInput formControlName="keepAliveTimeMs" type="number" min="0"/>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.grpc-keep-alive' | translate }}">info_outlined
</mat-icon>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.keepAliveTimeMs').hasError('required')">
{{'gateway.grpc-keep-alive-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.keepAliveTimeMs').hasError('min')">
{{'gateway.grpc-keep-alive-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.keepAliveTimeMs').hasError('pattern')">
{{'gateway.grpc-keep-alive-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" fxFlex="calc(50%-15px)" fxFlex.lt-md="100" class="mat-block tb-value-type">
<mat-label translate>gateway.grpc-min-time-between-pings</mat-label>
<input matInput formControlName="minTimeBetweenPingsMs" type="number" min="0"/>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.grpc-min-time-between-pings' | translate }}">info_outlined
</mat-icon>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.minTimeBetweenPingsMs').hasError('required')">
{{'gateway.grpc-min-time-between-pings-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.minTimeBetweenPingsMs').hasError('min')">
{{'gateway.grpc-min-time-between-pings-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.minTimeBetweenPingsMs').hasError('pattern')">
{{'gateway.grpc-min-time-between-pings-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" fxFlex="calc(50%-15px)" fxFlex.lt-md="100" class="mat-block tb-value-type">
<mat-label translate>gateway.grpc-max-pings-without-data</mat-label>
<input matInput formControlName="maxPingsWithoutData" type="number" min="0"/>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.grpc-max-pings-without-data' | translate }}">info_outlined
</mat-icon>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.maxPingsWithoutData').hasError('required')">
{{'gateway.grpc-max-pings-without-data-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.maxPingsWithoutData').hasError('min')">
{{'gateway.grpc-max-pings-without-data-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.maxPingsWithoutData').hasError('pattern')">
{{'gateway.grpc-max-pings-without-data-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" fxFlex="calc(50%-15px)" fxFlex.lt-md="100" class="mat-block tb-value-type">
<mat-label translate>gateway.grpc-min-ping-interval-without-data</mat-label>
<input matInput formControlName="minPingIntervalWithoutDataMs" type="number" min="0"/>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.grpc-min-ping-interval-without-data' | translate }}">info_outlined
</mat-icon>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.minPingIntervalWithoutDataMs').hasError('required')">
{{'gateway.grpc-min-ping-interval-without-data-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.minPingIntervalWithoutDataMs').hasError('min')">
{{'gateway.grpc-min-ping-interval-without-data-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.minPingIntervalWithoutDataMs').hasError('pattern')">
{{'gateway.grpc-min-ping-interval-without-data-pattern' | translate }}
</mat-error>
</mat-form-field>
</div>
</div>
</div>
</mat-tab>
<mat-tab label="{{'gateway.statistics.statistics' | translate}}">
<div formGroupName="thingsboard" fxLayout="column" class="mat-content mat-padding">
<div class="tb-form-panel" style="padding-top: 20px">
<div fxLayout="row wrap" formGroupName="statistics">
<mat-slide-toggle color="primary" fxFlex="100" formControlName="enable">
{{ 'gateway.statistics.statistics' | translate }}
</mat-slide-toggle>
<mat-form-field appearance="outline" fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.statistics.send-period</mat-label>
<input matInput formControlName="statsSendPeriodInSeconds" type="number" min="60"/>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.statistics.statsSendPeriodInSeconds').hasError('required')">
{{'gateway.statistics.send-period-required' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.statistics.statsSendPeriodInSeconds').hasError('min')">
{{'gateway.statistics.send-period-min' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.statistics.statsSendPeriodInSeconds').hasError('pattern')">
{{'gateway.statistics.send-period-pattern' | translate }}
</mat-error>
</mat-form-field>
</div>
</div>
<div class="tb-form-panel">
<div class="expansion-panel-header tb-form-panel-title">{{"gateway.statistics.commands" |translate}}
<span class="tb-hint">
{{'gateway.hints.commands' | translate}}</span></div>
<div fxLayout="column" formGroupName="statistics">
<div fxLayout="row" formArrayName="commands"
*ngFor="let commandControl of commandFormArray().controls; let $index = index"
style="margin-bottom: 15px">
<mat-card fxFlex.lt-md fxLayout="row wrap" fxLayout.lt-md="column" [formGroupName]="$index"
class="statistics-block">
<mat-form-field appearance="outline" fxFlex="calc(50%-15px)" fxFlex.lt-md class="mat-block tb-value-type">
<mat-label translate>gateway.statistics.attribute-name</mat-label>
<input matInput formControlName="attributeOnGateway"/>
<mat-error
*ngIf="commandControl.get('attributeOnGateway').hasError('required')">
{{'gateway.statistics.attribute-name-required' | translate }}
</mat-error>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" aria-hidden="false"
aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.attribute' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline" fxFlex="calc(50%-15px)" fxFlex.lt-md class="mat-block tb-value-type">
<mat-label translate>gateway.statistics.timeout</mat-label>
<input matInput formControlName="timeout" type="number" min="0"/>
<mat-error
*ngIf="commandControl.get('timeout').hasError('required')">
{{'gateway.statistics.timeout-required' | translate }}
</mat-error>
<mat-error
*ngIf="commandControl.get('timeout').hasError('min')">
{{'gateway.statistics.timeout-min' | translate }}
</mat-error>
<mat-error
*ngIf="commandControl.get('timeout').hasError('pattern')">
{{'gateway.statistics.timeout-pattern' | translate }}
</mat-error>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" aria-hidden="false"
aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.timeout' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline" fxFlex="calc(50%-15px)" fxFlex.lt-md class="mat-block tb-value-type"
style="flex-grow: 0">
<mat-label translate>gateway.statistics.command</mat-label>
<input matInput formControlName="command"/>
<mat-error
*ngIf="commandControl.get('command').hasError('required')">
{{'gateway.statistics.command-required' | translate }}
</mat-error>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" aria-hidden="false"
aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.command' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
</mat-card>
<button mat-icon-button (click)="removeCommandControl($index, $event)"
[disabled]="!gatewayConfigGroup.get('thingsboard.remoteConfiguration').value"
matTooltip="{{ 'gateway.statistics.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
<button mat-stroked-button color="primary"
style="width: fit-content;"
type="button"
[disabled]="!gatewayConfigGroup.get('thingsboard.remoteConfiguration').value"
(click)="addCommand()">
{{ 'gateway.statistics.add' | translate }}
</button>
</div>
</div>
</div>
</mat-tab>
<mat-tab label="{{'gateway.other' | translate}}">
<div formGroupName="thingsboard" fxLayout="column" class="mat-content mat-padding">
<div class="tb-form-panel" formGroupName="checkingDeviceActivity">
<div class="expansion-panel-header tb-form-panel-title">
<mat-slide-toggle color="primary" fxFlex="100" formControlName="checkDeviceInactivity">
{{ 'gateway.checking-device-activity' | translate }}
</mat-slide-toggle>
<span class="tb-hint"
style="padding-left: 40px; margin-top: -15px;">{{'gateway.hints.check-device-activity' | translate}}</span>
</div>
<div fxLayout="row wrap" fxLayout.lt-sm="column">
<mat-form-field
appearance="outline"
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.checkDeviceInactivity').value"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.inactivity-timeout-seconds</mat-label>
<input matInput formControlName="inactivityTimeoutSeconds" type="number" min="0"/>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds').hasError('required')">
{{'gateway.inactivity-timeout-seconds-required' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds').hasError('min')">
{{'gateway.inactivity-timeout-seconds-min' | translate }}
</mat-error>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" aria-hidden="false"
aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.inactivity-timeout' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<mat-form-field
appearance="outline"
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.checkDeviceInactivity').value"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.inactivity-check-period-seconds</mat-label>
<input matInput formControlName="inactivityCheckPeriodSeconds"/>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('required')">
{{'gateway.inactivity-check-period-seconds-required' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('min')">
{{'gateway.inactivity-check-period-seconds-min' | translate }}
</mat-error>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" aria-hidden="false"
aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.inactivity-period' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-panel">
<div translate class="expansion-panel-header tb-form-panel-title">gateway.advanced</div>
<div fxLayout="row wrap" fxLayout.lt-sm="column">
<mat-form-field appearance="outline" fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.min-pack-send-delay</mat-label>
<input matInput formControlName="minPackSendDelayMS" type="number" min="0"/>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.minPackSendDelayMS').hasError('required')">
{{ 'gateway.min-pack-send-delay-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.minPackSendDelayMS').hasError('min')">
{{ 'gateway.min-pack-send-delay-min' | translate }}
</mat-error>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" aria-hidden="false"
aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.minimal-pack-delay' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline" fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.mqtt-qos</mat-label>
<input matInput formControlName="qos" type="number" min="0" max="1"/>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.qos').hasError('required')">
{{ 'gateway.mqtt-qos-required' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.qos').hasError('min')">
{{ 'gateway.mqtt-qos-range' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.qos').hasError('max')">
{{ 'gateway.mqtt-qos-range' | translate}}
</mat-error>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" aria-hidden="false"
aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.qos' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline" fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.statistics.check-connectors-configuration</mat-label>
<input matInput formControlName="checkConnectorsConfigurationInSeconds" type="number" min="0"/>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('required')">
{{'gateway.statistics.check-connectors-configuration-required' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('min')">
{{'gateway.statistics.check-connectors-configuration-min' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('pattern')">
{{'gateway.statistics.check-connectors-configuration-pattern' | translate }}
</mat-error>
</mat-form-field>
</div>
</div>
</div>
</mat-tab>
</mat-tab-group>
<div mat-dialog-actions fxLayoutAlign="start center">
<button mat-button color="primary"
type="button"
*ngIf="dialogRef"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
[disabled]="gatewayConfigGroup.invalid || !gatewayConfigGroup.dirty"
(click)="saveConfig()">
{{ 'action.save' | translate }}
</button>
</div>
</form>

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

@ -0,0 +1,143 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
width: 100%;
height: 100%;
display: block;
.mat-icon {
color: rgba(0, 0, 0, .12);
}
.tb-form-panel {
margin-bottom: 20px;
}
.mat-toolbar {
background: transparent;
color: rgba(0, 0, 0, .87) !important;
}
.mat-content {
.expansion-panel-header {
font-weight: 600;
color: rgba(0, 0, 0, .87) !important;
}
mat-slide-toggle {
margin-bottom: 16px;
}
mat-form-field {
margin-right: 15px;
}
.slider-icon {
position: absolute;
transform: translateY(-3px);
}
.block-title {
font-size: 20px;
font-weight: 400;
padding-top: 16px;
}
.hover-cursor {
cursor: pointer;
}
}
.security-toggle-group {
margin-bottom: 15px;
margin-right: auto;
}
.logs-label {
font-weight: 500;
margin-bottom: 10px;
}
.statistics-block {
margin-bottom: 15px;
padding-left: 15px;
padding-top: 15px;
}
.first-capital {
text-transform: capitalize;
}
mat-panel-title {
display: block;
padding-top: 20px;
}
mat-panel-title span {
display: block;
padding-left: 0;
padding-top: 5px;
}
.tb-hint {
font-size: 13px;
color: rgba(0, 0, 0, .54);
width: fit-content;
cursor: pointer;
text-transform: none !important;
}
.line-break {
width: 100%;
}
textarea {
resize: none;
}
}
:host ::ng-deep {
.mat-tab-label-active {
color: white;
opacity: 1;
}
.mat-tab-label, .mat-tab-label-active{
min-width: 50px !important;
padding: 3px !important;
margin: 3px !important;
flex-grow: 1;
}
.mat-ink-bar {
height: 100%;
z-index: -10;
border-radius: 5px;
}
.pointer-event {
pointer-events: all;
}
.mat-mdc-form-field-icon-suffix {
z-index: 100;
}
.security-toggle-group span {
padding: 0 25px;
}
}

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

@ -0,0 +1,721 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import {
FormArray,
FormBuilder,
FormControl,
FormGroup,
UntypedFormGroup,
ValidationErrors,
ValidatorFn,
Validators
} from '@angular/forms';
import { EntityId } from '@shared/models/id/entity-id';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { AttributeService } from '@core/http/attribute.service';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import {
GatewayRemoteConfigurationDialogComponent,
GatewayRemoteConfigurationDialogData
} from '@home/components/widget/lib/gateway/gateway-remote-configuration-dialog';
import { DeviceService } from '@core/http/device.service';
import { Observable, of } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
import { DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models';
import { NULL_UUID } from '@shared/models/id/has-uuid';
export enum StorageTypes {
MEMORY = 'memory',
FILE = 'file',
SQLITE = 'sqlite'
}
export enum GatewayLogLevel {
none = 'NONE',
critical = 'CRITICAL',
error = 'ERROR',
warning = 'WARNING',
info = 'INFO',
debug = 'DEBUG'
}
export enum LogSavingPeriod {
days = 'D',
hours = 'H',
minutes = 'M',
seconds = 'S'
}
export enum LocalLogsConfigs {
service = 'service',
connector = 'connector',
converter = 'converter',
tb_connection = 'tb_connection',
storage = 'storage',
extension = 'extension'
}
export const localLogsConfigLabels = new Map<LocalLogsConfigs, string>([
[LocalLogsConfigs.service, 'Service'],
[LocalLogsConfigs.connector, 'Connector'],
[LocalLogsConfigs.converter, 'Converter'],
[LocalLogsConfigs.tb_connection, 'TB Connection'],
[LocalLogsConfigs.storage, 'Storage'],
[LocalLogsConfigs.extension, 'Extension']
]);
export const logSavingPeriodTranslations = new Map<LogSavingPeriod, string>(
[
[LogSavingPeriod.days, 'gateway.logs.days'],
[LogSavingPeriod.hours, 'gateway.logs.hours'],
[LogSavingPeriod.minutes, 'gateway.logs.minutes'],
[LogSavingPeriod.seconds, 'gateway.logs.seconds']
]
);
export const storageTypesTranslations = new Map<StorageTypes, string>(
[
[StorageTypes.MEMORY, 'gateway.storage-types.memory-storage'],
[StorageTypes.FILE, 'gateway.storage-types.file-storage'],
[StorageTypes.SQLITE, 'gateway.storage-types.sqlite']
]
);
export enum SecurityTypes {
ACCESS_TOKEN = 'accessToken',
USERNAME_PASSWORD = 'usernamePassword',
TLS_ACCESS_TOKEN = 'tlsAccessToken',
TLS_PRIVATE_KEY = 'tlsPrivateKey'
}
export const securityTypesTranslationsMap = new Map<SecurityTypes, string>(
[
[SecurityTypes.ACCESS_TOKEN, 'gateway.security-types.access-token'],
[SecurityTypes.USERNAME_PASSWORD, 'gateway.security-types.username-password'],
[SecurityTypes.TLS_ACCESS_TOKEN, 'gateway.security-types.tls-access-token'],
// [SecurityTypes.TLS_PRIVATE_KEY, 'gateway.security-types.tls-private-key'],
]
);
@Component({
selector: 'tb-gateway-configuration',
templateUrl: './gateway-configuration.component.html',
styleUrls: ['./gateway-configuration.component.scss']
})
export class GatewayConfigurationComponent implements OnInit {
gatewayConfigGroup: FormGroup;
storageTypes = storageTypesTranslations;
logSavingPeriods = logSavingPeriodTranslations;
localLogsConfigLabels = localLogsConfigLabels;
securityTypes = securityTypesTranslationsMap;
gatewayLogLevel = Object.values(GatewayLogLevel);
@Input()
device: EntityId;
@Input()
dialogRef: MatDialogRef<any>;
logSelector: FormControl;
securityType: SecurityTypes;
initialCredentials: DeviceCredentials;
constructor(protected router: Router,
protected store: Store<AppState>,
protected fb: FormBuilder,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
private cd: ChangeDetectorRef,
public dialog: MatDialog) {
}
ngOnInit() {
this.gatewayConfigGroup = this.fb.group({
thingsboard: this.fb.group({
host: [window.location.hostname, [Validators.required]],
port: [1883, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]],
remoteShell: [false, []],
remoteConfiguration: [true, []],
checkConnectorsConfigurationInSeconds: [60, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
statistics: this.fb.group({
enable: [true, []],
statsSendPeriodInSeconds: [3600, [Validators.required, Validators.min(60), Validators.pattern(/^-?[0-9]+$/)]],
commands: this.fb.array([], [])
}),
maxPayloadSizeBytes: [1024, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
minPackSendDelayMS: [200, [Validators.required, Validators.min(0), Validators.pattern(/^-?[0-9]+$/)]],
minPackSizeToSend: [500, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
handleDeviceRenaming: [true, []],
checkingDeviceActivity: this.fb.group({
checkDeviceInactivity: [false, []],
inactivityTimeoutSeconds: [200, [Validators.min(1)]],
inactivityCheckPeriodSeconds: [500, [Validators.min(1)]]
}),
security: this.fb.group({
type: [SecurityTypes.ACCESS_TOKEN, [Validators.required]],
accessToken: [null, [Validators.required]],
clientId: [null, []],
username: [null, []],
password: [null, []],
caCert: [null, []],
cert: [null, []],
privateKey: [null, []],
}),
qos: [1, [Validators.min(0), Validators.max(1), Validators.required]]
}),
storage: this.fb.group({
type: [StorageTypes.MEMORY, [Validators.required]],
read_records_count: [100, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]],
max_records_count: [100000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]],
data_folder_path: ['./data/', []],
max_file_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
max_read_records_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
max_records_per_file: [10000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
data_file_path: ['./data/data.db', []],
messages_ttl_check_in_hours: [1, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
messages_ttl_in_days: [7, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
}),
grpc: this.fb.group({
enabled: [false, []],
serverPort: [9595, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]],
keepAliveTimeMs: [10000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
keepAliveTimeoutMs: [5000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
keepalivePermitWithoutCalls: [true, []],
maxPingsWithoutData: [0, [Validators.required, Validators.min(0), Validators.pattern(/^-?[0-9]+$/)]],
minTimeBetweenPingsMs: [10000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
minPingIntervalWithoutDataMs: [5000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
}),
connectors: this.fb.array([]),
logs: this.fb.group({
dateFormat: ['%Y-%m-%d %H:%M:%S', [Validators.required]],
logFormat: ['%(asctime)s - |%(levelname)s| - [%(filename)s] - %(module)s - %(funcName)s - %(lineno)d - %(message)s',
[Validators.required]],
type: ['remote', [Validators.required]],
remote: this.fb.group({
enabled: [false],
logLevel: [GatewayLogLevel.info, [Validators.required]],
}),
local: this.fb.group({})
})
});
this.gatewayConfigGroup.get('thingsboard.security.password').valueChanges.subscribe(password => {
if (password && password !== '') {
this.gatewayConfigGroup.get('thingsboard.security.username').setValidators([Validators.required]);
} else {
this.gatewayConfigGroup.get('thingsboard.security.username').setValidators([]);
}
this.gatewayConfigGroup.get('thingsboard.security.username').updateValueAndValidity({emitEvent: false});
});
this.toggleRpcFields(false);
this.gatewayConfigGroup.get('thingsboard.remoteConfiguration').valueChanges.subscribe(enabled => {
if (!enabled) {
this.openConfigurationConfirmDialog();
}
});
this.logSelector = this.fb.control(LocalLogsConfigs.service);
for (const localLogsConfigsKey of Object.keys(LocalLogsConfigs)) {
this.addLocalLogConfig(localLogsConfigsKey, {});
}
const checkingDeviceActivityGroup = this.gatewayConfigGroup.get('thingsboard.checkingDeviceActivity') as FormGroup;
checkingDeviceActivityGroup.get('checkDeviceInactivity').valueChanges.subscribe(enabled => {
checkingDeviceActivityGroup.updateValueAndValidity();
if (enabled) {
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').setValidators([Validators.min(1), Validators.required]);
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').setValidators([Validators.min(1), Validators.required]);
} else {
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').clearValidators();
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').setErrors(null);
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').clearValidators();
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').setErrors(null);
}
});
this.gatewayConfigGroup.get('grpc.enabled').valueChanges.subscribe(value => {
this.toggleRpcFields(value);
});
const securityGroup = this.gatewayConfigGroup.get('thingsboard.security') as FormGroup;
securityGroup.get('type').valueChanges.subscribe(type => {
this.removeAllSecurityValidators();
if (type === SecurityTypes.ACCESS_TOKEN) {
securityGroup.get('accessToken').addValidators([Validators.required]);
securityGroup.get('accessToken').updateValueAndValidity();
} else if (type === SecurityTypes.TLS_PRIVATE_KEY) {
securityGroup.get('caCert').addValidators([Validators.required]);
securityGroup.get('caCert').updateValueAndValidity();
securityGroup.get('privateKey').addValidators([Validators.required]);
securityGroup.get('privateKey').updateValueAndValidity();
securityGroup.get('cert').addValidators([Validators.required]);
securityGroup.get('cert').updateValueAndValidity();
} else if (type === SecurityTypes.TLS_ACCESS_TOKEN) {
securityGroup.get('accessToken').addValidators([Validators.required]);
securityGroup.get('accessToken').updateValueAndValidity();
securityGroup.get('caCert').addValidators([Validators.required]);
securityGroup.get('caCert').updateValueAndValidity();
} else if (type === SecurityTypes.USERNAME_PASSWORD) {
securityGroup.addValidators([this.atLeastOneRequired(Validators.required, ['clientId', 'username'])])
// securityGroup.get('password').addValidators([Validators.required]);
// securityGroup.get('password').updateValueAndValidity();
}
securityGroup.updateValueAndValidity();
});
securityGroup.get('caCert').valueChanges.subscribe(_ => this.cd.detectChanges());
securityGroup.get('privateKey').valueChanges.subscribe(_ => this.cd.detectChanges());
securityGroup.get('cert').valueChanges.subscribe(_ => this.cd.detectChanges());
const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup;
storageGroup.get('type').valueChanges.subscribe(type => {
this.removeAllStorageValidators();
if (type === StorageTypes.MEMORY) {
storageGroup.get('read_records_count').addValidators(
[Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]);
storageGroup.get('max_records_count').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
} else if (type === StorageTypes.FILE) {
storageGroup.get('data_folder_path').addValidators([Validators.required]);
storageGroup.get('max_file_count').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('max_read_records_count').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('max_records_per_file').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
} else if (type === StorageTypes.SQLITE) {
storageGroup.get('data_file_path').addValidators([Validators.required]);
storageGroup.get('messages_ttl_check_in_hours').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('messages_ttl_in_days').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
}
});
this.fetchConfigAttribute(this.device);
}
atLeastOneRequired(validator: ValidatorFn, controls: string[] = null) {
return (group: UntypedFormGroup): ValidationErrors | null => {
if (!controls) {
controls = Object.keys(group.controls);
}
const hasAtLeastOne = group?.controls && controls.some(k => !validator(group.controls[k]));
return hasAtLeastOne ? null : {atLeastOne: true};
};
}
updateSecurityValidators(value: SecurityTypes) {
this.gatewayConfigGroup.get('thingsboard.security.type').setValue(value, {emitEvent: true});
this.gatewayConfigGroup.get('thingsboard.security.type').markAsDirty();
}
updateLogType(value: LocalLogsConfigs) {
this.logSelector.setValue(value);
}
updateStorageType(value: StorageTypes) {
this.gatewayConfigGroup.get('storage.type').setValue(value, {emitEvent: true});
this.gatewayConfigGroup.get('storage.type').markAsDirty();
}
fetchConfigAttribute(entityId: EntityId) {
if (entityId.id === NULL_UUID) return;
this.attributeService.getEntityAttributes(entityId, AttributeScope.CLIENT_SCOPE,
['general_configuration', 'grpc_configuration', 'logs_configuration', 'storage_configuration', 'RemoteLoggingLevel']).pipe(
mergeMap(attributes => attributes.length ? of(attributes) : this.attributeService.getEntityAttributes(
entityId, AttributeScope.SHARED_SCOPE, ['general_configuration', 'grpc_configuration',
'logs_configuration', 'storage_configuration', 'RemoteLoggingLevel']))
).subscribe(attributes => {
if (attributes.length) {
const general_configuration = attributes.find(attribute => attribute.key === 'general_configuration')?.value;
const grpc_configuration = attributes.find(attribute => attribute.key === 'grpc_configuration')?.value;
const logs_configuration = attributes.find(attribute => attribute.key === 'logs_configuration')?.value;
const storage_configuration = attributes.find(attribute => attribute.key === 'storage_configuration')?.value;
const remoteLoggingLevel = attributes.find(attribute => attribute.key === 'RemoteLoggingLevel')?.value;
if (general_configuration) {
const configObj = {thingsboard: general_configuration};
if (configObj.thingsboard.statistics && configObj.thingsboard.statistics.commands) {
for (const command of configObj.thingsboard.statistics.commands) {
this.addCommand(command);
}
delete configObj.thingsboard.statistics.commands;
}
this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false});
this.gatewayConfigGroup.markAsPristine();
if (!configObj.thingsboard.remoteConfiguration) {
this.gatewayConfigGroup.disable({emitEvent: false});
}
this.checkAndFetchCredentials(configObj.thingsboard.security);
}
if (grpc_configuration) {
const configObj = {grpc: grpc_configuration};
this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false});
this.toggleRpcFields(grpc_configuration.enabled);
}
if (logs_configuration) {
const configObj = {logs: this.logsToObj(logs_configuration)};
this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false});
this.cd.detectChanges();
}
if (storage_configuration) {
const configObj = {storage: storage_configuration};
this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false});
}
if (remoteLoggingLevel) {
const remoteLogsFormGroup = this.gatewayConfigGroup.get('logs.remote');
remoteLogsFormGroup.patchValue({
enabled: remoteLoggingLevel !== GatewayLogLevel.none,
logLevel: remoteLoggingLevel
}, {emitEvent: false});
remoteLogsFormGroup.markAsPristine();
}
this.cd.detectChanges();
} else {
this.checkAndFetchCredentials({});
}
});
}
checkAndFetchCredentials(security): void {
if (security.type !== SecurityTypes.TLS_PRIVATE_KEY) {
this.deviceService.getDeviceCredentials(this.device.id).subscribe(credentials => {
this.initialCredentials = credentials;
if (credentials.credentialsType === DeviceCredentialsType.ACCESS_TOKEN || security.type === SecurityTypes.TLS_ACCESS_TOKEN) {
this.gatewayConfigGroup.get('thingsboard.security.type').setValue(security.type === SecurityTypes.TLS_ACCESS_TOKEN? SecurityTypes.TLS_ACCESS_TOKEN : SecurityTypes.ACCESS_TOKEN);
this.gatewayConfigGroup.get('thingsboard.security.accessToken').setValue(credentials.credentialsId);
if(security.type === SecurityTypes.TLS_ACCESS_TOKEN) {
this.gatewayConfigGroup.get('thingsboard.security.caCert').setValue(security.caCert);
}
} else if (credentials.credentialsType === DeviceCredentialsType.MQTT_BASIC) {
const parsedValue = JSON.parse(credentials.credentialsValue);
this.gatewayConfigGroup.get('thingsboard.security.type').setValue(SecurityTypes.USERNAME_PASSWORD);
this.gatewayConfigGroup.get('thingsboard.security.clientId').setValue(parsedValue.clientId);
this.gatewayConfigGroup.get('thingsboard.security.username').setValue(parsedValue.userName);
this.gatewayConfigGroup.get('thingsboard.security.password').setValue(parsedValue.password, {emitEvent: false});
} else if (credentials.credentialsType === DeviceCredentialsType.X509_CERTIFICATE) {
//if sertificate is present set sertificate as present
}
});
}
}
logsToObj(logsConfig) {
const logsObject = {
local: {}
};
const logFormat = logsConfig.formatters.LogFormatter.format;
const dateFormat = logsConfig.formatters.LogFormatter.datefmt;
for (const localLogsConfigsKey of Object.keys(LocalLogsConfigs)) {
const handlerKey = localLogsConfigsKey + 'Handler';
logsObject[localLogsConfigsKey] = {
logLevel: logsConfig.loggers[localLogsConfigsKey].level,
filePath: logsConfig.handlers[handlerKey].filename.split('/' + localLogsConfigsKey)[0],
backupCount: logsConfig.handlers[handlerKey].backupCount,
savingTime: logsConfig.handlers[handlerKey].interval,
savingPeriod: logsConfig.handlers[handlerKey].when,
};
}
return {local: logsObject, logFormat, dateFormat};
}
toggleRpcFields(enable: boolean) {
const grpcGroup = this.gatewayConfigGroup.get('grpc') as FormGroup;
if (enable) {
grpcGroup.get('serverPort').enable();
grpcGroup.get('keepAliveTimeMs').enable();
grpcGroup.get('keepAliveTimeoutMs').enable();
grpcGroup.get('keepalivePermitWithoutCalls').enable();
grpcGroup.get('maxPingsWithoutData').enable();
grpcGroup.get('minTimeBetweenPingsMs').enable();
grpcGroup.get('minPingIntervalWithoutDataMs').enable();
} else {
grpcGroup.get('serverPort').disable();
grpcGroup.get('keepAliveTimeMs').disable();
grpcGroup.get('keepAliveTimeoutMs').disable();
grpcGroup.get('keepalivePermitWithoutCalls').disable();
grpcGroup.get('maxPingsWithoutData').disable();
grpcGroup.get('minTimeBetweenPingsMs').disable();
grpcGroup.get('minPingIntervalWithoutDataMs').disable();
}
}
addCommand(command?): void {
const data = command || {};
const commandsFormArray = this.commandFormArray();
const commandFormGroup = this.fb.group({
attributeOnGateway: [data.attributeOnGateway || null, [Validators.required]],
command: [data.command || null, [Validators.required]],
timeout: [data.timeout || null, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
});
commandsFormArray.push(commandFormGroup);
}
addLocalLogConfig(name, config): void {
const localLogsFormGroup = this.gatewayConfigGroup.get('logs.local') as FormGroup;
const configGroup = this.fb.group({
logLevel: [config.logLevel || GatewayLogLevel.info, [Validators.required]],
filePath: [config.filePath || './logs', [Validators.required]],
backupCount: [config.backupCount || 7, [Validators.required, Validators.min(0)]],
savingTime: [config.savingTime || 3, [Validators.required, Validators.min(0)]],
savingPeriod: [config.savingPeriod || LogSavingPeriod.days, [Validators.required]]
});
localLogsFormGroup.addControl(name, configGroup);
}
getLogFormGroup(value: string): FormGroup {
return this.gatewayConfigGroup.get(`logs.local.${value}`) as FormGroup;
}
commandFormArray(): FormArray {
return this.gatewayConfigGroup.get('thingsboard.statistics.commands') as FormArray;
}
removeCommandControl(index: number, event: any): void {
if (event.pointerType === '') return;
this.commandFormArray().removeAt(index);
this.gatewayConfigGroup.markAsDirty();
}
removeAllSecurityValidators(): void {
const securityGroup = this.gatewayConfigGroup.get('thingsboard.security') as FormGroup;
securityGroup.clearValidators();
for (const controlsKey in securityGroup.controls) {
if (controlsKey !== 'type') {
securityGroup.controls[controlsKey].clearValidators();
securityGroup.controls[controlsKey].setErrors(null);
securityGroup.controls[controlsKey].updateValueAndValidity();
}
}
}
removeAllStorageValidators(): void {
const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup;
for (const storageKey in storageGroup.controls) {
if (storageKey !== 'type') {
storageGroup.controls[storageKey].clearValidators();
storageGroup.controls[storageKey].setErrors(null);
storageGroup.controls[storageKey].updateValueAndValidity();
}
}
}
removeEmpty(obj) {
return Object.fromEntries(
Object.entries(obj)
.filter(([_, v]) => v != null)
.map(([k, v]) => [k, v === Object(v) ? this.removeEmpty(v) : v])
);
}
generateLogsFile(logsObj) {
const logAttrObj = {
version: 1,
disable_existing_loggers: false,
formatters: {
LogFormatter: {
class: 'logging.Formatter',
format: logsObj.logFormat,
datefmt: logsObj.dateFormat,
}
},
handlers: {
consoleHandler: {
class: 'logging.StreamHandler',
formatter: 'LogFormatter',
level: 'DEBUG',
stream: 'ext://sys.stdout'
},
databaseHandler: {
class: 'thingsboard_gateway.tb_utility.tb_handler.TimedRotatingFileHandler',
formatter: 'LogFormatter',
filename: './logs/database.log',
backupCount: 1,
encoding: 'utf-8'
}
},
loggers: {
database: {
handlers: ['databaseHandler', 'consoleHandler'],
level: 'DEBUG',
propagate: false
}
},
root: {
level: 'ERROR',
handlers: [
'consoleHandler'
]
},
ts: new Date().getTime()
};
for (const key of Object.keys(logsObj.local)) {
logAttrObj.handlers[key + 'Handler'] = this.createHandlerObj(logsObj.local[key], key);
logAttrObj.loggers[key] = this.createLoggerObj(logsObj.local[key], key);
}
return logAttrObj;
}
createHandlerObj(logObj, key) {
return {
class: 'thingsboard_gateway.tb_utility.tb_handler.TimedRotatingFileHandler',
formatter: 'LogFormatter',
filename: `${logObj.filePath}/${key}.log`,
backupCount: logObj.backupCount,
interval: logObj.savingTime,
when: logObj.savingPeriod,
encoding: 'utf-8'
};
}
createLoggerObj(logObj, key) {
return {
handlers: [`${key}Handler`, 'consoleHandler'],
level: logObj.logLevel,
propagate: false
};
}
saveConfig(): void {
const value = this.removeEmpty(this.gatewayConfigGroup.value);
value.thingsboard.statistics.commands = Object.values(value.thingsboard.statistics.commands);
const attributes = [];
attributes.push({
key: 'RemoteLoggingLevel',
value: value.logs.remote.enabled ? value.logs.remote.logLevel : GatewayLogLevel.none
});
delete value.connectors;
attributes.push({
key: 'logs_configuration',
value: this.generateLogsFile(value.logs)
});
value.grpc.ts = new Date().getTime();
attributes.push({
key: 'grpc_configuration',
value: value.grpc
});
value.storage.ts = new Date().getTime();
attributes.push({
key: 'storage_configuration',
value: value.storage
});
value.thingsboard.ts = new Date().getTime();
attributes.push({
key: 'general_configuration',
value: value.thingsboard
});
this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributes).subscribe(_ => {
this.updateCredentials(value.thingsboard.security).subscribe(_ => {
if (this.dialogRef) {
this.dialogRef.close();
} else {
this.gatewayConfigGroup.markAsPristine();
this.cd.detectChanges();
}
})
});
}
updateCredentials(securityConfig): Observable<any> {
let updateCredentials = false;
let newCredentials = {};
if (securityConfig.type === SecurityTypes.USERNAME_PASSWORD) {
if (this.initialCredentials.credentialsType !== DeviceCredentialsType.MQTT_BASIC) {
updateCredentials = true;
} else {
const parsedCredentials = JSON.parse(this.initialCredentials.credentialsValue);
updateCredentials = !(parsedCredentials.clientId === securityConfig.clientId && parsedCredentials.userName === securityConfig.username && parsedCredentials.password === securityConfig.password);
}
if (updateCredentials) {
let credentialsValue: { clientId?, userName?, password? } = {};
const credentialsType = DeviceCredentialsType.MQTT_BASIC;
if (securityConfig.clientId) credentialsValue.clientId = securityConfig.clientId;
if (securityConfig.username) credentialsValue.userName = securityConfig.username;
if (securityConfig.password) credentialsValue.password = securityConfig.password;
newCredentials = {
credentialsType,
credentialsValue: JSON.stringify(credentialsValue)
};
}
} else if (securityConfig.type === SecurityTypes.ACCESS_TOKEN || securityConfig.type === SecurityTypes.TLS_ACCESS_TOKEN) {
if (this.initialCredentials.credentialsType !== DeviceCredentialsType.ACCESS_TOKEN) {
updateCredentials = true;
} else {
updateCredentials = this.initialCredentials.credentialsId !== securityConfig.accessToken;
}
if (updateCredentials) {
newCredentials = {
credentialsType: DeviceCredentialsType.ACCESS_TOKEN,
credentialsId: securityConfig.accessToken
}
}
}
if (updateCredentials) {
return this.deviceService.saveDeviceCredentials({...this.initialCredentials,...newCredentials})
}
return of(null);
}
cancel(): void {
if (this.dialogRef) {
this.dialogRef.close();
}
}
private openConfigurationConfirmDialog(): void {
this.deviceService.getDevice(this.device.id).subscribe(gateway => {
this.dialog.open<GatewayRemoteConfigurationDialogComponent, GatewayRemoteConfigurationDialogData>
(GatewayRemoteConfigurationDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
gatewayName: gateway.name
}
}).afterClosed().subscribe(
(res) => {
if (!res) {
this.gatewayConfigGroup.get('thingsboard.remoteConfiguration').setValue(true, {emitEvent: false});
}
}
);
});
}
}

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

@ -0,0 +1,198 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form style="height: calc(100% - 7px)" fxLayout="column">
<div fxLayout="row" fxLayout.lt-lg="column" fxLayoutGap="15px" fxFlex class="connector-container">
<mat-card fxLayout="column" fxFlex.lt-lg style="overflow: auto; min-height: 35vh">
<mat-toolbar class="mat-mdc-table-toolbar">
<h2>{{ 'gateway.connectors' | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
[disabled]="isLoading$ | async"
(click)="addAttribute()"
matTooltip="{{ 'action.add' | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
</button>
</mat-toolbar>
<table mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()"
matSortDisableClear>
<ng-container matColumnDef="enabled" sticky>
<mat-header-cell *matHeaderCellDef style="width: 30px;">
{{ 'gateway.connectors-table-enabled' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let attribute">
<mat-slide-toggle [checked]="activeConnectors.includes(attribute.key)"
(click)="$event.stopPropagation(); enableConnector(attribute)"></mat-slide-toggle>
</mat-cell>
</ng-container>
<ng-container matColumnDef="key">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 40%">
{{ 'gateway.connectors-table-name' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let attribute">
{{ attribute.key }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="type">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 30%">
{{ 'gateway.connectors-table-type' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let attribute" style="text-transform: uppercase">
{{ returnType(attribute) }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="syncStatus">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 30%">
{{ 'gateway.configuration' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let attribute" style="text-transform: uppercase">
<div
[ngClass]="{
'status-block': true,
'status-sync':isConnectorSynced(attribute),
'status-unsync':!isConnectorSynced(attribute)
}">{{isConnectorSynced(attribute)?'sync' : 'out of sync'}}</div>
</mat-cell>
</ng-container>
<ng-container matColumnDef="errors">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 30%">
{{ 'gateway.connectors-table-status' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let attribute" style="text-transform: uppercase">
<span class="dot"
matTooltip="{{ 'Errors: '+ getErrorsCount(attribute)}}"
matTooltipPosition="above"
[ngClass]="{
'hasErrors': +getErrorsCount(attribute) > 0,
'noErrors': +getErrorsCount(attribute) == 0 || getErrorsCount(attribute) == ''
}"></span>
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<mat-header-cell *matHeaderCellDef
[ngStyle.gt-md]="{ minWidth: '144px', maxWidth: '144px', width: '144px'}">
{{ 'gateway.connectors-table-actions' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let attribute"
[ngStyle.gt-md]="{ minWidth: '144px', maxWidth: '144px', width: '144px'}">
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-icon-button
matTooltip="RPC"
matTooltipPosition="above"
(click)="connectorRpc(attribute, $event)">
<mat-icon>private_connectivity</mat-icon>
</button>
<button mat-icon-button
matTooltip="Logs"
matTooltipPosition="above"
(click)="connectorLogs(attribute, $event)">
<mat-icon>list</mat-icon>
</button>
<button mat-icon-button
matTooltip="Delete connector"
matTooltipPosition="above"
(click)="deleteConnector(attribute, $event)">
<mat-icon>delete</mat-icon>
</button>
</div>
<div fxHide fxShow.lt-lg>
<button mat-icon-button
(click)="$event.stopPropagation()"
[matMenuTriggerFor]="cellActionsMenu">
<mat-icon class="material-icons">more_vert</mat-icon>
</button>
<mat-menu #cellActionsMenu="matMenu" xPosition="before">
<button mat-icon-button
matTooltip="RPC"
matTooltipPosition="above"
(click)="connectorRpc(attribute, $event)">
<mat-icon>private_connectivity</mat-icon>
</button>
<button mat-icon-button
matTooltip="Delete connector"
matTooltipPosition="above"
(click)="connectorLogs(attribute, $event)">
<mat-icon>list</mat-icon>
</button>
<button mat-icon-button
matTooltip="Delete connector"
matTooltipPosition="above"
(click)="deleteConnector(attribute, $event)">
<mat-icon>delete</mat-icon>
</button>
</mat-menu>
</div>
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}"
*matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row [ngClass]="{'mat-row-select': true,
'tb-current-entity': isSameConnector(attribute)}"
*matRowDef="let attribute; columns: displayedColumns;" (click)="selectConnector(attribute)"></mat-row>
</table>
<mat-divider></mat-divider>
</mat-card>
<div [formGroup]="connectorForm" fxLayout="column">
<mat-card fxLayout="row" fxLayout.lt-lg="column" fxLayoutGap="15px" fxLayoutGap.lt-lg="5px">
<mat-form-field class="mat-block tb-value-type">
<mat-label>{{ 'gateway.connectors-table-name' | translate }}</mat-label>
<input matInput formControlName="name" #nameInput/>
</mat-form-field>
<mat-form-field class="mat-block tb-value-type">
<mat-label>{{ 'gateway.connectors-table-type' | translate }}</mat-label>
<mat-select formControlName="type">
<mat-option style="text-transform: uppercase"
*ngFor="let type of gatewayConnectorDefaultTypes | keyvalue" [value]="type.key">{{type.value}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="mat-block tb-value-type" *ngIf="connectorForm.get('type').value === 'grpc'">
<mat-label>{{ 'gateway.connectors-table-key' | translate }}</mat-label>
<input matInput formControlName="key"/>
</mat-form-field>
<mat-form-field class="mat-block tb-value-type" *ngIf="connectorForm.get('type').value === 'custom'">
<mat-label>{{ 'gateway.connectors-table-class' | translate }}</mat-label>
<input matInput formControlName="class"/>
</mat-form-field>
<mat-form-field class="mat-block tb-value-type">
<mat-label translate>gateway.remote-logging-level</mat-label>
<mat-select formControlName="logLevel">
<mat-option *ngFor="let logLevel of gatewayLogLevel" [value]="logLevel">{{logLevel}}</mat-option>
</mat-select>
</mat-form-field>
</mat-card>
<mat-card fxLayout="column" fxFlex>
<tb-json-object-edit
fxFlex
fxLayout="column"
jsonRequired
label="{{ 'gateway.configuration' | translate }}"
formControlName="configurationJson">
</tb-json-object-edit>
<div fxLayoutAlign="start center">
<button mat-raised-button color="primary"
class="action-btns"
type="button"
[disabled]="!connectorForm.dirty || connectorForm.invalid"
(click)="saveConnector()">
{{ 'action.save' | translate }}
</button>
</div>
</mat-card>
</div>
</div>
</form>

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

@ -0,0 +1,114 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
width: 100%;
height: 100%;
display: block;
overflow-x: auto;
padding: 0;
.connector-container {
width: 100%;
& > mat-card, & > div {
min-width: calc(50% - 15px);
}
mat-card {
margin: 10px;
padding: 10px;
max-width: 100%;
}
}
.tb-entity-table {
.tb-entity-table-content {
width: 100%;
height: 100%;
background: #fff;
overflow: hidden;
}
}
.mat-toolbar {
background: transparent;
color: rgba(0, 0, 0, .87) !important;
}
.mat-mdc-form-field {
flex-grow: 1;
}
mat-card {
padding-left: 10px;
background: transparent;
}
.mat-card-selected {
background-color: rgba(48, 86, 128, 0.1);
}
.mat-mdc-slide-toggle {
margin: 15px;
}
.status-block {
text-align: center;
border-radius: 16px;
font-weight: 500;
width: fit-content;
padding: 5px 15px;
}
.status-sync {
background: rgba(25, 128, 56, .06);
color: rgb(25, 128, 56);
}
.status-unsync {
background: rgba(203, 37, 48, .06);
color: rgb(203, 37, 48);
}
.action-btns {
margin: 10px 10px 0;
}
mat-row {
cursor: pointer;
}
.dot {
height: 12px;
width: 12px;
background-color: #bbb;
border-radius: 50%;
display: inline-block;
}
.hasErrors {
background-color: rgb(203, 37, 48);
}
.noErrors {
background-color: rgb(25, 128, 56);
}
}
:host ::ng-deep tb-json-object-edit > div {
flex-grow: 1;
}

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

@ -0,0 +1,517 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, NgZone, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup, UntypedFormControl, ValidatorFn, Validators } from '@angular/forms';
import { EntityId } from '@shared/models/id/entity-id';
import { MatDialog } from '@angular/material/dialog';
import { AttributeService } from '@core/http/attribute.service';
import { DeviceService } from '@core/http/device.service';
import { TranslateService } from '@ngx-translate/core';
import { forkJoin } from 'rxjs';
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { PageComponent } from '@shared/components/page.component';
import { PageLink } from '@shared/models/page/page-link';
import { AttributeDatasource } from '@home/models/datasource/attribute-datasource';
import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { MatSort } from '@angular/material/sort';
import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
import { MatTableDataSource } from '@angular/material/table';
import { GatewayLogLevel } from '@home/components/widget/lib/gateway/gateway-configuration.component';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { DialogService } from '@core/services/dialog.service';
import { WidgetContext } from '@home/models/widget-component.models';
import { deepClone } from '@core/utils';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { DatasourceType, widgetType } from '@shared/models/widget.models';
import { UtilsService } from '@core/services/utils.service';
import { EntityType } from '@shared/models/entity-type.models';
export interface gatewayConnector {
name: string;
type: string;
configuration?: string;
configurationJson: string;
logLevel: string;
key?: string;
}
export const GatewayConnectorDefaultTypesTranslates = new Map<string, string>([
['mqtt', 'MQTT'],
['modbus', 'MODBUS'],
['grpc', 'GRPC'],
['opcua', 'OPCUA'],
['opcua_asyncio', 'OPCUA ASYNCIO'],
['ble', 'BLE'],
['request', 'REQUEST'],
['can', 'CAN'],
['bacnet', 'BACNET'],
['odbc', 'ODBC'],
['rest', 'REST'],
['snmp', 'SNMP'],
['ftp', 'FTP'],
['socket', 'SOCKET'],
['xmpp', 'XMPP'],
['ocpp', 'OCCP'],
['custom', 'CUSTOM']
]);
@Component({
selector: 'tb-gateway-connector',
templateUrl: './gateway-connectors.component.html',
styleUrls: ['./gateway-connectors.component.scss']
})
export class GatewayConnectorComponent extends PageComponent implements AfterViewInit {
pageLink: PageLink;
attributeDataSource: AttributeDatasource;
inactiveConnectorsDataSource: AttributeDatasource;
serverDataSource: AttributeDatasource;
dataSource: MatTableDataSource<AttributeData>;
displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions'];
gatewayConnectorDefaultTypes = GatewayConnectorDefaultTypesTranslates;
@Input()
ctx: WidgetContext;
@Input()
device: EntityId;
@ViewChild('nameInput') nameInput: ElementRef;
@ViewChild(MatSort, {static: false}) sort: MatSort;
connectorForm: FormGroup;
viewsInited = false;
textSearchMode: boolean;
activeConnectors: Array<string>;
inactiveConnectors: Array<string>;
InitialActiveConnectors: Array<string>;
gatewayLogLevel = Object.values(GatewayLogLevel);
activeData: Array<any> = [];
inactiveData: Array<any> = [];
sharedAttributeData: Array<AttributeData> = [];
initialConnector: gatewayConnector;
subscriptionOptions: WidgetSubscriptionOptions = {
callbacks: {
onDataUpdated: () => this.ctx.ngZone.run(() => {
this.onDataUpdated();
}),
onDataUpdateError: (subscription, e) => this.ctx.ngZone.run(() => {
this.onDataUpdateError(e);
})
}
};
subscription: IWidgetSubscription;
constructor(protected router: Router,
protected store: Store<AppState>,
protected fb: FormBuilder,
protected translate: TranslateService,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
protected dialogService: DialogService,
private telemetryWsService: TelemetryWebsocketService,
private zone: NgZone,
private utils: UtilsService,
private cd: ChangeDetectorRef,
public dialog: MatDialog) {
super(store);
const sortOrder: SortOrder = {property: 'key', direction: Direction.ASC};
this.pageLink = new PageLink(1000, 0, null, sortOrder);
this.attributeDataSource = new AttributeDatasource(this.attributeService, this.telemetryWsService, this.zone, this.translate);
this.inactiveConnectorsDataSource = new AttributeDatasource(this.attributeService, this.telemetryWsService, this.zone, this.translate);
this.serverDataSource = new AttributeDatasource(this.attributeService, this.telemetryWsService, this.zone, this.translate);
this.dataSource = new MatTableDataSource<AttributeData>([]);
this.connectorForm = this.fb.group({
name: ['', [Validators.required, this.uniqNameRequired()]],
type: ['', [Validators.required]],
logLevel: ['', [Validators.required]],
key: ['auto'],
class: [''],
configuration: [''],
configurationJson: [{}, [Validators.required]]
});
this.connectorForm.disable();
}
ngAfterViewInit() {
this.connectorForm.valueChanges.subscribe(() => {
this.cd.detectChanges();
});
this.dataSource.sort = this.sort;
this.dataSource.sortingDataAccessor = (data: AttributeData, sortHeaderId: string) => {
if (sortHeaderId === 'syncStatus') {
return this.isConnectorSynced(data) ? 1 : 0;
} else if (sortHeaderId === 'enabled') {
return this.activeConnectors.includes(data.key) ? 1 : 0;
}
return data[sortHeaderId] || data.value[sortHeaderId];
};
this.viewsInited = true;
if (this.device) {
if (this.device.id === NULL_UUID) return;
forkJoin(this.attributeService.getEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, ['active_connectors']),
this.attributeService.getEntityAttributes(this.device, AttributeScope.SERVER_SCOPE, ['inactive_connectors'])).subscribe(attributes => {
if (attributes.length) {
this.activeConnectors = attributes[0].length ? attributes[0][0].value : [];
this.activeConnectors = typeof this.activeConnectors === 'string' ? JSON.parse(this.activeConnectors) : this.activeConnectors;
this.inactiveConnectors = attributes[1].length ? attributes[1][0].value : [];
this.inactiveConnectors = typeof this.inactiveConnectors === 'string' ? JSON.parse(this.inactiveConnectors) : this.inactiveConnectors;
this.updateData(true);
} else {
this.activeConnectors = [];
this.inactiveConnectors = [];
this.updateData(true);
}
});
}
}
uniqNameRequired(): ValidatorFn {
return (c: UntypedFormControl) => {
const newName = c.value.trim().toLowerCase();
const found = this.dataSource.data.find((connectorAttr) => {
const connectorData = connectorAttr.value;
return connectorData.name.toLowerCase() === newName;
});
if (found) {
if (this.initialConnector && this.initialConnector.name.toLowerCase() === newName) {
return null;
}
return {
duplicateName: {
valid: false
}
};
}
return null;
};
}
saveConnector(): void {
const value = this.connectorForm.value;
value.configuration = this.camelize(value.name) + '.json';
if (value.type !== 'grpc') {
delete value.key;
}
if (value.type !== 'custom') {
delete value.class;
}
value.ts = new Date().getTime();
const attributesToSave = [{
key: value.name,
value
}];
const attributesToDelete = [];
const scope = (this.initialConnector && this.activeConnectors.includes(this.initialConnector.name)) ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE;
let updateActiveConnectors = false;
if (this.initialConnector && this.initialConnector.name !== value.name) {
attributesToDelete.push({key: this.initialConnector.name});
updateActiveConnectors = true;
const activeIndex = this.activeConnectors.indexOf(this.initialConnector.name);
const inactiveIndex = this.inactiveConnectors.indexOf(this.initialConnector.name);
if (activeIndex !== -1) {
this.activeConnectors.splice(activeIndex, 1);
}
if (inactiveIndex !== -1) {
this.inactiveConnectors.splice(inactiveIndex, 1);
}
}
if (!this.activeConnectors.includes(value.name) && scope == AttributeScope.SHARED_SCOPE) {
this.activeConnectors.push(value.name);
updateActiveConnectors = true;
}
if (!this.inactiveConnectors.includes(value.name) && scope == AttributeScope.SERVER_SCOPE) {
this.inactiveConnectors.push(value.name);
updateActiveConnectors = true;
}
const tasks = [this.attributeService.saveEntityAttributes(this.device, scope, attributesToSave)];
if (updateActiveConnectors) {
tasks.push(this.attributeService.saveEntityAttributes(this.device, scope, [{
key: scope == AttributeScope.SHARED_SCOPE ? 'active_connectors' : 'inactive_connectors',
value: scope == AttributeScope.SHARED_SCOPE ? this.activeConnectors : this.inactiveConnectors
}]));
}
if (attributesToDelete.length) {
tasks.push(this.attributeService.deleteEntityAttributes(this.device, scope, attributesToDelete));
}
forkJoin(tasks).subscribe(_ => {
this.initialConnector = value;
this.showToast('Update Successful');
this.updateData(true);
});
}
updateData(reload: boolean = false) {
this.pageLink.sortOrder.property = this.sort.active;
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
this.attributeDataSource.loadAttributes(this.device, AttributeScope.CLIENT_SCOPE, this.pageLink, reload).subscribe(data => {
this.activeData = data.data.filter(value => this.activeConnectors.includes(value.key));
this.combineData();
this.generateSubscription();
});
this.inactiveConnectorsDataSource.loadAttributes(this.device, AttributeScope.SHARED_SCOPE, this.pageLink, reload).subscribe(data => {
this.sharedAttributeData = data.data.filter(value => this.activeConnectors.includes(value.key));
this.combineData();
});
this.serverDataSource.loadAttributes(this.device, AttributeScope.SERVER_SCOPE, this.pageLink, reload).subscribe(data => {
this.inactiveData = data.data.filter(value => this.inactiveConnectors.includes(value.key));
this.combineData();
});
}
isConnectorSynced(attribute: AttributeData) {
const connectorData = attribute.value;
if (!connectorData.ts) return false;
const clientIndex = this.activeData.findIndex(data => {
const sharedData = data.value;
return sharedData.name === connectorData.name;
})
if (clientIndex == -1) return false;
const sharedIndex = this.sharedAttributeData.findIndex(data => {
const sharedData = data.value;
return sharedData.name === connectorData.name && sharedData.ts && sharedData.ts <= connectorData.ts;
})
return sharedIndex !== -1;
}
combineData() {
this.dataSource.data = [...this.activeData, ...this.inactiveData, ...this.sharedAttributeData].filter((item, index, self) =>
index === self.findIndex((t) => t.key === item.key)
).map(attribute=>{
attribute.value = typeof attribute.value === 'string' ? JSON.parse(attribute.value) : attribute.value;
return attribute
});
}
addAttribute(): void {
if (this.connectorForm.disabled) {
this.connectorForm.enable();
}
this.nameInput.nativeElement.focus();
this.clearOutConnectorForm();
}
clearOutConnectorForm(): void {
this.connectorForm.setValue({
name: '',
type: 'mqtt',
logLevel: GatewayLogLevel.info,
key: 'auto',
class: '',
configuration: '',
configurationJson: {}
});
this.initialConnector = null;
this.connectorForm.markAsPristine();
}
selectConnector(attribute): void {
if (this.connectorForm.disabled) {
this.connectorForm.enable();
}
const connector = attribute.value;
if (!connector.configuration) {
connector.configuration = '';
}
if (!connector.key) {
connector.key = 'auto';
}
this.initialConnector = connector;
this.connectorForm.patchValue(connector);
this.connectorForm.markAsPristine();
}
isSameConnector(attribute): boolean {
if (!this.initialConnector) return false;
const connector = attribute.value;
return this.initialConnector.name === connector.name;
}
showToast(message: string) {
this.store.dispatch(new ActionNotificationShow(
{
message,
type: 'success',
duration: 1000,
verticalPosition: 'top',
horizontalPosition: 'right',
target: 'dashboardRoot',
// panelClass: this.widgetNamespace,
forceDismiss: true
}));
}
returnType(attribute) {
const value = attribute.value;
return this.gatewayConnectorDefaultTypes.get(value.type);
}
deleteConnector(attribute: AttributeData, $event: Event): void {
if ($event) {
$event.stopPropagation();
}
const title = `Delete connector ${attribute.key}?`;
const content = `All connector data will be deleted.`;
this.dialogService.confirm(title, content, 'Cancel', 'Delete').subscribe(result => {
if (result) {
const tasks = [];
const scope = (this.initialConnector && this.activeConnectors.includes(this.initialConnector.name)) ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE;
tasks.push(this.attributeService.deleteEntityAttributes(this.device, scope, [attribute]));
const activeIndex = this.activeConnectors.indexOf(attribute.key);
const inactiveIndex = this.inactiveConnectors.indexOf(attribute.key);
if (activeIndex !== -1) {
this.activeConnectors.splice(activeIndex, 1);
}
if (inactiveIndex !== -1) {
this.inactiveConnectors.splice(inactiveIndex, 1);
}
tasks.push(this.attributeService.saveEntityAttributes(this.device, scope, [{
key: scope == AttributeScope.SHARED_SCOPE ? 'active_connectors' : 'inactive_connectors',
value: scope == AttributeScope.SHARED_SCOPE ? this.activeConnectors : this.inactiveConnectors
}]));
forkJoin(tasks).subscribe(_ => {
if (this.initialConnector ? this.initialConnector.name === attribute.key : true) {
this.clearOutConnectorForm();
this.cd.detectChanges();
this.connectorForm.disable();
}
this.updateData(true);
});
}
});
}
camelize(str): string {
return str.toLowerCase().replace(/\s+/g, '_');
}
connectorLogs(attribute: AttributeData, $event: Event): void {
if ($event) {
$event.stopPropagation();
}
const params = deepClone(this.ctx.stateController.getStateParams());
params.connector_logs = attribute;
params.targetEntityParamName = 'connector_logs';
this.ctx.stateController.openState('connector_logs', params);
}
connectorRpc(attribute: AttributeData, $event: Event): void {
if ($event) {
$event.stopPropagation();
}
const params = deepClone(this.ctx.stateController.getStateParams());
params.connector_rpc = attribute;
params.targetEntityParamName = 'connector_rpc';
this.ctx.stateController.openState('connector_rpc', params);
}
enableConnector(attribute): void {
const wasEnabled = this.activeConnectors.includes(attribute.key);
const scopeOld = wasEnabled ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE;
const scopeNew = !wasEnabled ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE;
attribute.value.ts = new Date().getTime();
const tasks = [this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, [{
key: 'active_connectors',
value: this.activeConnectors
}]), this.attributeService.saveEntityAttributes(this.device, AttributeScope.SERVER_SCOPE, [{
key: 'inactive_connectors',
value: this.inactiveConnectors
}]), this.attributeService.deleteEntityAttributes(this.device, scopeOld, [attribute]),
this.attributeService.saveEntityAttributes(this.device, scopeNew, [attribute])];
if (wasEnabled) {
const index = this.activeConnectors.indexOf(attribute.key);
this.activeConnectors.splice(index, 1);
this.inactiveConnectors.push(attribute.key);
} else {
const index = this.inactiveConnectors.indexOf(attribute.key);
this.inactiveConnectors.splice(index, 1);
this.activeConnectors.push(attribute.key);
}
forkJoin(tasks).subscribe(_ => {
this.updateData(true);
});
}
onDataUpdateError(e: any) {
const exceptionData = this.utils.parseException(e);
let errorText = exceptionData.name;
if (exceptionData.message) {
errorText += ': ' + exceptionData.message;
}
console.error(errorText);
}
onDataUpdated() {
this.cd.detectChanges();
}
generateSubscription() {
if (this.subscription) {
this.subscription.unsubscribe();
}
if (this.device) {
const subscriptionInfo = [{
type: DatasourceType.entity,
entityType: EntityType.DEVICE,
entityId: this.device.id,
entityName: "Gateway",
timeseries: []
}];
this.dataSource.data.forEach(value => {
subscriptionInfo[0].timeseries.push({name: `${value.key}_ERRORS_COUNT`, label: `${value.key}_ERRORS_COUNT`})
})
this.ctx.subscriptionApi.createSubscriptionFromInfo(widgetType.latest, subscriptionInfo,this.subscriptionOptions, false, true).subscribe(subscription => {
this.subscription = subscription;
});
}
}
getErrorsCount(attribute) {
const connectorName = attribute.key;
const connector = this.subscription && this.subscription.data.find(data=>data && data.dataKey.name === `${connectorName}_ERRORS_COUNT`);
return (connector && this.activeConnectors.includes(connectorName))? connector.data[0][1]: 'Inactive';
}
}

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.html

@ -187,7 +187,7 @@
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
<div class="tb-panel-title">{{ 'gateway.connectors' | translate | uppercase }}</div>
<div class="tb-panel-title">{{ 'gateway.connectors-config' | translate | uppercase }}</div>
</mat-panel-title>
</mat-expansion-panel-header>

1
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.ts

@ -286,6 +286,7 @@ export class GatewayFormComponent extends PageComponent implements OnInit, OnDes
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
jsonValue: config,
required: true,
title: this.translate.instant('gateway.title-connectors-json', {typeName: type})
}
}).afterClosed().subscribe(

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

@ -0,0 +1,61 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<nav mat-tab-nav-bar [tabPanel]="tabPanel">
<a mat-tab-link *ngFor="let link of logLinks"
(click)="onTabChanged(link)"
[active]="activeLink.name == link.name"> {{link.name}} </a>
</nav>
<mat-tab-nav-panel #tabPanel></mat-tab-nav-panel>
<table mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()"
matSortDisableClear>
<ng-container matColumnDef="ts">
<mat-header-cell *matHeaderCellDef mat-sort-header> Created time</mat-header-cell>
<mat-cell *matCellDef="let attribute">
{{ attribute.ts | date:'yyyy-MM-dd HH:mm:ss'}}
</mat-cell>
</ng-container>
<ng-container matColumnDef="status">
<mat-header-cell *matHeaderCellDef mat-sort-header> Status</mat-header-cell>
<mat-cell *matCellDef="let attribute">
<span [ngClass]="statusClass(attribute.status )">{{ attribute.status }}</span>
</mat-cell>
</ng-container>
<ng-container matColumnDef="message">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 70%">Message</mat-header-cell>
<mat-cell *matCellDef="let attribute" [ngClass]="statusClassMsg(attribute.status )">
{{ attribute.message }}
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}"
*matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row [ngClass]="{'mat-row-select': true}"
*matRowDef="let attribute; columns: displayedColumns;"></mat-row>
</table>
<span [fxShow]="dataSource.data.length === 0"
fxFlex
fxLayoutAlign="center center"
class="no-data-found">{{'attribute.no-telemetry-text' | translate}}</span>
<span fxFlex [fxShow]="dataSource.data.length !== 0"></span>
<mat-divider></mat-divider>
<mat-paginator #paginator
[length]="dataSource.data.length"
[pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize"
[pageSizeOptions]="[10, 20, 30]"></mat-paginator>

56
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.scss

@ -0,0 +1,56 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow-x: auto;
padding: 0;
.status {
border-radius: 20px;
font-weight: 500;
padding: 5px 15px;
}
.status-debug {
color: green;
background: rgba(0, 128, 0, 0.1);
}
.status-warning {
color: orange;
background: rgba(255, 165, 0, 0.1);
}
.status-error {
color: red;
background: rgba(255, 0, 0, 0.1);
}
.status-info {
color: blue;
background: rgba(0, 0, 128, 0.1);
}
.msg-status-exception {
color: red;
}
}

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

@ -0,0 +1,237 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup } from '@angular/forms';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { AttributeService } from '@core/http/attribute.service';
import { DeviceService } from '@core/http/device.service';
import { TranslateService } from '@ngx-translate/core';
import { AttributeData, DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { PageComponent } from '@shared/components/page.component';
import { PageLink } from '@shared/models/page/page-link';
import { AttributeDatasource } from "@home/models/datasource/attribute-datasource";
import { Direction, SortOrder } from "@shared/models/page/sort-order";
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { GatewayLogLevel } from '@home/components/widget/lib/gateway/gateway-configuration.component';
import { DialogService } from '@core/services/dialog.service';
import { WidgetContext } from '@home/models/widget-component.models';
import { MatPaginator } from '@angular/material/paginator';
export interface GatewayConnector {
name: string;
type: string;
configuration?: string;
configurationJson: string;
logLevel: string;
key?: string;
}
export interface LogLink {
name: string;
key: string;
filterFn?: Function;
}
@Component({
selector: 'tb-gateway-logs',
templateUrl: './gateway-logs.component.html',
styleUrls: ['./gateway-logs.component.scss']
})
export class GatewayLogsComponent extends PageComponent implements AfterViewInit {
pageLink: PageLink;
attributeDataSource: AttributeDatasource;
dataSource: MatTableDataSource<any>
displayedColumns = ['ts', 'status', 'message'];
@Input()
ctx: WidgetContext;
@Input()
dialogRef: MatDialogRef<any>;
@ViewChild('searchInput') searchInputField: ElementRef;
@ViewChild(MatSort) sort: MatSort;
@ViewChild(MatPaginator) paginator: MatPaginator;
connectorForm: FormGroup;
viewsInited = false;
textSearchMode: boolean;
activeConnectors: Array<string>;
inactiveConnectors: Array<string>;
InitialActiveConnectors: Array<string>;
gatewayLogLevel = Object.values(GatewayLogLevel);
logLinks: Array<LogLink>;
initialConnector: GatewayConnector;
activeLink: LogLink;
gatewayLogLinks: Array<LogLink> = [
{
name: "General",
key: "LOGS"
}, {
name: "Service",
key: "SERVICE_LOGS"
},
{
name: "Connection",
key: "CONNECTION_LOGS"
}, {
name: "Storage",
key: "STORAGE_LOGS"
},
{
key: 'EXTENSIONS_LOGS',
name: "Extension"
}]
constructor(protected router: Router,
protected store: Store<AppState>,
protected fb: FormBuilder,
protected translate: TranslateService,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
protected dialogService: DialogService,
public dialog: MatDialog) {
super(store);
const sortOrder: SortOrder = {property: 'ts', direction: Direction.DESC};
this.pageLink = new PageLink(10, 0, null, sortOrder);
this.dataSource = new MatTableDataSource<AttributeData>([]);
}
ngAfterViewInit() {
this.dataSource.sort = this.sort;
this.dataSource.paginator = this.paginator;
this.ctx.defaultSubscription.onTimewindowChangeFunction = timewindow => {
this.ctx.defaultSubscription.options.timeWindowConfig = timewindow;
this.ctx.defaultSubscription.updateDataSubscriptions();
return timewindow;
}
if (this.ctx.settings.isConnectorLog && this.ctx.settings.connectorLogState) {
const connector = this.ctx.stateController.getStateParams()[this.ctx.settings.connectorLogState];
this.logLinks = [{
key: `${connector.key}_LOGS`,
name: "Connector",
filterFn: (attrData)=>{
return !attrData.message.includes(`_converter.py`)
}
},{
key: `${connector.key}_LOGS`,
name: "Converter",
filterFn: (attrData)=>{
return attrData.message.includes(`_converter.py`)
}
}]
} else {
this.logLinks = this.gatewayLogLinks;
}
this.activeLink = this.logLinks[0];
this.changeSubscription();
}
updateData(sort?) {
if (this.ctx.defaultSubscription.data.length && this.ctx.defaultSubscription.data[0]) {
let attrData = this.ctx.defaultSubscription.data[0].data.map(data => {
let result = {
ts: data[0],
key: this.activeLink.key,
message: /\[(.*)/.exec(data[1])[0],
status: 'INVALID LOG FORMAT'
};
try {
result.status= data[1].match(/\|(\w+)\|/)[1];
} catch (e) {
result.status = 'INVALID LOG FORMAT'
}
return result;
});
if (this.activeLink.filterFn) {
attrData = attrData.filter(data => this.activeLink.filterFn(data));
}
this.dataSource.data = attrData;
if (sort) {
this.dataSource.sortData(this.dataSource.data, this.sort);
}
}
}
onTabChanged(link) {
this.activeLink = link;
this.changeSubscription();
}
statusClass(status) {
switch (status) {
case GatewayLogLevel.debug:
return "status status-debug";
case GatewayLogLevel.warning:
return "status status-warning";
case GatewayLogLevel.error:
case "EXCEPTION":
return "status status-error";
case GatewayLogLevel.info:
default:
return "status status-info";
}
}
statusClassMsg(status) {
if (status === "EXCEPTION") {
return 'msg-status-exception';
}
}
changeSubscription() {
if (this.ctx.datasources && this.ctx.datasources[0].entity && this.ctx.defaultSubscription.options.datasources) {
this.ctx.defaultSubscription.options.datasources[0].dataKeys = [{
name: this.activeLink.key,
type: DataKeyType.timeseries,
settings: {}
}];
this.ctx.defaultSubscription.unsubscribe();
this.ctx.defaultSubscription.updateDataSubscriptions();
this.ctx.defaultSubscription.callbacks.onDataUpdated = () => {
this.updateData();
}
}
}
}

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

@ -0,0 +1,57 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form style="width: 600px; position: relative;">
<mat-toolbar color="warn">
<mat-icon>warning</mat-icon>
<h2 translate>gateway.configuration-delete-dialog-header</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="close()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content tb-toast toastTarget="activationLinkDialogContent">
<div class="mat-content" fxLayout="column">
<span innerHTML="{{'gateway.configuration-delete-dialog-body' | translate}} <b>{{gatewayName}}</b>" > </span>
<mat-form-field class="mat-block tb-value-type" style="flex-grow: 0">
<mat-label translate>gateway.configuration-delete-dialog-input</mat-label>
<input matInput [formControl]="gatewayControl" required/>
<mat-error
*ngIf="gatewayControl.hasError('required')">
{{'gateway.configuration-delete-dialog-input-required' | translate }}
</mat-error>
</mat-form-field>
</div>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="warn"
type="button"
cdkFocusInitial
(click)="close()">
{{ 'action.cancel' | translate }}
</button>
<button mat-button color="warn"
type="button"
cdkFocusInitial
[disabled]="gatewayControl.value !== gatewayName"
(click)="turnOff()">
{{ 'gateway.configuration-delete-dialog-confirm' | translate }}
</button>
</div>
</form>

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

@ -0,0 +1,61 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import { FormBuilder, FormControl } from '@angular/forms';
export interface GatewayRemoteConfigurationDialogData {
gatewayName: string;
}
@Component({
selector: 'gateway-remote-configuration-dialog',
templateUrl: './gateway-remote-configuration-dialog.html'
})
export class GatewayRemoteConfigurationDialogComponent extends DialogComponent<GatewayRemoteConfigurationDialogComponent,
boolean> implements OnInit {
gatewayName: string;
gatewayControl: FormControl;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: GatewayRemoteConfigurationDialogData,
public dialogRef: MatDialogRef<GatewayRemoteConfigurationDialogComponent, boolean>,
private fb: FormBuilder) {
super(store, router, dialogRef);
this.gatewayName = this.data.gatewayName;
this.gatewayControl = this.fb.control(null);
}
ngOnInit(): void {
}
close(): void {
this.dialogRef.close();
}
turnOff(): void {
this.dialogRef.close(true);
}
}

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

@ -0,0 +1,53 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div fxLayout="row" fxLayout.lt-sm="column" class="command-form" fxLayoutGap="10px" [formGroup]="commandForm">
<mat-form-field class="mat-block tb-value-type">
<mat-label>{{'gateway.statistics.command' | translate}}</mat-label>
<mat-select formControlName="command" *ngIf="!isConnector">
<mat-option *ngFor="let command of RPCCommands" [value]="command">
{{command}}
</mat-option>
</mat-select>
<input matInput formControlName="command" *ngIf="isConnector"/>
</mat-form-field>
<mat-form-field class="mat-block tb-value-type" fxFlex *ngIf="!isConnector">
<mat-label>{{'gateway.statistics.timeout-ms' | translate}}</mat-label>
<input matInput formControlName="time" type="number" min="1"/>
<mat-error
*ngIf="commandForm.get('time').hasError('min')">
{{'gateway.statistics.timeout-min' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block tb-value-type" fxFlex *ngIf="isConnector">
<mat-label>{{'widget-config.datasource-parameters' | translate}}</mat-label>
<input matInput formControlName="params" type="JSON"/>
<mat-icon class="material-icons-outlined" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
(click)="openEditJSONDialog($event)"
matTooltip="{{'gateway.rpc-command-edit-params' | translate}}">edit
</mat-icon>
</mat-form-field>
<button mat-raised-button color="primary" (click)="sendCommand()"
[disabled]="commandForm.invalid">{{'gateway.rpc-command-send' | translate}}</button>
</div>
<mat-card class="result-block" [formGroup]="commandForm" fxFlex>
<span>{{'gateway.rpc-command-result' | translate}}</span>
<mat-divider></mat-divider>
<tb-json-content [contentType]="contentTypes.JSON" readonly="true" formControlName="result" fxFlex></tb-json-content>
</mat-card>

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

@ -0,0 +1,55 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
width: 100%;
height: 100%;
overflow-x: auto;
display: flex;
flex-direction: column;
padding: 0;
.command-form {
width: 100%;
flex-wrap: nowrap;
padding: 0 15px 5px;
margin-bottom: 5px;
& > button {
margin-top: 10px;
}
}
.result-block {
padding: 0 15px;
display: flex;
flex-direction: column;
& > span {
font-weight: 600;
}
}
}
:host ::ng-deep {
.tb-json-content {
height: 100%;
}
.mat-mdc-form-field-icon-suffix {
z-index: 100;
}
}

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

@ -0,0 +1,130 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { AfterViewInit, Component, Input } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { AttributeService } from '@core/http/attribute.service';
import { DeviceService } from '@core/http/device.service';
import { TranslateService } from '@ngx-translate/core';
import { PageComponent } from "@shared/components/page.component";
import { DialogService } from '@core/services/dialog.service';
import { WidgetContext } from '@home/models/widget-component.models';
import { ContentType } from '@shared/models/constants';
import {
JsonObjectEditDialogComponent,
JsonObjectEditDialogData
} from '@shared/components/dialog/json-object-edit-dialog.component';
import { jsonRequired } from '@shared/components/json-object-edit.component';
@Component({
selector: 'tb-gateway-service-rpc',
templateUrl: './gateway-service-rpc.component.html',
styleUrls: ['./gateway-service-rpc.component.scss']
})
export class GatewayServiceRPCComponent extends PageComponent implements AfterViewInit {
@Input()
ctx: WidgetContext;
contentTypes = ContentType;
@Input()
dialogRef: MatDialogRef<any>;
commandForm: FormGroup;
isConnector: boolean;
connectorType: string;
RPCCommands: Array<string> = [
"Ping",
"Stats",
"Devices",
"Update",
"Version",
"Restart",
"Reboot"
]
constructor(protected router: Router,
protected store: Store<AppState>,
protected fb: FormBuilder,
protected translate: TranslateService,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
protected dialogService: DialogService,
public dialog: MatDialog) {
super(store);
this.commandForm = this.fb.group({
command: [null,[Validators.required]],
time: [60, [Validators.required, Validators.min(1)]],
params: [{}, [jsonRequired]],
result: [null]
})
}
ngAfterViewInit() {
this.isConnector = this.ctx.settings.isConnector;
if (!this.isConnector) {
this.commandForm.get('command').setValue(this.RPCCommands[0]);
} else {
this.connectorType = this.ctx.stateController.getStateParams().connector_rpc.value.type;
}
}
sendCommand() {
const formValues = this.commandForm.value;
const commandPrefix = this.isConnector ? `${this.connectorType}_` : 'gateway_';
this.ctx.controlApi.sendTwoWayCommand(commandPrefix+formValues.command.toLowerCase(), {},formValues.time).subscribe(resp=>{
this.commandForm.get('result').setValue(JSON.stringify(resp));
},error => {
console.log(error);
this.commandForm.get('result').setValue(JSON.stringify(error.error));
})
}
openEditJSONDialog($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.dialog.open<JsonObjectEditDialogComponent, JsonObjectEditDialogData, object>(JsonObjectEditDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
jsonValue: JSON.parse(this.commandForm.get('params').value),
required: true
}
}).afterClosed().subscribe(
(res) => {
if (res) {
this.commandForm.get('params').setValue(JSON.stringify(res));
}
}
);
}
}

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

@ -0,0 +1,81 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="statistics-container" fxLayout="row" fxLayout.lt-md="column">
<mat-card [formGroup]="statisticForm" *ngIf="!general">
<mat-form-field class="mat-block">
<mat-label>{{'gateway.statistics.statistic' | translate}}</mat-label>
<mat-select formControlName="statisticKey">
<mat-option *ngFor="let key of statisticsKeys" [value]="key">
{{key}}
</mat-option>
<mat-option *ngFor="let command of commands" [value]="command.attributeOnGateway">
{{command.attributeOnGateway}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-error
*ngIf="!statisticsKeys.length && !commands.length">
{{'gateway.statistics.statistic-commands-empty' | translate }}
</mat-error>
<mat-form-field class="mat-block" *ngIf="commandObj">
<mat-label>{{'gateway.statistics.command' | translate}}</mat-label>
<input matInput [value]="commandObj.command" disabled>
</mat-form-field>
</mat-card>
<div class="chart-box" fxLayout="column">
<div class="chart-container" #statisticChart [fxShow]="isNumericData">
</div>
<table [fxShow]="!isNumericData" mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()"
matSortDisableClear>
<ng-container matColumnDef="0">
<mat-header-cell *matHeaderCellDef mat-sort-header>{{'audit-log.timestamp' | translate}}</mat-header-cell>
<mat-cell *matCellDef="let row; let rowIndex = index">
{{row[0]| date:'yyyy-MM-dd HH:mm:ss' }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="1">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 70%">{{"event.message" | translate}}</mat-header-cell>
<mat-cell *matCellDef="let row">
{{ row[1] }}
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}"
*matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row [ngClass]="{'mat-row-select': true}"
*matRowDef="let row; columns: displayedColumns;"></mat-row>
</table>
<span [fxShow]="dataSource.data.length === 0 && !isNumericData"
fxLayoutAlign="center center"
class="no-data-found">{{'attribute.no-telemetry-text' | translate}}</span>
<div fxFlex class="legend" fxLayout="row" fxLayoutAlign="center center" [fxShow]="isNumericData">
<div class="legend-keys" *ngFor="let legendKey of legendData?.keys" fxLayout="row"
fxLayoutAlign="center center">
<span class="legend-line" [ngStyle]="{backgroundColor: legendKey.dataKey.color}"></span>
<div class="legend-label"
(click)="onLegendKeyHiddenChange(legendKey.dataIndex)"
[ngClass]="{ 'hidden-label': legendData.keys[legendKey.dataIndex].dataKey.hidden }"
[innerHTML]="legendKey.dataKey.label">
</div>
</div>
</div>
</div>
</div>

86
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.scss

@ -0,0 +1,86 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
width: 100%;
height: 100%;
padding: 0;
.statistics-container {
height: 100%;
overflow-y: auto;
mat-card {
width: 40%;
height: 100%;
margin-right: 35px;
padding: 15px;
}
@media only screen and (max-width: 750px) {
mat-card {
width: 100%;
}
}
.chart-box, .chart-container {
height: 100%;
flex-grow: 1;
}
.chart-box {
overflow: auto;
}
& > * {
height: 100%;
}
}
.legend {
flex-wrap: wrap;
width: 100%;
padding-top: 8px;
padding-bottom: 4px;
margin-top: 15px;
.legend-keys {
.legend-label {
padding: 2px 20px 2px 10px;
white-space: nowrap;
&.hidden-label {
text-decoration: line-through;
opacity: .6;
}
&:focus {
outline: none;
}
}
.legend-line {
display: inline-block;
width: 15px;
height: 3px;
text-align: left;
vertical-align: middle;
outline: none;
}
}
}
}

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

@ -0,0 +1,299 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { AttributeService } from '@core/http/attribute.service';
import { DeviceService } from '@core/http/device.service';
import { TranslateService } from '@ngx-translate/core';
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { PageComponent } from '@shared/components/page.component';
import { DialogService } from '@core/services/dialog.service';
import { WidgetContext } from '@home/models/widget-component.models';
import { TbFlot } from '@home/components/widget/lib/flot-widget';
import { ResizeObserver } from '@juggle/resize-observer';
import { IWidgetSubscription, SubscriptionInfo, WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { UtilsService } from '@core/services/utils.service';
import { DatasourceType, LegendConfig, LegendData, LegendPosition, widgetType } from '@shared/models/widget.models';
import { EntityType } from '@shared/models/entity-type.models';
import { EntityId } from '@shared/models/id/entity-id';
import { BaseData } from '@shared/models/base-data';
import { PageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { MatTableDataSource } from '@angular/material/table';
import { MatSort } from '@angular/material/sort';
import { NULL_UUID } from '@shared/models/id/has-uuid';
@Component({
selector: 'tb-gateway-statistics',
templateUrl: './gateway-statistics.component.html',
styleUrls: ['./gateway-statistics.component.scss']
})
export class GatewayStatisticsComponent extends PageComponent implements AfterViewInit {
@ViewChild(MatSort) sort: MatSort;
@ViewChild('statisticChart') statisticChart: ElementRef;
@Input()
ctx: WidgetContext;
@Input()
public general: boolean;
public isNumericData: boolean = false;
public chartInited: boolean;
private flot: TbFlot;
private flotCtx;
public statisticForm: FormGroup;
public statisticsKeys = [];
public commands = [];
public commandObj: any;
public dataSource: MatTableDataSource<any>;
public pageLink: PageLink;
private resize$: ResizeObserver;
private subscription: IWidgetSubscription;
private subscriptionInfo: SubscriptionInfo [];
public legendData: LegendData;
public displayedColumns: Array<string>;
private subscriptionOptions: WidgetSubscriptionOptions = {
callbacks: {
onDataUpdated: () => this.ctx.ngZone.run(() => {
this.onDataUpdated();
}),
onDataUpdateError: (subscription, e) => this.ctx.ngZone.run(() => {
this.onDataUpdateError(e);
})
},
useDashboardTimewindow: false,
legendConfig: {
position: LegendPosition.bottom
} as LegendConfig
};
constructor(protected router: Router,
protected store: Store<AppState>,
protected fb: FormBuilder,
protected translate: TranslateService,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
protected dialogService: DialogService,
private cd: ChangeDetectorRef,
private utils: UtilsService,
public dialog: MatDialog) {
super(store);
const sortOrder: SortOrder = {property: '0', direction: Direction.DESC};
this.pageLink = new PageLink(Number.POSITIVE_INFINITY, 0, null, sortOrder);
this.displayedColumns = ['0', '1'];
this.dataSource = new MatTableDataSource<any>([]);
this.statisticForm = this.fb.group({
statisticKey: [null, []]
})
this.statisticForm.get('statisticKey').valueChanges.subscribe(value => {
this.commandObj = null;
if (this.commands.length) {
this.commandObj = this.commands.find(command => command.attributeOnGateway === value);
}
if (this.subscriptionInfo) this.createChartsSubscription(this.ctx.defaultSubscription.datasources[0].entity, value);
})
}
ngAfterViewInit() {
this.dataSource.sort = this.sort;
this.sort.sortChange.subscribe(_=>{
this.sortData();
})
this.init();
if (this.ctx.defaultSubscription.datasources.length) {
const gateway = this.ctx.defaultSubscription.datasources[0].entity;
if (gateway.id.id === NULL_UUID) return;
if (!this.general) {
this.attributeService.getEntityAttributes(gateway.id, AttributeScope.SHARED_SCOPE, ["general_configuration"]).subscribe((resp: AttributeData[]) => {
if (resp && resp.length) {
this.commands = resp[0].value.statistics.commands;
if (!this.statisticForm.get('statisticKey').value && this.commands && this.commands.length) {
this.statisticForm.get('statisticKey').setValue(this.commands[0].attributeOnGateway);
this.createChartsSubscription(gateway, this.commands[0].attributeOnGateway);
}
}
})
} else {
let connectorsTs;
this.attributeService.getEntityTimeseriesLatest(gateway.id).subscribe(
data => {
connectorsTs = Object.keys(data)
.filter(el => el.includes(
'ConnectorEventsProduced'
) || el.includes(
'ConnectorEventsSent'))
this.createGeneralChartsSubscription(gateway, connectorsTs);
})
}
}
}
public sortData () {
this.dataSource.sortData(this.dataSource.data, this.sort);
}
public onLegendKeyHiddenChange(index: number) {
this.legendData.keys[index].dataKey.hidden = !this.legendData.keys[index].dataKey.hidden;
this.subscription.updateDataVisibility(index);
}
private createChartsSubscription(gateway: BaseData<EntityId>, attr: string) {
let subscriptionInfo = [{
type: DatasourceType.entity,
entityType: EntityType.DEVICE,
entityId: gateway.id.id,
entityName: gateway.name,
timeseries: []
}];
subscriptionInfo[0].timeseries = [{name: attr, label: attr}];
this.subscriptionInfo = subscriptionInfo;
this.changeSubscription(subscriptionInfo);
}
private createGeneralChartsSubscription(gateway: BaseData<EntityId>, attrData: [string]) {
let subscriptionInfo = [{
type: DatasourceType.entity,
entityType: EntityType.DEVICE,
entityId: gateway.id.id,
entityName: gateway.name,
timeseries: []
}];
subscriptionInfo[0].timeseries = [];
if (attrData && attrData.length) {
attrData.forEach(attr => {
subscriptionInfo[0].timeseries.push({name: attr, label: attr})
})
}
this.ctx.defaultSubscription.datasources[0].dataKeys.forEach(dataKey => {
subscriptionInfo[0].timeseries.push({name: dataKey.name, label: dataKey.label})
})
this.subscriptionInfo = subscriptionInfo;
this.changeSubscription(subscriptionInfo);
}
init = () => {
this.flotCtx = {
$scope: this.ctx.$scope,
$injector: this.ctx.$injector,
utils: this.ctx.utils,
isMobile: this.ctx.isMobile,
isEdit: this.ctx.isEdit,
subscriptionApi: this.ctx.subscriptionApi,
detectChanges: this.ctx.detectChanges,
settings: this.ctx.settings
};
}
updateChart = () => {
if (this.flot && this.ctx.defaultSubscription.data.length) {
this.flot.update();
}
}
resize = () => {
if (this.flot) {
this.flot.resize();
}
}
private reset() {
if (this.resize$) {
this.resize$.disconnect();
}
if (this.subscription) {
this.subscription.unsubscribe();
}
if (this.flot) {
this.flot.destroy();
}
}
private onDataUpdateError(e: any) {
const exceptionData = this.utils.parseException(e);
let errorText = exceptionData.name;
if (exceptionData.message) {
errorText += ': ' + exceptionData.message;
}
console.error(errorText);
}
private onDataUpdated() {
this.isDataOnlyNumbers();
if (this.isNumericData) {
if (this.chartInited) {
if (this.flot) {
this.flot.update();
}
} else {
this.initChart();
}
}
}
private initChart() {
this.chartInited = true;
this.flotCtx.$container = $(this.statisticChart.nativeElement);
this.resize$.observe(this.statisticChart.nativeElement);
this.flot = new TbFlot(this.flotCtx as WidgetContext, "line");
this.flot.update();
}
private isDataOnlyNumbers() {
if (this.general) {
this.isNumericData = true;
return;
}
this.dataSource.data = this.subscription.data.length ? this.subscription.data[0].data : [];
this.isNumericData = this.dataSource.data.every(data => !isNaN(+data[1]) );
}
changeSubscription(subscriptionInfo: SubscriptionInfo[]) {
if (this.subscription) {
this.reset();
}
if (this.ctx.datasources[0].entity) {
this.ctx.subscriptionApi.createSubscriptionFromInfo(widgetType.timeseries, subscriptionInfo, this.subscriptionOptions, false, true).subscribe(subscription => {
this.subscription = subscription;
this.isDataOnlyNumbers();
this.legendData = this.subscription.legendData;
this.flotCtx.defaultSubscription = subscription;
this.resize$ = new ResizeObserver(() => {
this.resize();
});
this.ctx.detectChanges();
if (this.isNumericData) {
this.initChart();
}
})
}
}
}

6
ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html

@ -26,14 +26,14 @@
<tb-json-object-edit
[editorStyle]="{minHeight: '100px'}"
fillHeight="true"
[required]="settings.attributeRequired"
[jsonRequired]="settings.attributeRequired"
label="{{ settings.showLabel ? labelValue : '' }}"
formControlName="currentValue"
(focusin)="isFocused = true;"
(focusout)="isFocused = false;"
></tb-json-object-edit>
</fieldset>
<div class="tb-json-input-form__actions" fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="20px">
<div class="tb-json-input__actions" fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="20px">
<button mat-button color="primary"
type="button"
[disabled]="!attributeUpdateFormGroup.dirty"
@ -42,7 +42,7 @@
matTooltipPosition="above">
{{ "action.undo" | translate }}
</button>
<button mat-button mat-raised-button color="primary"
<button mat-raised-button color="primary"
type="submit"
[disabled]="attributeUpdateFormGroup.invalid || !attributeUpdateFormGroup.dirty">
{{ "action.save" | translate }}

4
ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.scss

@ -27,6 +27,10 @@
font-size: 18px;
color: #a0a0a0;
}
&__actions {
height: 48px;
}
}
.tb-toast {

7
ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts

@ -23,13 +23,14 @@ import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { Datasource, DatasourceData, DatasourceType, WidgetConfig } from '@shared/models/widget.models';
import { IWidgetSubscription } from '@core/api/widget-api.models';
import { UntypedFormBuilder, UntypedFormGroup, ValidatorFn, Validators } from '@angular/forms';
import { UntypedFormBuilder, UntypedFormGroup, ValidatorFn } from '@angular/forms';
import { AttributeService } from '@core/http/attribute.service';
import { AttributeData, AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models';
import { EntityId } from '@shared/models/id/entity-id';
import { EntityType } from '@shared/models/entity-type.models';
import { createLabelFromDatasource, isDefinedAndNotNull } from '@core/utils';
import { Observable } from 'rxjs';
import { jsonRequired } from '@shared/components/json-object-edit.component';
enum JsonInputWidgetMode {
ATTRIBUTE = 'ATTRIBUTE',
@ -131,7 +132,7 @@ export class JsonInputWidgetComponent extends PageComponent implements OnInit {
private buildForm() {
const validators: ValidatorFn[] = [];
if (this.settings.attributeRequired) {
validators.push(Validators.required);
validators.push(jsonRequired);
}
this.attributeUpdateFormGroup = this.fb.group({
currentValue: [{}, validators]
@ -143,7 +144,7 @@ export class JsonInputWidgetComponent extends PageComponent implements OnInit {
private updateWidgetData(data: Array<DatasourceData>) {
if (!this.errorMessage) {
let value = {};
let value = null;
if (data[0].data[0][1] !== '') {
try {
value = JSON.parse(data[0].data[0][1]);

4
ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html

@ -91,7 +91,7 @@
(click)="openEditJSONDialog($event, key, source)">
<mat-icon>open_in_new</mat-icon>
</button>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required') && !multipleInputFormGroup.get(key.formId).hasError('invalidJSON')">
{{ getErrorMessageText(key.settings,'required') }}
</mat-error>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('invalidJSON')">
@ -193,7 +193,7 @@
</button>
<button mat-button mat-raised-button color="primary" type="submit"
class="tb-multiple-input--buttons-container__button"
[disabled]="!multipleInputFormGroup.dirty || multipleInputFormGroup.invalid">
[disabled]="!multipleInputFormGroup.dirty || invalid()">
{{ saveButtonLabel }}
</button>
</div>

16
ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts

@ -604,7 +604,8 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
}
public inputChanged(source: MultipleInputWidgetSource, key: MultipleInputWidgetDataKey) {
if (!this.settings.showActionButtons && !this.isSavingInProgress && this.multipleInputFormGroup.get(key.formId).valid) {
const control = this.multipleInputFormGroup.get(key.formId);
if (!this.settings.showActionButtons && !this.isSavingInProgress && (Array.isArray(control.value) || control.valid)) {
this.isSavingInProgress = true;
const dataToSave: MultipleInputWidgetSource = {
datasource: source.datasource,
@ -775,6 +776,7 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
jsonValue: formControl.value,
required: key.settings.required,
title: key.settings.dialogTitle,
saveLabel: key.settings.saveButtonLabel,
cancelLabel: key.settings.cancelButtonLabel
@ -791,4 +793,16 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
}
);
}
invalid(): boolean {
for (const source of this.sources) {
for (const key of this.visibleKeys(source)) {
const control = this.multipleInputFormGroup.get(key.formId);
if (!Array.isArray(control.value) && control.invalid) {
return true;
}
}
}
return false;
}
}

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

@ -0,0 +1,26 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<section class="tb-widget-settings" [formGroup]="GatewayLogSettingForm" fxLayout="column">
<mat-slide-toggle class="mat-slide" formControlName="isConnectorLog">
{{"widgets.gateway.is-connector" | translate}}
</mat-slide-toggle>
<mat-form-field fxFlex class="mat-block" *ngIf="GatewayLogSettingForm.get('isConnectorLog').value">
<mat-label>{{"widgets.gateway.state-param-name" | translate}}</mat-label>
<input matInput formControlName="connectorLogState">
</mat-form-field>
</section>

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

@ -0,0 +1,54 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@Component({
selector: 'tb-gateway-logs-settings',
templateUrl: './gateway-logs-settings.component.html',
styleUrls: ['../widget-settings.scss']
})
export class GatewayLogsSettingsComponent extends WidgetSettingsComponent {
GatewayLogSettingForm: UntypedFormGroup;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.GatewayLogSettingForm;
}
protected defaultSettings(): WidgetSettings {
return {
isConnectorLog: false,
connectorLogState: 'default'
};
}
protected onSettingsSet(settings: WidgetSettings) {
this.GatewayLogSettingForm = this.fb.group({
isConnectorLog: [false, []],
connectorLogState: ['default', []]
});
}
}

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

@ -0,0 +1,22 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<section class="tb-widget-settings" [formGroup]="GatewayLogSettingForm" fxLayout="column">
<mat-slide-toggle class="mat-slide" formControlName="isConnector">
{{"widgets.gateway.is-connector" | translate}}
</mat-slide-toggle>
</section>

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

@ -0,0 +1,52 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@Component({
selector: 'tb-gateway-service-rpc-settings',
templateUrl: './gateway-service-rpc-settings.component.html',
styleUrls: ['../widget-settings.scss']
})
export class GatewayServiceRPCSettingsComponent extends WidgetSettingsComponent {
GatewayLogSettingForm: UntypedFormGroup;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.GatewayLogSettingForm;
}
protected defaultSettings(): WidgetSettings {
return {
isConnector: false,
};
}
protected onSettingsSet(settings: WidgetSettings) {
this.GatewayLogSettingForm = this.fb.group({
isConnector: [false, []]
});
}
}

12
ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts

@ -257,6 +257,12 @@ import {
import {
TripAnimationPointSettingsComponent
} from '@home/components/widget/lib/settings/map/trip-animation-point-settings.component';
import {
GatewayLogsSettingsComponent
} from '@home/components/widget/lib/settings/gateway/gateway-logs-settings.component';
import {
GatewayServiceRPCSettingsComponent
} from '@home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component';
import {
DocLinksWidgetSettingsComponent
} from '@home/components/widget/lib/settings/home-page/doc-links-widget-settings.component';
@ -369,6 +375,8 @@ import {
TripAnimationPointSettingsComponent,
MapWidgetSettingsComponent,
RouteMapWidgetSettingsComponent,
GatewayLogsSettingsComponent,
GatewayServiceRPCSettingsComponent,
TripAnimationWidgetSettingsComponent,
DocLinksWidgetSettingsComponent,
QuickLinksWidgetSettingsComponent,
@ -476,6 +484,8 @@ import {
TripAnimationPointSettingsComponent,
MapWidgetSettingsComponent,
RouteMapWidgetSettingsComponent,
GatewayLogsSettingsComponent,
GatewayServiceRPCSettingsComponent,
TripAnimationWidgetSettingsComponent,
DocLinksWidgetSettingsComponent,
QuickLinksWidgetSettingsComponent,
@ -549,6 +559,8 @@ export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsCo
'tb-map-widget-settings': MapWidgetSettingsComponent,
'tb-route-map-widget-settings': RouteMapWidgetSettingsComponent,
'tb-trip-animation-widget-settings': TripAnimationWidgetSettingsComponent,
'tb-gateway-logs-settings': GatewayLogsSettingsComponent,
'tb-gateway-service-rpc-settings':GatewayServiceRPCSettingsComponent,
'tb-doc-links-widget-settings': DocLinksWidgetSettingsComponent,
'tb-quick-links-widget-settings': QuickLinksWidgetSettingsComponent,
'tb-value-card-widget-settings': ValueCardWidgetSettingsComponent,

23
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -43,6 +43,15 @@ import { HomePageWidgetsModule } from '@home/components/widget/lib/home-page/hom
import { WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
import { FlotWidgetComponent } from '@home/components/widget/lib/flot-widget.component';
import { LegendComponent } from '@home/components/widget/lib/legend.component';
import { GatewayConnectorComponent } from '@home/components/widget/lib/gateway/gateway-connectors.component';
import { GatewayLogsComponent } from '@home/components/widget/lib/gateway/gateway-logs.component';
import { GatewayStatisticsComponent } from '@home/components/widget/lib/gateway/gateway-statistics.component';
import { GatewayServiceRPCComponent } from '@home/components/widget/lib/gateway/gateway-service-rpc.component';
import { DeviceGatewayCommandComponent } from '@home/components/widget/lib/gateway/device-gateway-command.component';
import { GatewayConfigurationComponent } from '@home/components/widget/lib/gateway/gateway-configuration.component';
import {
GatewayRemoteConfigurationDialogComponent
} from '@home/components/widget/lib/gateway/gateway-remote-configuration-dialog';
import { ValueCardWidgetComponent } from '@home/components/widget/lib/cards/value-card-widget.component';
import {
AggregatedValueCardWidgetComponent
@ -71,6 +80,13 @@ import {
SelectEntityDialogComponent,
LegendComponent,
FlotWidgetComponent,
GatewayConnectorComponent,
GatewayLogsComponent,
GatewayStatisticsComponent,
GatewayServiceRPCComponent,
DeviceGatewayCommandComponent,
GatewayConfigurationComponent,
GatewayRemoteConfigurationDialogComponent,
ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent
],
@ -101,6 +117,13 @@ import {
MarkdownWidgetComponent,
LegendComponent,
FlotWidgetComponent,
GatewayConnectorComponent,
GatewayLogsComponent,
GatewayStatisticsComponent,
GatewayServiceRPCComponent,
DeviceGatewayCommandComponent,
GatewayConfigurationComponent,
GatewayRemoteConfigurationDialogComponent,
ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent
],

1
ui-ngx/src/app/modules/home/components/widget/widget-config.component.html

@ -89,7 +89,6 @@
<ng-template matExpansionPanelContent>
<tb-json-object-edit
[editorStyle]="{minHeight: '100px'}"
required
label="{{ 'widget-config.title-style' | translate }}"
formControlName="titleStyle"
></tb-json-object-edit>

1
ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts

@ -98,7 +98,6 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
}
updateForm(entity: Resource) {
this.entity.name = entity.title;
if (this.isEdit) {
this.entityForm.get('resourceType').disable({emitEvent: false});
if (entity.resourceType !== ResourceType.JS_MODULE) {

11
ui-ngx/src/app/modules/home/pages/home-pages.models.ts

@ -16,9 +16,18 @@
import { BreadCrumbLabelFunction } from '@shared/components/breadcrumb';
import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component';
import { EntityType } from '@shared/models/entity-type.models';
import { ResourceInfo } from '@shared/models/resource.models';
import { OtaPackage } from '@shared/models/ota-package.models';
export const entityDetailsPageBreadcrumbLabelFunction: BreadCrumbLabelFunction<EntityDetailsPageComponent>
= ((route, translate, component) => {
return component.entity?.name;
switch (component.entitiesTableConfig.entityType) {
case EntityType.TB_RESOURCE:
case EntityType.OTA_PACKAGE:
return (component.entity as ResourceInfo | OtaPackage)?.title;
default:
return component.entity?.name;
}
});

2
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.html

@ -22,7 +22,7 @@
class="tb-rule-node-configuration-json"
formControlName="configuration"
[label]="'rulenode.configuration' | translate"
[required]="required"
[jsonRequired]="required"
[fillHeight]="true">
</tb-json-object-edit>
</div>

3
ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html

@ -33,8 +33,7 @@
<tb-json-object-edit
formControlName="json"
label="{{ 'value.json-value' | translate }}"
validateContent="true"
[required]="true"
[jsonRequired]="required"
[fillHeight]="false">
</tb-json-object-edit>
</fieldset>

3
ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts

@ -26,6 +26,7 @@ import { isNotEmptyStr } from '@core/utils';
export interface JsonObjectEditDialogData {
jsonValue: object;
required?: boolean;
title?: string;
saveLabel?: string;
cancelLabel?: string;
@ -43,6 +44,8 @@ export class JsonObjectEditDialogComponent extends DialogComponent<JsonObjectEdi
saveButtonLabel = this.translate.instant('action.save');
cancelButtonLabel = this.translate.instant('action.cancel');
required = this.data.required === true;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: JsonObjectEditDialogData,

37
ui-ngx/src/app/shared/components/directives/tb-json-to-string.directive.ts

@ -17,19 +17,23 @@
import { Directive, ElementRef, forwardRef, HostListener, Renderer2, SkipSelf } from '@angular/core';
import {
ControlValueAccessor,
UntypedFormControl,
FormGroupDirective,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
NgForm,
UntypedFormControl,
ValidationErrors,
Validator
} from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material/core';
import { isObject } from "@core/utils";
import { isObject } from '@core/utils';
@Directive({
selector: '[tb-json-to-string]',
// eslint-disable-next-line @angular-eslint/no-host-metadata-property
host: {
'(blur)': 'onTouched()'
},
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TbJsonToStringDirective),
@ -48,18 +52,26 @@ import { isObject } from "@core/utils";
export class TbJsonToStringDirective implements ControlValueAccessor, Validator, ErrorStateMatcher {
private propagateChange = null;
public onTouched = () => {};
private parseError: boolean;
private data: any;
@HostListener('input', ['$event.target.value']) input(newValue: any): void {
try {
this.data = JSON.parse(newValue);
if (isObject(this.data)) {
this.parseError = false;
if (newValue) {
this.data = JSON.parse(newValue);
if (isObject(this.data)) {
this.parseError = false;
} else {
this.data = null;
this.parseError = true;
}
} else {
this.parseError = true;
this.data = null;
this.parseError = false;
}
} catch (e) {
this.data = null;
this.parseError = true;
}
@ -73,9 +85,7 @@ export class TbJsonToStringDirective implements ControlValueAccessor, Validator,
}
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.parseError);
return originalErrorState || customErrorState;
return !!(control && control.invalid && !Array.isArray(control.value) && control.touched);
}
validate(c: UntypedFormControl): ValidationErrors {
@ -87,11 +97,9 @@ export class TbJsonToStringDirective implements ControlValueAccessor, Validator,
}
writeValue(obj: any): void {
if (obj) {
this.data = obj;
this.parseError = false;
this.render.setProperty(this.element.nativeElement, 'value', JSON.stringify(obj));
}
this.data = obj;
this.parseError = false;
this.render.setProperty(this.element.nativeElement, 'value', obj ? JSON.stringify(obj) : '');
}
registerOnChange(fn: any): void {
@ -99,5 +107,6 @@ export class TbJsonToStringDirective implements ControlValueAccessor, Validator,
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
}

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

Loading…
Cancel
Save