Browse Source

Merge with master

pull/10210/head
Andrii Shvaika 2 years ago
parent
commit
decbbcb9cd
  1. 3
      application/src/main/data/json/system/widget_bundles/buttons.json
  2. 3
      application/src/main/data/json/system/widget_bundles/control_widgets.json
  3. 36
      application/src/main/data/json/system/widget_types/power_button.json
  4. 3
      application/src/main/data/json/system/widget_types/progress_bar.json
  5. 3
      application/src/main/data/json/system/widget_types/simple_value_and_chart_card.json
  6. 39
      application/src/main/data/json/system/widget_types/slider.json
  7. 36
      application/src/main/data/json/tenant/dashboards/gateways.json
  8. 13
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
  9. 2
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
  10. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  11. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java
  12. 1
      application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java
  13. 2
      application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java
  14. 28
      application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
  15. 2
      application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java
  16. 8
      application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java
  17. 38
      application/src/main/java/org/thingsboard/server/service/ws/notification/sub/AbstractNotificationSubscription.java
  18. 6
      application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java
  19. 3
      application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java
  20. 37
      application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java
  21. 23
      application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java
  22. 7
      application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java
  23. 87
      application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java
  24. 4
      application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java
  25. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java
  26. 15
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java
  27. 8
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java
  28. 24
      dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java
  29. 4
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java
  30. 5
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
  31. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java
  32. 67
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java
  33. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNodeConfiguration.java
  34. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoader.java
  35. 23
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/GpsGeofencingEvents.java
  36. 39
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/GpsGeofencingActionTestCase.java
  37. 259
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNodeTest.java
  38. 18
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoaderTest.java
  39. 1
      ui-ngx/package.json
  40. 1
      ui-ngx/src/app/core/api/widget-api.models.ts
  41. 37
      ui-ngx/src/app/core/http/image.service.ts
  42. 1
      ui-ngx/src/app/core/notification/notification.models.ts
  43. 8
      ui-ngx/src/app/core/services/item-buffer.service.ts
  44. 30
      ui-ngx/src/app/modules/common/modules-map.ts
  45. 8
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts
  46. 2
      ui-ngx/src/app/modules/home/components/filter/filters-edit.component.ts
  47. 2
      ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.html
  48. 15
      ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.scss
  49. 6
      ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html
  50. 10
      ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.scss
  51. 6
      ui-ngx/src/app/modules/home/components/public-api.ts
  52. 4
      ui-ngx/src/app/modules/home/components/vc/repository-settings.component.html
  53. 5
      ui-ngx/src/app/modules/home/components/vc/repository-settings.component.ts
  54. 6
      ui-ngx/src/app/modules/home/components/vc/version-control.component.html
  55. 16
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  56. 197
      ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.html
  57. 195
      ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.ts
  58. 4
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.html
  59. 50
      ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.html
  60. 6
      ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.ts
  61. 4
      ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html
  62. 271
      ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.html
  63. 309
      ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.ts
  64. 17
      ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.models.ts
  65. 31
      ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.scss
  66. 6
      ui-ngx/src/app/modules/home/components/widget/lib/button/action-button-widget.component.html
  67. 6
      ui-ngx/src/app/modules/home/components/widget/lib/button/command-button-widget.component.html
  68. 2
      ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts
  69. 8
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts
  70. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html
  71. 7
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.scss
  72. 3
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts
  73. 63
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts
  74. 31
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.models.ts
  75. 23
      ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts
  76. 3
      ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts
  77. 26
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.html
  78. 64
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.scss
  79. 203
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.ts
  80. 953
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts
  81. 6
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html
  82. 59
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.html
  83. 135
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.scss
  84. 347
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.ts
  85. 196
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.models.ts
  86. 142
      ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.html
  87. 88
      ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.ts
  88. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.html
  89. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.html
  90. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.ts
  91. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings.component.ts
  92. 19
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html
  93. 10
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts
  94. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-appearance.component.html
  95. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html
  96. 216
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slider-widget-settings.component.html
  97. 186
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slider-widget-settings.component.ts
  98. 46
      ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.html
  99. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.ts
  100. 18
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts

3
application/src/main/data/json/system/widget_bundles/buttons.json

@ -9,6 +9,7 @@
}, },
"widgetTypeFqns": [ "widgetTypeFqns": [
"action_button", "action_button",
"command_button" "command_button",
"power_button"
] ]
} }

3
application/src/main/data/json/system/widget_bundles/control_widgets.json

@ -9,6 +9,9 @@
}, },
"widgetTypeFqns": [ "widgetTypeFqns": [
"single_switch", "single_switch",
"command_button",
"power_button",
"slider",
"control_widgets.switch_control", "control_widgets.switch_control",
"control_widgets.slide_toggle_control", "control_widgets.slide_toggle_control",
"control_widgets.round_switch", "control_widgets.round_switch",

36
application/src/main/data/json/system/widget_types/power_button.json

File diff suppressed because one or more lines are too long

3
application/src/main/data/json/system/widget_types/progress_bar.json

File diff suppressed because one or more lines are too long

3
application/src/main/data/json/system/widget_types/simple_value_and_chart_card.json

File diff suppressed because one or more lines are too long

39
application/src/main/data/json/system/widget_types/slider.json

File diff suppressed because one or more lines are too long

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

@ -2317,7 +2317,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "45e4507d-3adc-bb31-8b2b-1ba09bbd56ac" "id": "45e4507d-3adc-bb31-8b2b-1ba09bbd56ac"
@ -2484,7 +2484,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "852eccce-98eb-24db-c783-bdd62566f906" "id": "852eccce-98eb-24db-c783-bdd62566f906"
@ -2650,7 +2650,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "3c31ba62-e760-2bea-4c8d-d32784a86c24" "id": "3c31ba62-e760-2bea-4c8d-d32784a86c24"
@ -2816,7 +2816,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "4b55ea81-93bf-4206-9166-3e0bdc1dd9f3" "id": "4b55ea81-93bf-4206-9166-3e0bdc1dd9f3"
@ -2982,7 +2982,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "babf88d0-a118-e2b5-f10e-3a5970c8a65b" "id": "babf88d0-a118-e2b5-f10e-3a5970c8a65b"
@ -3148,7 +3148,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "94de7690-f91d-b032-6771-85af99abd749" "id": "94de7690-f91d-b032-6771-85af99abd749"
@ -3314,7 +3314,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "18414f44-1c65-536a-14de-eaf21a7d56bd" "id": "18414f44-1c65-536a-14de-eaf21a7d56bd"
@ -3480,7 +3480,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "794974da-c9d2-a9f7-be47-c9eb642094e8" "id": "794974da-c9d2-a9f7-be47-c9eb642094e8"
@ -3646,7 +3646,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "2add705b-3e53-8559-8126-380cac686fb0" "id": "2add705b-3e53-8559-8126-380cac686fb0"
@ -3812,7 +3812,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "7e1ba820-9992-d52a-579b-20485abb3926" "id": "7e1ba820-9992-d52a-579b-20485abb3926"
@ -3978,7 +3978,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "91af27c1-b37c-2276-6022-a332e41b2b33" "id": "91af27c1-b37c-2276-6022-a332e41b2b33"
@ -4144,7 +4144,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "26cf8696-054b-13ec-7984-6fc5df20e6f1" "id": "26cf8696-054b-13ec-7984-6fc5df20e6f1"
@ -4310,7 +4310,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "1dcfaf24-32be-cd19-62d6-86d12cc6a7ef" "id": "1dcfaf24-32be-cd19-62d6-86d12cc6a7ef"
@ -4476,7 +4476,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "ad2bc817-f3c4-150c-4672-8fe0c38aee8d" "id": "ad2bc817-f3c4-150c-4672-8fe0c38aee8d"
@ -4642,7 +4642,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "d1ad84cd-bd9c-4dca-e4a0-f444ae8598bd" "id": "d1ad84cd-bd9c-4dca-e4a0-f444ae8598bd"
@ -4808,7 +4808,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "bf80eef9-b879-9a08-40a4-488dbdefa125" "id": "bf80eef9-b879-9a08-40a4-488dbdefa125"
@ -4974,7 +4974,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "b5a406b3-cc0a-8a09-9aec-3f8befae5fb8" "id": "b5a406b3-cc0a-8a09-9aec-3f8befae5fb8"
@ -5140,7 +5140,7 @@
"useShowWidgetActionFunction": null, "useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;", "showWidgetActionFunction": "return true;",
"type": "custom", "type": "custom",
"customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "ec1dfba3-4b43-2491-8948-f602337f8a3b" "id": "ec1dfba3-4b43-2491-8948-f602337f8a3b"

13
application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java

@ -92,7 +92,9 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToTransportUpdateCredentialsProto; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportUpdateCredentialsProto;
import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg;
import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto;
import org.thingsboard.server.gen.transport.TransportProtos.UplinkNotificationMsg;
import org.thingsboard.server.service.rpc.RpcSubmitStrategy; import org.thingsboard.server.service.rpc.RpcSubmitStrategy;
import org.thingsboard.server.service.state.DefaultDeviceStateService;
import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper; import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper;
import jakarta.annotation.Nullable; import jakarta.annotation.Nullable;
@ -175,7 +177,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
private EdgeId findRelatedEdgeId() { private EdgeId findRelatedEdgeId() {
List<EntityRelation> result = List<EntityRelation> result =
systemContext.getRelationService().findByToAndType(tenantId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON); systemContext.getRelationService().findByToAndType(tenantId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE);
if (result != null && result.size() > 0) { if (result != null && result.size() > 0) {
EntityRelation relationToEdge = result.get(0); EntityRelation relationToEdge = result.get(0);
if (relationToEdge.getFrom() != null && relationToEdge.getFrom().getId() != null) { if (relationToEdge.getFrom() != null && relationToEdge.getFrom().getId() != null) {
@ -214,8 +216,11 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
if (systemContext.isEdgesEnabled() && edgeId != null) { if (systemContext.isEdgesEnabled() && edgeId != null) {
log.debug("[{}][{}] device is related to edge: [{}]. Saving RPC request: [{}][{}] to edge queue", tenantId, deviceId, edgeId.getId(), rpcId, requestId); log.debug("[{}][{}] device is related to edge: [{}]. Saving RPC request: [{}][{}] to edge queue", tenantId, deviceId, edgeId.getId(), rpcId, requestId);
try { try {
saveRpcRequestToEdgeQueue(request, requestId).get(); if (systemContext.getEdgeService().isEdgeActiveAsync(tenantId, edgeId, DefaultDeviceStateService.ACTIVITY_STATE).get()) {
sent = true; saveRpcRequestToEdgeQueue(request, requestId).get();
} else {
log.error("[{}][{}][{}] Failed to save RPC request to edge queue {}. The Edge is currently offline or unreachable", tenantId, deviceId, edgeId.getId(), request);
}
} catch (InterruptedException | ExecutionException e) { } catch (InterruptedException | ExecutionException e) {
log.error("[{}][{}][{}] Failed to save RPC request to edge queue {}", tenantId, deviceId, edgeId.getId(), request, e); log.error("[{}][{}][{}] Failed to save RPC request to edge queue {}", tenantId, deviceId, edgeId.getId(), request, e);
} }
@ -472,7 +477,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
callback.onSuccess(); callback.onSuccess();
} }
private void processUplinkNotificationMsg(SessionInfoProto sessionInfo, TransportProtos.UplinkNotificationMsg uplinkNotificationMsg) { private void processUplinkNotificationMsg(SessionInfoProto sessionInfo, UplinkNotificationMsg uplinkNotificationMsg) {
String nodeId = sessionInfo.getNodeId(); String nodeId = sessionInfo.getNodeId();
sessions.entrySet().stream() sessions.entrySet().stream()
.filter(kv -> kv.getValue().getSessionInfo().getNodeId().equals(nodeId) && (kv.getValue().isSubscribedToAttributes() || kv.getValue().isSubscribedToRPC())) .filter(kv -> kv.getValue().getSessionInfo().getNodeId().equals(nodeId) && (kv.getValue().isSubscribedToAttributes() || kv.getValue().isSubscribedToRPC()))

2
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java

@ -16,7 +16,6 @@
package org.thingsboard.server.actors.ruleChain; package org.thingsboard.server.actors.ruleChain;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorCtx;
import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.actors.TbActorRef;
@ -29,6 +28,7 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleState; import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java

@ -478,6 +478,8 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
TbMsgMetaData md = new TbMsgMetaData(); TbMsgMetaData md = new TbMsgMetaData();
if (!persistToTelemetry) { if (!persistToTelemetry) {
md.putValue(DataConstants.SCOPE, DataConstants.SERVER_SCOPE); md.putValue(DataConstants.SCOPE, DataConstants.SERVER_SCOPE);
md.putValue("edgeName", edge.getName());
md.putValue("edgeType", edge.getType());
} }
TbMsg tbMsg = TbMsg.newMsg(msgType, edgeId, md, TbMsgDataType.JSON, data); TbMsg tbMsg = TbMsg.newMsg(msgType, edgeId, md, TbMsgDataType.JSON, data);
clusterService.pushMsgToRuleEngine(tenantId, edgeId, tbMsg, null); clusterService.pushMsgToRuleEngine(tenantId, edgeId, tbMsg, null);

5
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.edge.rpc.processor.rule;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChain;
@ -53,6 +54,10 @@ public class RuleChainEdgeProcessor extends BaseEdgeProcessor {
isRoot = Boolean.parseBoolean(edgeEvent.getBody().get(EDGE_IS_ROOT_BODY_KEY).asText()); isRoot = Boolean.parseBoolean(edgeEvent.getBody().get(EDGE_IS_ROOT_BODY_KEY).asText());
} catch (Exception ignored) {} } catch (Exception ignored) {}
} }
if (!isRoot) {
Edge edge = edgeService.findEdgeById(edgeEvent.getTenantId(), edgeEvent.getEdgeId());
isRoot = edge.getRootRuleChainId().equals(ruleChainId);
}
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
RuleChainUpdateMsg ruleChainUpdateMsg = ((RuleChainMsgConstructor) RuleChainUpdateMsg ruleChainUpdateMsg = ((RuleChainMsgConstructor)
ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion))

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

@ -24,7 +24,6 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.RuleEngineException;
import org.thingsboard.server.common.msg.queue.RuleNodeInfo; import org.thingsboard.server.common.msg.queue.RuleNodeInfo;
import org.thingsboard.server.common.msg.queue.TbMsgCallback; import org.thingsboard.server.common.msg.queue.TbMsgCallback;
import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;

2
application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java

@ -36,8 +36,6 @@ public abstract class TbSubscription<T> {
private final TbSubscriptionType type; private final TbSubscriptionType type;
private final BiConsumer<TbSubscription<T>, T> updateProcessor; private final BiConsumer<TbSubscription<T>, T> updateProcessor;
protected final AtomicInteger sequence = new AtomicInteger();
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

28
application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java

@ -259,13 +259,13 @@ public class DefaultWebSocketService implements WebSocketService {
} }
@Override @Override
public void sendUpdate(String sessionId, TelemetrySubscriptionUpdate update) { public void sendUpdate(String sessionId, int cmdId, TelemetrySubscriptionUpdate update) {
sendUpdate(sessionId, update.getSubscriptionId(), update); doSendUpdate(sessionId, cmdId, update);
} }
@Override @Override
public void sendUpdate(String sessionId, CmdUpdate update) { public void sendUpdate(String sessionId, CmdUpdate update) {
sendUpdate(sessionId, update.getCmdId(), update); doSendUpdate(sessionId, update.getCmdId(), update);
} }
@Override @Override
@ -274,7 +274,7 @@ public class DefaultWebSocketService implements WebSocketService {
sendUpdate(sessionRef, update); sendUpdate(sessionRef, update);
} }
private <T> void sendUpdate(String sessionId, int cmdId, T update) { private <T> void doSendUpdate(String sessionId, int cmdId, T update) {
WsSessionMetaData md = wsSessionsMap.get(sessionId); WsSessionMetaData md = wsSessionsMap.get(sessionId);
if (md != null) { if (md != null) {
sendUpdate(md.getSessionRef(), cmdId, update); sendUpdate(md.getSessionRef(), cmdId, update);
@ -288,7 +288,7 @@ public class DefaultWebSocketService implements WebSocketService {
try { try {
msgEndpoint.close(md.getSessionRef(), status); msgEndpoint.close(md.getSessionRef(), status);
} catch (IOException e) { } catch (IOException e) {
log.warn("[{}] Failed to send session close: {}", sessionId, e); log.warn("[{}] Failed to send session close", sessionId, e);
} }
} }
} }
@ -439,7 +439,7 @@ public class DefaultWebSocketService implements WebSocketService {
TbAttributeSubscription sub = TbAttributeSubscription.builder() TbAttributeSubscription sub = TbAttributeSubscription.builder()
.serviceId(serviceId) .serviceId(serviceId)
.sessionId(sessionId) .sessionId(sessionId)
.subscriptionId(cmd.getCmdId()) .subscriptionId(sessionRef.getSessionSubIdSeq().incrementAndGet())
.tenantId(sessionRef.getSecurityCtx().getTenantId()) .tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityId) .entityId(entityId)
.queryTs(queryTs) .queryTs(queryTs)
@ -449,7 +449,7 @@ public class DefaultWebSocketService implements WebSocketService {
.updateProcessor((subscription, update) -> { .updateProcessor((subscription, update) -> {
subLock.lock(); subLock.lock();
try { try {
sendUpdate(subscription.getSessionId(), update); sendUpdate(subscription.getSessionId(), cmd.getCmdId(), update);
} finally { } finally {
subLock.unlock(); subLock.unlock();
} }
@ -545,7 +545,7 @@ public class DefaultWebSocketService implements WebSocketService {
TbAttributeSubscription sub = TbAttributeSubscription.builder() TbAttributeSubscription sub = TbAttributeSubscription.builder()
.serviceId(serviceId) .serviceId(serviceId)
.sessionId(sessionId) .sessionId(sessionId)
.subscriptionId(cmd.getCmdId()) .subscriptionId(sessionRef.getSessionSubIdSeq().incrementAndGet())
.tenantId(sessionRef.getSecurityCtx().getTenantId()) .tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityId) .entityId(entityId)
.queryTs(queryTs) .queryTs(queryTs)
@ -554,7 +554,7 @@ public class DefaultWebSocketService implements WebSocketService {
.updateProcessor((subscription, update) -> { .updateProcessor((subscription, update) -> {
subLock.lock(); subLock.lock();
try { try {
sendUpdate(subscription.getSessionId(), update); sendUpdate(subscription.getSessionId(), cmd.getCmdId(), update);
} finally { } finally {
subLock.unlock(); subLock.unlock();
} }
@ -643,13 +643,13 @@ public class DefaultWebSocketService implements WebSocketService {
TbTimeSeriesSubscription sub = TbTimeSeriesSubscription.builder() TbTimeSeriesSubscription sub = TbTimeSeriesSubscription.builder()
.serviceId(serviceId) .serviceId(serviceId)
.sessionId(sessionId) .sessionId(sessionId)
.subscriptionId(cmd.getCmdId()) .subscriptionId(sessionRef.getSessionSubIdSeq().incrementAndGet())
.tenantId(sessionRef.getSecurityCtx().getTenantId()) .tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityId) .entityId(entityId)
.updateProcessor((subscription, update) -> { .updateProcessor((subscription, update) -> {
subLock.lock(); subLock.lock();
try { try {
sendUpdate(subscription.getSessionId(), update); sendUpdate(subscription.getSessionId(), cmd.getCmdId(), update);
} finally { } finally {
subLock.unlock(); subLock.unlock();
} }
@ -698,13 +698,13 @@ public class DefaultWebSocketService implements WebSocketService {
TbTimeSeriesSubscription sub = TbTimeSeriesSubscription.builder() TbTimeSeriesSubscription sub = TbTimeSeriesSubscription.builder()
.serviceId(serviceId) .serviceId(serviceId)
.sessionId(sessionId) .sessionId(sessionId)
.subscriptionId(cmd.getCmdId()) .subscriptionId(sessionRef.getSessionSubIdSeq().incrementAndGet())
.tenantId(sessionRef.getSecurityCtx().getTenantId()) .tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityId) .entityId(entityId)
.updateProcessor((subscription, update) -> { .updateProcessor((subscription, update) -> {
subLock.lock(); subLock.lock();
try { try {
sendUpdate(subscription.getSessionId(), update); sendUpdate(subscription.getSessionId(), cmd.getCmdId(), update);
} finally { } finally {
subLock.unlock(); subLock.unlock();
} }
@ -836,7 +836,7 @@ public class DefaultWebSocketService implements WebSocketService {
try { try {
msgEndpoint.sendPing(md.getSessionRef(), currentTime); msgEndpoint.sendPing(md.getSessionRef(), currentTime);
} catch (IOException e) { } catch (IOException e) {
log.warn("[{}] Failed to send ping: {}", md.getSessionRef().getSessionId(), e); log.warn("[{}] Failed to send ping:", md.getSessionRef().getSessionId(), e);
} }
})); }));
} }

2
application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java

@ -29,7 +29,7 @@ public interface WebSocketService {
void handleCommands(WebSocketSessionRef sessionRef, WsCommandsWrapper commandsWrapper); void handleCommands(WebSocketSessionRef sessionRef, WsCommandsWrapper commandsWrapper);
void sendUpdate(String sessionId, TelemetrySubscriptionUpdate update); void sendUpdate(String sessionId, int cmdId, TelemetrySubscriptionUpdate update);
void sendUpdate(String sessionId, CmdUpdate update); void sendUpdate(String sessionId, CmdUpdate update);

8
application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java

@ -115,7 +115,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
private void fetchUnreadNotificationsCount(NotificationsCountSubscription subscription) { private void fetchUnreadNotificationsCount(NotificationsCountSubscription subscription) {
log.trace("[{}, subId: {}] Fetching unread notifications count from DB", subscription.getSessionId(), subscription.getSubscriptionId()); log.trace("[{}, subId: {}] Fetching unread notifications count from DB", subscription.getSessionId(), subscription.getSubscriptionId());
int unreadCount = notificationService.countUnreadNotificationsByRecipientId(subscription.getTenantId(), (UserId) subscription.getEntityId()); int unreadCount = notificationService.countUnreadNotificationsByRecipientId(subscription.getTenantId(), (UserId) subscription.getEntityId());
subscription.getUnreadCounter().set(unreadCount); subscription.getTotalUnreadCounter().set(unreadCount);
} }
@ -196,20 +196,20 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
private void handleNotificationUpdate(NotificationsCountSubscription subscription, NotificationUpdate update) { private void handleNotificationUpdate(NotificationsCountSubscription subscription, NotificationUpdate update) {
log.trace("[{}, subId: {}] Handling notification update for count sub: {}", subscription.getSessionId(), subscription.getSubscriptionId(), update); log.trace("[{}, subId: {}] Handling notification update for count sub: {}", subscription.getSessionId(), subscription.getSubscriptionId(), update);
if (update.isCreated()) { if (update.isCreated()) {
subscription.getUnreadCounter().incrementAndGet(); subscription.getTotalUnreadCounter().incrementAndGet();
sendUpdate(subscription.getSessionId(), subscription.createUpdate()); sendUpdate(subscription.getSessionId(), subscription.createUpdate());
} else if (update.isUpdated()) { } else if (update.isUpdated()) {
if (update.getNewStatus() == NotificationStatus.READ) { if (update.getNewStatus() == NotificationStatus.READ) {
if (update.isAllNotifications()) { if (update.isAllNotifications()) {
fetchUnreadNotificationsCount(subscription); fetchUnreadNotificationsCount(subscription);
} else { } else {
subscription.getUnreadCounter().decrementAndGet(); subscription.getTotalUnreadCounter().decrementAndGet();
} }
sendUpdate(subscription.getSessionId(), subscription.createUpdate()); sendUpdate(subscription.getSessionId(), subscription.createUpdate());
} }
} else if (update.isDeleted()) { } else if (update.isDeleted()) {
if (update.getNotification().getStatus() != NotificationStatus.READ) { if (update.getNotification().getStatus() != NotificationStatus.READ) {
subscription.getUnreadCounter().decrementAndGet(); subscription.getTotalUnreadCounter().decrementAndGet();
sendUpdate(subscription.getSessionId(), subscription.createUpdate()); sendUpdate(subscription.getSessionId(), subscription.createUpdate());
} }
} }

38
application/src/main/java/org/thingsboard/server/service/ws/notification/sub/AbstractNotificationSubscription.java

@ -0,0 +1,38 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.ws.notification.sub;
import lombok.Getter;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.service.subscription.TbSubscription;
import org.thingsboard.server.service.subscription.TbSubscriptionType;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
@Getter
public abstract class AbstractNotificationSubscription<T> extends TbSubscription<T> {
protected final AtomicInteger sequence = new AtomicInteger();
protected final AtomicInteger totalUnreadCounter = new AtomicInteger();
public AbstractNotificationSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId, TbSubscriptionType type, BiConsumer<TbSubscription<T>, T> updateProcessor) {
super(serviceId, sessionId, subscriptionId, tenantId, entityId, type, updateProcessor);
}
}

6
application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java

@ -27,9 +27,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
@Getter @Getter
public class NotificationsCountSubscription extends TbSubscription<NotificationsSubscriptionUpdate> { public class NotificationsCountSubscription extends AbstractNotificationSubscription<NotificationsSubscriptionUpdate> {
private final AtomicInteger unreadCounter = new AtomicInteger();
@Builder @Builder
public NotificationsCountSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId, public NotificationsCountSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId,
@ -40,7 +38,7 @@ public class NotificationsCountSubscription extends TbSubscription<Notifications
public UnreadNotificationsCountUpdate createUpdate() { public UnreadNotificationsCountUpdate createUpdate() {
return UnreadNotificationsCountUpdate.builder() return UnreadNotificationsCountUpdate.builder()
.cmdId(getSubscriptionId()) .cmdId(getSubscriptionId())
.totalUnreadCount(unreadCounter.get()) .totalUnreadCount(totalUnreadCounter.get())
.sequenceNumber(sequence.incrementAndGet()) .sequenceNumber(sequence.incrementAndGet())
.build(); .build();
} }

3
application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java

@ -35,11 +35,10 @@ import java.util.function.BiConsumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Getter @Getter
public class NotificationsSubscription extends TbSubscription<NotificationsSubscriptionUpdate> { public class NotificationsSubscription extends AbstractNotificationSubscription<NotificationsSubscriptionUpdate> {
private final Map<UUID, Notification> latestUnreadNotifications = new HashMap<>(); private final Map<UUID, Notification> latestUnreadNotifications = new HashMap<>();
private final int limit; private final int limit;
private final AtomicInteger totalUnreadCounter = new AtomicInteger();
@Builder @Builder
public NotificationsSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId, public NotificationsSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId,

37
application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java

@ -710,7 +710,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses()); Assert.assertTrue(edgeImitator.waitForResponses());
Assert.assertTrue(onUpdateCallback.getSubscribeLatch().await(30, TimeUnit.SECONDS)); Assert.assertTrue(onUpdateCallback.getSubscribeLatch().await(TIMEOUT, TimeUnit.SECONDS));
Assert.assertEquals(JacksonUtil.newObjectNode().put(attrKey, attrValue), Assert.assertEquals(JacksonUtil.newObjectNode().put(attrKey, attrValue),
JacksonUtil.fromBytes(onUpdateCallback.getPayloadBytes())); JacksonUtil.fromBytes(onUpdateCallback.getPayloadBytes()));
@ -797,7 +797,21 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
// clean up stored edge events // clean up stored edge events
edgeEventService.cleanupEvents(1); edgeEventService.cleanupEvents(1);
// perform rpc call to verify edgeId in DeviceActorMessageProcessor updated properly // edge is disconnected: perform rpc call - no edge event saved
doPostAsync(
"/api/rpc/oneway/" + device.getId().getId().toString(),
JacksonUtil.toString(createDefaultRpc()),
String.class,
status().isOk());
Awaitility.await()
.atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> {
PageData<EdgeEvent> result = edgeEventService.findEdgeEvents(tenantId, tmpEdge.getId(), 0L, null, new TimePageLink(1));
return result.getTotalElements() == 0;
});
// edge is connected: perform rpc call to verify edgeId in DeviceActorMessageProcessor updated properly
simulateEdgeActivation(tmpEdge);
doPostAsync( doPostAsync(
"/api/rpc/oneway/" + device.getId().getId().toString(), "/api/rpc/oneway/" + device.getId().getId().toString(),
JacksonUtil.toString(createDefaultRpc()), JacksonUtil.toString(createDefaultRpc()),
@ -856,4 +870,23 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
return rpc; return rpc;
} }
private void simulateEdgeActivation(Edge edge) throws Exception {
ObjectNode attributes = JacksonUtil.newObjectNode();
attributes.put("active", true);
doPost("/api/plugins/telemetry/EDGE/" + edge.getId() + "/attributes/" + DataConstants.SERVER_SCOPE, attributes);
Awaitility.await()
.atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> {
List<Map<String, Object>> values = doGetAsyncTyped("/api/plugins/telemetry/EDGE/" + edge.getId() +
"/values/attributes/SERVER_SCOPE", new TypeReference<>() {});
Optional<Map<String, Object>> activeAttrOpt = values.stream().filter(att -> att.get("key").equals("active")).findFirst();
if (activeAttrOpt.isEmpty()) {
return false;
}
Map<String, Object> activeAttr = activeAttrOpt.get();
return "true".equals(activeAttr.get("value").toString());
});
}
} }

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

@ -146,7 +146,7 @@ public class RuleChainEdgeTest extends AbstractEdgeTest {
} }
} }
private void createRuleChainMetadata(RuleChain ruleChain) { private RuleChainMetaData createRuleChainMetadata(RuleChain ruleChain) {
RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); RuleChainMetaData ruleChainMetaData = new RuleChainMetaData();
ruleChainMetaData.setRuleChainId(ruleChain.getId()); ruleChainMetaData.setRuleChainId(ruleChain.getId());
@ -182,7 +182,7 @@ public class RuleChainEdgeTest extends AbstractEdgeTest {
ruleChainMetaData.addConnectionInfo(0, 2, "fail"); ruleChainMetaData.addConnectionInfo(0, 2, "fail");
ruleChainMetaData.addConnectionInfo(1, 2, "success"); ruleChainMetaData.addConnectionInfo(1, 2, "success");
doPost("/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class); return doPost("/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class);
} }
@Test @Test
@ -193,9 +193,10 @@ public class RuleChainEdgeTest extends AbstractEdgeTest {
ruleChain.setType(RuleChainType.EDGE); ruleChain.setType(RuleChainType.EDGE);
RuleChain savedRuleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class); RuleChain savedRuleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class);
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(2);
doPost("/api/edge/" + edge.getUuidId() doPost("/api/edge/" + edge.getUuidId()
+ "/ruleChain/" + savedRuleChain.getUuidId(), RuleChain.class); + "/ruleChain/" + savedRuleChain.getUuidId(), RuleChain.class);
RuleChainMetaData metaData = createRuleChainMetadata(savedRuleChain);
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
// set new rule chain as root // set new rule chain as root
@ -213,6 +214,22 @@ public class RuleChainEdgeTest extends AbstractEdgeTest {
Assert.assertTrue(ruleChainMsg.isRoot()); Assert.assertTrue(ruleChainMsg.isRoot());
Assert.assertEquals(savedRuleChain.getId(), ruleChainMsg.getId()); Assert.assertEquals(savedRuleChain.getId(), ruleChainMsg.getId());
// update metadata for root rule chain
edgeImitator.expectMessageAmount(1);
metaData.getNodes().forEach(n -> n.setDebugMode(true));
doPost("/api/ruleChain/metadata", metaData, RuleChainMetaData.class);
Assert.assertTrue(edgeImitator.waitForMessages());
ruleChainUpdateMsgOpt = edgeImitator.findMessageByType(RuleChainUpdateMsg.class);
Assert.assertTrue(ruleChainUpdateMsgOpt.isPresent());
ruleChainUpdateMsg = ruleChainUpdateMsgOpt.get();
ruleChainMsg = JacksonUtil.fromString(ruleChainUpdateMsg.getEntity(), RuleChain.class, true);
Assert.assertNotNull(ruleChainMsg);
Assert.assertTrue(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(ruleChainUpdateMsg.getMsgType()) ||
UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE.equals(ruleChainUpdateMsg.getMsgType()));
Assert.assertEquals(savedRuleChain.getId(), ruleChainMsg.getId());
Assert.assertEquals(savedRuleChain.getName(), ruleChainMsg.getName());
Assert.assertTrue(ruleChainMsg.isRoot());
// revert root rule chain // revert root rule chain
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(1);
doPost("/api/edge/" + edge.getUuidId() doPost("/api/edge/" + edge.getUuidId()

7
application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java

@ -26,6 +26,7 @@ import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import org.thingsboard.edge.rpc.EdgeGrpcClient; import org.thingsboard.edge.rpc.EdgeGrpcClient;
import org.thingsboard.edge.rpc.EdgeRpcClient; import org.thingsboard.edge.rpc.EdgeRpcClient;
import org.thingsboard.server.controller.AbstractWebTest;
import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg; import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
@ -72,8 +73,6 @@ import java.util.stream.Collectors;
@Slf4j @Slf4j
public class EdgeImitator { public class EdgeImitator {
public static final int TIMEOUT_IN_SECONDS = 30;
private String routingKey; private String routingKey;
private String routingSecret; private String routingSecret;
@ -344,7 +343,7 @@ public class EdgeImitator {
} }
public boolean waitForMessages() throws InterruptedException { public boolean waitForMessages() throws InterruptedException {
return waitForMessages(TIMEOUT_IN_SECONDS); return waitForMessages(AbstractWebTest.TIMEOUT);
} }
public boolean waitForMessages(int timeoutInSeconds) throws InterruptedException { public boolean waitForMessages(int timeoutInSeconds) throws InterruptedException {
@ -359,7 +358,7 @@ public class EdgeImitator {
} }
public boolean waitForResponses() throws InterruptedException { public boolean waitForResponses() throws InterruptedException {
return responsesLatch.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS); return responsesLatch.await(AbstractWebTest.TIMEOUT, TimeUnit.SECONDS);
} }
public void expectResponsesAmount(int messageAmount) { public void expectResponsesAmount(int messageAmount) {

87
application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.service.edge.rpc.constructor; package org.thingsboard.server.service.edge.rpc.constructor;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.junit.Assert; import org.junit.Assert;
@ -61,7 +60,7 @@ public class RuleChainMsgConstructorTest {
} }
@Test @Test
public void testConstructRuleChainMetadataUpdatedMsg_V_3_4_0() throws JsonProcessingException { public void testConstructRuleChainMetadataUpdatedMsg_V_3_4_0() {
RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID()); RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID());
RuleChainMetaData ruleChainMetaData = createRuleChainMetaData( RuleChainMetaData ruleChainMetaData = createRuleChainMetaData(
ruleChainId, 3, createRuleNodes(ruleChainId), createConnections()); ruleChainId, 3, createRuleNodes(ruleChainId), createConnections());
@ -80,7 +79,7 @@ public class RuleChainMsgConstructorTest {
} }
@Test @Test
public void testConstructRuleChainMetadataUpdatedMsg_V_3_3_3() throws JsonProcessingException { public void testConstructRuleChainMetadataUpdatedMsg_V_3_3_3() {
RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID()); RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID());
RuleChainMetaData ruleChainMetaData = createRuleChainMetaData( RuleChainMetaData ruleChainMetaData = createRuleChainMetaData(
ruleChainId, 3, createRuleNodes(ruleChainId), createConnections()); ruleChainId, 3, createRuleNodes(ruleChainId), createConnections());
@ -120,7 +119,7 @@ public class RuleChainMsgConstructorTest {
} }
@Test @Test
public void testConstructRuleChainMetadataUpdatedMsg_V_3_3_0() throws JsonProcessingException { public void testConstructRuleChainMetadataUpdatedMsg_V_3_3_0() {
RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID()); RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID());
RuleChainMetaData ruleChainMetaData = createRuleChainMetaData(ruleChainId, 3, createRuleNodes(ruleChainId), createConnections()); RuleChainMetaData ruleChainMetaData = createRuleChainMetaData(ruleChainId, 3, createRuleNodes(ruleChainId), createConnections());
RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg =
@ -161,7 +160,7 @@ public class RuleChainMsgConstructorTest {
} }
@Test @Test
public void testConstructRuleChainMetadataUpdatedMsg_V_3_3_0_inDifferentOrder() throws JsonProcessingException { public void testConstructRuleChainMetadataUpdatedMsg_V_3_3_0_inDifferentOrder() {
// same rule chain metadata, but different order of rule nodes // same rule chain metadata, but different order of rule nodes
RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID()); RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID());
RuleChainMetaData ruleChainMetaData1 = createRuleChainMetaData(ruleChainId, 8, createRuleNodesInDifferentOrder(ruleChainId), createConnectionsInDifferentOrder()); RuleChainMetaData ruleChainMetaData1 = createRuleChainMetaData(ruleChainId, 8, createRuleNodesInDifferentOrder(ruleChainId), createConnectionsInDifferentOrder());
@ -254,7 +253,7 @@ public class RuleChainMsgConstructorTest {
return result; return result;
} }
private List<RuleNode> createRuleNodes(RuleChainId ruleChainId) throws JsonProcessingException { private List<RuleNode> createRuleNodes(RuleChainId ruleChainId) {
List<RuleNode> result = new ArrayList<>(); List<RuleNode> result = new ArrayList<>();
result.add(getOutputNode(ruleChainId)); result.add(getOutputNode(ruleChainId));
result.add(getAcknowledgeNode(ruleChainId)); result.add(getAcknowledgeNode(ruleChainId));
@ -301,7 +300,7 @@ public class RuleChainMsgConstructorTest {
return result; return result;
} }
private List<RuleNode> createRuleNodesInDifferentOrder(RuleChainId ruleChainId) throws JsonProcessingException { private List<RuleNode> createRuleNodesInDifferentOrder(RuleChainId ruleChainId) {
List<RuleNode> result = new ArrayList<>(); List<RuleNode> result = new ArrayList<>();
result.add(getPushToAnalyticsNode(ruleChainId)); result.add(getPushToAnalyticsNode(ruleChainId));
result.add(getPushToCloudNode(ruleChainId)); result.add(getPushToCloudNode(ruleChainId));
@ -319,99 +318,99 @@ public class RuleChainMsgConstructorTest {
} }
private RuleNode getOutputNode(RuleChainId ruleChainId) throws JsonProcessingException { private RuleNode getOutputNode(RuleChainId ruleChainId) {
return createRuleNode(ruleChainId, return createRuleNode(ruleChainId,
"org.thingsboard.rule.engine.flow.TbRuleChainOutputNode", "org.thingsboard.rule.engine.flow.TbRuleChainOutputNode",
"Output node", "Output node",
JacksonUtil.OBJECT_MAPPER.readTree("{\"version\":0}"), JacksonUtil.toJsonNode("{\"version\":0}"),
JacksonUtil.OBJECT_MAPPER.readTree("{\"description\":\"\",\"layoutX\":178,\"layoutY\":592}")); JacksonUtil.toJsonNode("{\"description\":\"\",\"layoutX\":178,\"layoutY\":592}"));
} }
private RuleNode getCheckpointNode(RuleChainId ruleChainId) throws JsonProcessingException { private RuleNode getCheckpointNode(RuleChainId ruleChainId) {
return createRuleNode(ruleChainId, return createRuleNode(ruleChainId,
"org.thingsboard.rule.engine.flow.TbCheckpointNode", "org.thingsboard.rule.engine.flow.TbCheckpointNode",
"Checkpoint node", "Checkpoint node",
JacksonUtil.OBJECT_MAPPER.readTree("{\"queueName\":\"HighPriority\"}"), JacksonUtil.toJsonNode("{\"queueName\":\"HighPriority\"}"),
JacksonUtil.OBJECT_MAPPER.readTree("{\"description\":\"\",\"layoutX\":178,\"layoutY\":647}")); JacksonUtil.toJsonNode("{\"description\":\"\",\"layoutX\":178,\"layoutY\":647}"));
} }
private RuleNode getSaveTimeSeriesNode(RuleChainId ruleChainId) throws JsonProcessingException { private RuleNode getSaveTimeSeriesNode(RuleChainId ruleChainId) {
return createRuleNode(ruleChainId, return createRuleNode(ruleChainId,
"org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode",
"Save Timeseries", "Save Timeseries",
JacksonUtil.OBJECT_MAPPER.readTree("{\"defaultTTL\":0}"), JacksonUtil.toJsonNode("{\"defaultTTL\":0}"),
JacksonUtil.OBJECT_MAPPER.readTree("{\"layoutX\":823,\"layoutY\":157}")); JacksonUtil.toJsonNode("{\"layoutX\":823,\"layoutY\":157}"));
} }
private RuleNode getMessageTypeSwitchNode(RuleChainId ruleChainId) throws JsonProcessingException { private RuleNode getMessageTypeSwitchNode(RuleChainId ruleChainId) {
return createRuleNode(ruleChainId, return createRuleNode(ruleChainId,
"org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode",
"Message Type Switch", "Message Type Switch",
JacksonUtil.OBJECT_MAPPER.readTree("{\"version\":0}"), JacksonUtil.toJsonNode("{\"version\":0}"),
JacksonUtil.OBJECT_MAPPER.readTree("{\"layoutX\":347,\"layoutY\":149}")); JacksonUtil.toJsonNode("{\"layoutX\":347,\"layoutY\":149}"));
} }
private RuleNode getLogOtherNode(RuleChainId ruleChainId) throws JsonProcessingException { private RuleNode getLogOtherNode(RuleChainId ruleChainId) {
return createRuleNode(ruleChainId, return createRuleNode(ruleChainId,
"org.thingsboard.rule.engine.action.TbLogNode", "org.thingsboard.rule.engine.action.TbLogNode",
"Log Other", "Log Other",
JacksonUtil.OBJECT_MAPPER.readTree("{\"jsScript\":\"return '\\\\nIncoming message:\\\\n' + JSON.stringify(msg) + '\\\\nIncoming metadata:\\\\n' + JSON.stringify(metadata);\"}"), JacksonUtil.toJsonNode("{\"jsScript\":\"return '\\\\nIncoming message:\\\\n' + JSON.stringify(msg) + '\\\\nIncoming metadata:\\\\n' + JSON.stringify(metadata);\"}"),
JacksonUtil.OBJECT_MAPPER.readTree("{\"layoutX\":824,\"layoutY\":378}")); JacksonUtil.toJsonNode("{\"layoutX\":824,\"layoutY\":378}"));
} }
private RuleNode getPushToCloudNode(RuleChainId ruleChainId) throws JsonProcessingException { private RuleNode getPushToCloudNode(RuleChainId ruleChainId) {
return createRuleNode(ruleChainId, return createRuleNode(ruleChainId,
"org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode", "org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode",
"Push to cloud", "Push to cloud",
JacksonUtil.OBJECT_MAPPER.readTree("{\"scope\":\"SERVER_SCOPE\"}"), JacksonUtil.toJsonNode("{\"scope\":\"SERVER_SCOPE\"}"),
JacksonUtil.OBJECT_MAPPER.readTree("{\"layoutX\":1129,\"layoutY\":52}")); JacksonUtil.toJsonNode("{\"layoutX\":1129,\"layoutY\":52}"));
} }
private RuleNode getAcknowledgeNode(RuleChainId ruleChainId) throws JsonProcessingException { private RuleNode getAcknowledgeNode(RuleChainId ruleChainId) {
return createRuleNode(ruleChainId, return createRuleNode(ruleChainId,
"org.thingsboard.rule.engine.flow.TbAckNode", "org.thingsboard.rule.engine.flow.TbAckNode",
"Acknowledge node", "Acknowledge node",
JacksonUtil.OBJECT_MAPPER.readTree("{\"version\":0}"), JacksonUtil.toJsonNode("{\"version\":0}"),
JacksonUtil.OBJECT_MAPPER.readTree("{\"description\":\"\",\"layoutX\":177,\"layoutY\":703}")); JacksonUtil.toJsonNode("{\"description\":\"\",\"layoutX\":177,\"layoutY\":703}"));
} }
private RuleNode getDeviceProfileNode(RuleChainId ruleChainId) throws JsonProcessingException { private RuleNode getDeviceProfileNode(RuleChainId ruleChainId) {
return createRuleNode(ruleChainId, return createRuleNode(ruleChainId,
"org.thingsboard.rule.engine.profile.TbDeviceProfileNode", "org.thingsboard.rule.engine.profile.TbDeviceProfileNode",
"Device Profile Node", "Device Profile Node",
JacksonUtil.OBJECT_MAPPER.readTree("{\"persistAlarmRulesState\":false,\"fetchAlarmRulesStateOnStart\":false}"), JacksonUtil.toJsonNode("{\"persistAlarmRulesState\":false,\"fetchAlarmRulesStateOnStart\":false}"),
JacksonUtil.OBJECT_MAPPER.readTree("{\"description\":\"Process incoming messages from devices with the alarm rules defined in the device profile. Dispatch all incoming messages with \\\"Success\\\" relation type.\",\"layoutX\":187,\"layoutY\":468}")); JacksonUtil.toJsonNode("{\"description\":\"Process incoming messages from devices with the alarm rules defined in the device profile. Dispatch all incoming messages with \\\"Success\\\" relation type.\",\"layoutX\":187,\"layoutY\":468}"));
} }
private RuleNode getSaveClientAttributesNode(RuleChainId ruleChainId) throws JsonProcessingException { private RuleNode getSaveClientAttributesNode(RuleChainId ruleChainId) {
return createRuleNode(ruleChainId, return createRuleNode(ruleChainId,
"org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode",
"Save Client Attributes", "Save Client Attributes",
JacksonUtil.OBJECT_MAPPER.readTree("{\"scope\":\"CLIENT_SCOPE\"}"), JacksonUtil.toJsonNode("{\"scope\":\"CLIENT_SCOPE\"}"),
JacksonUtil.OBJECT_MAPPER.readTree("{\"layoutX\":824,\"layoutY\":52}")); JacksonUtil.toJsonNode("{\"layoutX\":824,\"layoutY\":52}"));
} }
private RuleNode getLogRpcFromDeviceNode(RuleChainId ruleChainId) throws JsonProcessingException { private RuleNode getLogRpcFromDeviceNode(RuleChainId ruleChainId) {
return createRuleNode(ruleChainId, return createRuleNode(ruleChainId,
"org.thingsboard.rule.engine.action.TbLogNode", "org.thingsboard.rule.engine.action.TbLogNode",
"Log RPC from Device", "Log RPC from Device",
JacksonUtil.OBJECT_MAPPER.readTree("{\"jsScript\":\"return '\\\\nIncoming message:\\\\n' + JSON.stringify(msg) + '\\\\nIncoming metadata:\\\\n' + JSON.stringify(metadata);\"}"), JacksonUtil.toJsonNode("{\"jsScript\":\"return '\\\\nIncoming message:\\\\n' + JSON.stringify(msg) + '\\\\nIncoming metadata:\\\\n' + JSON.stringify(metadata);\"}"),
JacksonUtil.OBJECT_MAPPER.readTree("{\"layoutX\":825,\"layoutY\":266}")); JacksonUtil.toJsonNode("{\"layoutX\":825,\"layoutY\":266}"));
} }
private RuleNode getRpcCallRequestNode(RuleChainId ruleChainId) throws JsonProcessingException { private RuleNode getRpcCallRequestNode(RuleChainId ruleChainId) {
return createRuleNode(ruleChainId, return createRuleNode(ruleChainId,
"org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode",
"RPC Call Request", "RPC Call Request",
JacksonUtil.OBJECT_MAPPER.readTree("{\"timeoutInSeconds\":60}"), JacksonUtil.toJsonNode("{\"timeoutInSeconds\":60}"),
JacksonUtil.OBJECT_MAPPER.readTree("{\"layoutX\":824,\"layoutY\":466}")); JacksonUtil.toJsonNode("{\"layoutX\":824,\"layoutY\":466}"));
} }
private RuleNode getPushToAnalyticsNode(RuleChainId ruleChainId) throws JsonProcessingException { private RuleNode getPushToAnalyticsNode(RuleChainId ruleChainId) {
return createRuleNode(ruleChainId, return createRuleNode(ruleChainId,
"org.thingsboard.rule.engine.flow.TbRuleChainInputNode", "org.thingsboard.rule.engine.flow.TbRuleChainInputNode",
"Push to Analytics", "Push to Analytics",
JacksonUtil.OBJECT_MAPPER.readTree("{\"ruleChainId\":\"af588000-6c7c-11ec-bafd-c9a47a5c8d99\"}"), JacksonUtil.toJsonNode("{\"ruleChainId\":\"af588000-6c7c-11ec-bafd-c9a47a5c8d99\"}"),
JacksonUtil.OBJECT_MAPPER.readTree("{\"description\":\"\",\"layoutX\":477,\"layoutY\":560}")); JacksonUtil.toJsonNode("{\"description\":\"\",\"layoutX\":477,\"layoutY\":560}"));
} }
} }

4
application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java

@ -55,6 +55,7 @@ import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.dao.widget.WidgetsBundleService;
@ -208,6 +209,9 @@ public abstract class BaseEdgeProcessorTest {
@MockBean @MockBean
protected AttributesService attributesService; protected AttributesService attributesService;
@MockBean
protected TimeseriesService timeseriesService;
@MockBean @MockBean
protected TbClusterService tbClusterService; protected TbClusterService tbClusterService;

2
common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java

@ -91,4 +91,6 @@ public interface EdgeService extends EntityDaoService {
PageData<EdgeId> findRelatedEdgeIdsByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink); PageData<EdgeId> findRelatedEdgeIdsByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink);
String findMissingToRelatedRuleChains(TenantId tenantId, EdgeId edgeId, String tbRuleChainInputNodeClassName); String findMissingToRelatedRuleChains(TenantId tenantId, EdgeId edgeId, String tbRuleChainInputNodeClassName);
ListenableFuture<Boolean> isEdgeActiveAsync(TenantId tenantId, EdgeId edgeId, String activityState);
} }

15
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java

@ -222,7 +222,7 @@ public class DefaultCoapClientContext implements CoapClientContext {
private void onUplink(TbCoapClientState client, boolean notifyOtherServers, long uplinkTs) { private void onUplink(TbCoapClientState client, boolean notifyOtherServers, long uplinkTs) {
PowerMode powerMode = client.getPowerMode(); PowerMode powerMode = client.getPowerMode();
PowerSavingConfiguration profileSettings = null; PowerSavingConfiguration profileSettings = null;
if (powerMode == null) { if (powerMode == null && client.getProfileId() != null) {
var clientProfile = getProfile(client.getProfileId()); var clientProfile = getProfile(client.getProfileId());
if (clientProfile.isPresent()) { if (clientProfile.isPresent()) {
profileSettings = clientProfile.get().getClientSettings(); profileSettings = clientProfile.get().getClientSettings();
@ -736,7 +736,7 @@ public class DefaultCoapClientContext implements CoapClientContext {
private boolean isDownlinkAllowed(TbCoapClientState client) { private boolean isDownlinkAllowed(TbCoapClientState client) {
PowerMode powerMode = client.getPowerMode(); PowerMode powerMode = client.getPowerMode();
PowerSavingConfiguration profileSettings = null; PowerSavingConfiguration profileSettings = null;
if (powerMode == null) { if (powerMode == null && client.getProfileId() != null) {
var clientProfile = getProfile(client.getProfileId()); var clientProfile = getProfile(client.getProfileId());
if (clientProfile.isPresent()) { if (clientProfile.isPresent()) {
profileSettings = clientProfile.get().getClientSettings(); profileSettings = clientProfile.get().getClientSettings();
@ -785,11 +785,12 @@ public class DefaultCoapClientContext implements CoapClientContext {
private PowerMode getPowerMode(TbCoapClientState client) { private PowerMode getPowerMode(TbCoapClientState client) {
PowerMode powerMode = client.getPowerMode(); PowerMode powerMode = client.getPowerMode();
if (powerMode == null) { if (powerMode == null) {
Optional<CoapDeviceProfileTransportConfiguration> deviceProfile = getProfile(client.getProfileId()); powerMode = PowerMode.PSM;
if (deviceProfile.isPresent()) { if (client.getProfileId() != null) {
powerMode = deviceProfile.get().getClientSettings().getPowerMode(); Optional<CoapDeviceProfileTransportConfiguration> deviceProfile = getProfile(client.getProfileId());
} else { if (deviceProfile.isPresent()) {
powerMode = PowerMode.PSM; powerMode = deviceProfile.get().getClientSettings().getPowerMode();
}
} }
} }
return powerMode; return powerMode;

8
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java

@ -411,7 +411,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
public boolean isDownlinkAllowed(LwM2mClient client) { public boolean isDownlinkAllowed(LwM2mClient client) {
PowerMode powerMode = client.getPowerMode(); PowerMode powerMode = client.getPowerMode();
OtherConfiguration profileSettings = null; OtherConfiguration profileSettings = null;
if (powerMode == null) { if (powerMode == null && client.getProfileId() != null) {
var clientProfile = getProfile(client.getProfileId()); var clientProfile = getProfile(client.getProfileId());
profileSettings = clientProfile.getClientLwM2mSettings(); profileSettings = clientProfile.getClientLwM2mSettings();
powerMode = profileSettings.getPowerMode(); powerMode = profileSettings.getPowerMode();
@ -419,7 +419,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
powerMode = PowerMode.DRX; powerMode = PowerMode.DRX;
} }
} }
if (PowerMode.DRX.equals(powerMode) || otaUpdateService.isOtaDownloading(client)) { if (powerMode == null || PowerMode.DRX.equals(powerMode) || otaUpdateService.isOtaDownloading(client)) {
return true; return true;
} }
client.lock(); client.lock();
@ -460,7 +460,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
public void onUplink(LwM2mClient client) { public void onUplink(LwM2mClient client) {
PowerMode powerMode = client.getPowerMode(); PowerMode powerMode = client.getPowerMode();
OtherConfiguration profileSettings = null; OtherConfiguration profileSettings = null;
if (powerMode == null) { if (powerMode == null && client.getProfileId() != null) {
var clientProfile = getProfile(client.getProfileId()); var clientProfile = getProfile(client.getProfileId());
profileSettings = clientProfile.getClientLwM2mSettings(); profileSettings = clientProfile.getClientLwM2mSettings();
powerMode = profileSettings.getPowerMode(); powerMode = profileSettings.getPowerMode();
@ -468,7 +468,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
powerMode = PowerMode.DRX; powerMode = PowerMode.DRX;
} }
} }
if (PowerMode.DRX.equals(powerMode)) { if (powerMode == null || PowerMode.DRX.equals(powerMode)) {
client.updateLastUplinkTime(); client.updateLastUplinkTime();
return; return;
} }

24
dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java

@ -30,6 +30,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener; import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
@ -47,6 +48,7 @@ import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageDataIterableByTenantIdEntityId; import org.thingsboard.server.common.data.page.PageDataIterableByTenantIdEntityId;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
@ -54,6 +56,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
@ -64,6 +67,7 @@ import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.Validator;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import jakarta.annotation.Nullable; import jakarta.annotation.Nullable;
@ -103,12 +107,20 @@ public class EdgeServiceImpl extends AbstractCachedEntityService<EdgeCacheKey, E
@Autowired @Autowired
private RelationService relationService; private RelationService relationService;
@Autowired
private TimeseriesService timeseriesService;
@Autowired
private AttributesService attributesService;
@Autowired @Autowired
private DataValidator<Edge> edgeValidator; private DataValidator<Edge> edgeValidator;
@Value("${edges.enabled}") @Value("${edges.enabled}")
@Getter @Getter
private boolean edgesEnabled; private boolean edgesEnabled;
@Value("${edges.state.persistToTelemetry:false}")
private boolean persistToTelemetry;
@TransactionalEventListener(classes = EdgeCacheEvictEvent.class) @TransactionalEventListener(classes = EdgeCacheEvictEvent.class)
@Override @Override
@ -529,6 +541,18 @@ public class EdgeServiceImpl extends AbstractCachedEntityService<EdgeCacheKey, E
return result.toString(); return result.toString();
} }
@Override
public ListenableFuture<Boolean> isEdgeActiveAsync(TenantId tenantId, EdgeId edgeId, String key) {
ListenableFuture<? extends Optional<? extends KvEntry>> futureKvEntry;
if (persistToTelemetry) {
futureKvEntry = timeseriesService.findLatest(tenantId, edgeId, key);
} else {
futureKvEntry = attributesService.find(tenantId, edgeId, DataConstants.SERVER_SCOPE, key);
}
return Futures.transformAsync(futureKvEntry, kvEntryOpt ->
Futures.immediateFuture(kvEntryOpt.flatMap(KvEntry::getBooleanValue).orElse(false)), MoreExecutors.directExecutor());
}
private List<RuleChain> findEdgeRuleChains(TenantId tenantId, EdgeId edgeId) { private List<RuleChain> findEdgeRuleChains(TenantId tenantId, EdgeId edgeId) {
List<RuleChain> result = new ArrayList<>(); List<RuleChain> result = new ArrayList<>();
PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE); PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE);

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

@ -40,9 +40,9 @@ import org.thingsboard.server.common.data.notification.rule.trigger.config.Alarm
import org.thingsboard.server.common.data.notification.rule.trigger.config.ApiUsageLimitNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.ApiUsageLimitNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.DeviceActivityNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.DeviceActivityNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.DeviceActivityNotificationRuleTriggerConfig.DeviceEvent; import org.thingsboard.server.common.data.notification.rule.trigger.config.DeviceActivityNotificationRuleTriggerConfig.DeviceEvent;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeCommunicationFailureNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeConnectionNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeConnectionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeConnectionNotificationRuleTriggerConfig.EdgeConnectivityEvent; import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeConnectionNotificationRuleTriggerConfig.EdgeConnectivityEvent;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeCommunicationFailureNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EntitiesLimitNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.EntitiesLimitNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EntityActionNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.EntityActionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NewPlatformVersionNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.NewPlatformVersionNotificationRuleTriggerConfig;
@ -347,7 +347,7 @@ public class DefaultNotifications {
public static final DefaultNotification edgeCommunicationFailures = DefaultNotification.builder() public static final DefaultNotification edgeCommunicationFailures = DefaultNotification.builder()
.name("Edge communication failure notification") .name("Edge communication failure notification")
.type(NotificationType.EDGE_COMMUNICATION_FAILURE) .type(NotificationType.EDGE_COMMUNICATION_FAILURE)
.subject("Edge '${edgeName}' communication failure occured") .subject("Edge '${edgeName}' communication failure occurred")
.text("Failure message: '${failureMsg}'") .text("Failure message: '${failureMsg}'")
.icon("error").color(RED_COLOR) .icon("error").color(RED_COLOR)
.button("Go to Edge").link("/edgeManagement/instances/${edgeId}") .button("Go to Edge").link("/edgeManagement/instances/${edgeId}")

5
rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java

@ -525,12 +525,11 @@ public class RestClient implements Closeable {
} }
public PageData<AlarmCommentInfo> getAlarmComments(AlarmId alarmId, PageLink pageLink) { public PageData<AlarmCommentInfo> getAlarmComments(AlarmId alarmId, PageLink pageLink) {
String urlSecondPart = "/api/alarm/{alarmId}/comment";
Map<String, String> params = new HashMap<>(); Map<String, String> params = new HashMap<>();
params.put("alarmId", alarmId.getId().toString()); params.put("alarmId", alarmId.getId().toString());
addPageLinkToParam(params, pageLink);
return restTemplate.exchange( return restTemplate.exchange(
baseURL + urlSecondPart + "&" + getUrlParams(pageLink), baseURL + "/api/alarm/{alarmId}/comment?" + getUrlParams(pageLink),
HttpMethod.GET, HttpMethod.GET,
HttpEntity.EMPTY, HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<AlarmCommentInfo>>() { new ParameterizedTypeReference<PageData<AlarmCommentInfo>>() {

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java

@ -47,6 +47,7 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.INACTIVITY_EVENT;
import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST; import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST;
import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST;
import static org.thingsboard.server.common.data.msg.TbMsgType.TIMESERIES_UPDATED; import static org.thingsboard.server.common.data.msg.TbMsgType.TIMESERIES_UPDATED;
import static org.thingsboard.server.common.data.msg.TbMsgType.TO_SERVER_RPC_REQUEST;
@Slf4j @Slf4j
public abstract class AbstractTbMsgPushNode<T extends BaseTbMsgPushNodeConfiguration, S, U> implements TbNode { public abstract class AbstractTbMsgPushNode<T extends BaseTbMsgPushNodeConfiguration, S, U> implements TbNode {
@ -176,6 +177,6 @@ public abstract class AbstractTbMsgPushNode<T extends BaseTbMsgPushNodeConfigura
protected boolean isSupportedMsgType(TbMsg msg) { protected boolean isSupportedMsgType(TbMsg msg) {
return msg.isTypeOneOf(POST_TELEMETRY_REQUEST, POST_ATTRIBUTES_REQUEST, ATTRIBUTES_UPDATED, ATTRIBUTES_DELETED, TIMESERIES_UPDATED, return msg.isTypeOneOf(POST_TELEMETRY_REQUEST, POST_ATTRIBUTES_REQUEST, ATTRIBUTES_UPDATED, ATTRIBUTES_DELETED, TIMESERIES_UPDATED,
ALARM, CONNECT_EVENT, DISCONNECT_EVENT, ACTIVITY_EVENT, INACTIVITY_EVENT); ALARM, CONNECT_EVENT, DISCONNECT_EVENT, ACTIVITY_EVENT, INACTIVITY_EVENT, TO_SERVER_RPC_REQUEST);
} }
} }

67
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java

@ -15,6 +15,8 @@
*/ */
package org.thingsboard.rule.engine.geo; package org.thingsboard.rule.engine.geo;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
@ -29,6 +31,7 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import java.util.Collections; import java.util.Collections;
@ -40,6 +43,11 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.ENTERED;
import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.INSIDE;
import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.LEFT;
import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.OUTSIDE;
/** /**
* Created by ashvayka on 19.01.18. * Created by ashvayka on 19.01.18.
*/ */
@ -47,15 +55,24 @@ import java.util.concurrent.TimeoutException;
@RuleNode( @RuleNode(
type = ComponentType.ACTION, type = ComponentType.ACTION,
name = "gps geofencing events", name = "gps geofencing events",
version = 1,
configClazz = TbGpsGeofencingActionNodeConfiguration.class, configClazz = TbGpsGeofencingActionNodeConfiguration.class,
relationTypes = {"Success", "Entered", "Left", "Inside", "Outside"}, relationTypes = {"Success", "Entered", "Left", "Inside", "Outside"},
nodeDescription = "Produces incoming messages using GPS based geofencing", nodeDescription = "Produces incoming messages using GPS based geofencing",
nodeDetails = "Extracts latitude and longitude parameters from incoming message and returns different events based on configuration parameters", nodeDetails = "Extracts latitude and longitude parameters from incoming message and returns different events based on configuration parameters. " +
"<br><br>" +
"If an object with coordinates extracted from incoming message enters the geofence, sends a message with the type <code>Entered</code>. " +
"If an object leaves the geofence, sends a message with the type <code>Left</code>. " +
"If the presence monitoring strategy <b>\"On first message\"</b> is selected, sends messages via rule node connection type <code>Inside</code> or <code>Outside</code> only the first time the geofencing and duration conditions are satisfied; otherwise sends messages via rule node connection type <code>Success</code>. " +
"If the presence monitoring strategy <b>\"On each message\"</b> is selected, sends messages via rule node connection type <code>Inside</code> or <code>Outside</code> every time the geofencing condition is satisfied. " +
"<br><br>" +
"Output connections: <code>Entered</code>, <code>Left</code>, <code>Inside</code>, <code>Outside</code>, <code>Success</code>",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeGpsGeofencingConfig" configDirective = "tbActionNodeGpsGeofencingConfig"
) )
public class TbGpsGeofencingActionNode extends AbstractGeofencingNode<TbGpsGeofencingActionNodeConfiguration> { public class TbGpsGeofencingActionNode extends AbstractGeofencingNode<TbGpsGeofencingActionNodeConfiguration> {
private static final String REPORT_PRESENCE_STATUS_ON_EACH_MESSAGE = "reportPresenceStatusOnEachMessage";
private final Map<EntityId, EntityGeofencingState> entityStates = new HashMap<>(); private final Map<EntityId, EntityGeofencingState> entityStates = new HashMap<>();
private final Gson gson = new Gson(); private final Gson gson = new Gson();
private final JsonParser parser = new JsonParser(); private final JsonParser parser = new JsonParser();
@ -81,25 +98,32 @@ public class TbGpsGeofencingActionNode extends AbstractGeofencingNode<TbGpsGeofe
} }
}); });
boolean told = false;
if (entityState.getStateSwitchTime() == 0L || entityState.isInside() != matches) { if (entityState.getStateSwitchTime() == 0L || entityState.isInside() != matches) {
switchState(ctx, msg.getOriginator(), entityState, matches, ts); switchState(ctx, msg.getOriginator(), entityState, matches, ts);
ctx.tellNext(msg, matches ? "Entered" : "Left"); ctx.tellNext(msg, matches ? ENTERED : LEFT);
told = true; return;
} else { }
if (!entityState.isStayed()) {
long stayTime = ts - entityState.getStateSwitchTime(); if (config.isReportPresenceStatusOnEachMessage()) {
if (stayTime > (entityState.isInside() ? ctx.tellNext(msg, entityState.isInside() ? INSIDE : OUTSIDE);
TimeUnit.valueOf(config.getMinInsideDurationTimeUnit()).toMillis(config.getMinInsideDuration()) : TimeUnit.valueOf(config.getMinOutsideDurationTimeUnit()).toMillis(config.getMinOutsideDuration()))) { return;
setStaid(ctx, msg.getOriginator(), entityState);
ctx.tellNext(msg, entityState.isInside() ? "Inside" : "Outside");
told = true;
}
}
} }
if (!told) {
if (entityState.isStayed()) {
ctx.tellSuccess(msg); ctx.tellSuccess(msg);
return;
}
long stayTime = ts - entityState.getStateSwitchTime();
if (stayTime > (entityState.isInside() ?
TimeUnit.valueOf(config.getMinInsideDurationTimeUnit()).toMillis(config.getMinInsideDuration()) :
TimeUnit.valueOf(config.getMinOutsideDurationTimeUnit()).toMillis(config.getMinOutsideDuration()))) {
setStaid(ctx, msg.getOriginator(), entityState);
ctx.tellNext(msg, entityState.isInside() ? INSIDE : OUTSIDE);
return;
} }
ctx.tellSuccess(msg);
} }
private void switchState(TbContext ctx, EntityId entityId, EntityGeofencingState entityState, boolean matches, long ts) { private void switchState(TbContext ctx, EntityId entityId, EntityGeofencingState entityState, boolean matches, long ts) {
@ -128,4 +152,17 @@ public class TbGpsGeofencingActionNode extends AbstractGeofencingNode<TbGpsGeofe
protected Class<TbGpsGeofencingActionNodeConfiguration> getConfigClazz() { protected Class<TbGpsGeofencingActionNodeConfiguration> getConfigClazz() {
return TbGpsGeofencingActionNodeConfiguration.class; return TbGpsGeofencingActionNodeConfiguration.class;
} }
@Override
public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
boolean hasChanges = false;
if (fromVersion == 0) {
if (!oldConfiguration.has(REPORT_PRESENCE_STATUS_ON_EACH_MESSAGE)) {
hasChanges = true;
((ObjectNode) oldConfiguration).put(REPORT_PRESENCE_STATUS_ON_EACH_MESSAGE, false);
}
}
return new TbPair<>(hasChanges, oldConfiguration);
}
} }

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNodeConfiguration.java

@ -31,6 +31,8 @@ public class TbGpsGeofencingActionNodeConfiguration extends TbGpsGeofencingFilte
private String minInsideDurationTimeUnit; private String minInsideDurationTimeUnit;
private String minOutsideDurationTimeUnit; private String minOutsideDurationTimeUnit;
private boolean reportPresenceStatusOnEachMessage;
@Override @Override
public TbGpsGeofencingActionNodeConfiguration defaultConfiguration() { public TbGpsGeofencingActionNodeConfiguration defaultConfiguration() {
TbGpsGeofencingActionNodeConfiguration configuration = new TbGpsGeofencingActionNodeConfiguration(); TbGpsGeofencingActionNodeConfiguration configuration = new TbGpsGeofencingActionNodeConfiguration();
@ -43,6 +45,7 @@ public class TbGpsGeofencingActionNodeConfiguration extends TbGpsGeofencingFilte
configuration.setMinOutsideDurationTimeUnit(TimeUnit.MINUTES.name()); configuration.setMinOutsideDurationTimeUnit(TimeUnit.MINUTES.name());
configuration.setMinInsideDuration(1); configuration.setMinInsideDuration(1);
configuration.setMinOutsideDuration(1); configuration.setMinOutsideDuration(1);
configuration.setReportPresenceStatusOnEachMessage(true);
return configuration; return configuration;
} }
} }

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoader.java

@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
@ -63,6 +64,9 @@ public class EntitiesFieldsAsyncLoader {
case ENTITY_VIEW: case ENTITY_VIEW:
return toEntityFieldsDataAsync(ctx.getEntityViewService().findEntityViewByIdAsync(ctx.getTenantId(), (EntityViewId) originatorId), return toEntityFieldsDataAsync(ctx.getEntityViewService().findEntityViewByIdAsync(ctx.getTenantId(), (EntityViewId) originatorId),
EntityFieldsData::new, ctx); EntityFieldsData::new, ctx);
case EDGE:
return toEntityFieldsDataAsync(ctx.getEdgeService().findEdgeByIdAsync(ctx.getTenantId(), (EdgeId) originatorId),
EntityFieldsData::new, ctx);
default: default:
return Futures.immediateFailedFuture(new TbNodeException("Unexpected originator EntityType: " + originatorId.getEntityType())); return Futures.immediateFailedFuture(new TbNodeException("Unexpected originator EntityType: " + originatorId.getEntityType()));
} }

23
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/GpsGeofencingEvents.java

@ -0,0 +1,23 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.util;
public class GpsGeofencingEvents {
public static final String ENTERED = "Entered";
public static final String INSIDE = "Inside";
public static final String LEFT = "Left";
public static final String OUTSIDE = "Outside";
}

39
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/GpsGeofencingActionTestCase.java

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.geo;
import lombok.Data;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.HashMap;
import java.util.Map;
@Data
public class GpsGeofencingActionTestCase {
private EntityId entityId;
private Map<EntityId, EntityGeofencingState> entityStates;
private boolean msgInside;
private boolean reportPresenceStatusOnEachMessage;
public GpsGeofencingActionTestCase(EntityId entityId, boolean msgInside, boolean reportPresenceStatusOnEachMessage, EntityGeofencingState entityGeofencingState) {
this.entityId = entityId;
this.msgInside = msgInside;
this.reportPresenceStatusOnEachMessage = reportPresenceStatusOnEachMessage;
this.entityStates = new HashMap<>();
this.entityStates.put(entityId, entityGeofencingState);
}
}

259
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNodeTest.java

@ -0,0 +1,259 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.geo;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
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.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.attributes.AttributesService;
import java.time.Duration;
import java.util.UUID;
import java.util.stream.Stream;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.ENTERED;
import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.INSIDE;
import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.LEFT;
import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.OUTSIDE;
import static org.thingsboard.server.common.data.msg.TbNodeConnectionType.SUCCESS;
@ExtendWith(MockitoExtension.class)
class TbGpsGeofencingActionNodeTest extends AbstractRuleNodeUpgradeTest {
@Mock
private TbContext ctx;
@Mock
private AttributesService attributesService;
private TbGpsGeofencingActionNode node;
@BeforeEach
void setUp() {
node = spy(new TbGpsGeofencingActionNode());
}
@AfterEach
void tearDown() {
node.destroy();
}
private static Stream<Arguments> givenReportPresenceStatusOnEachMessage_whenOnMsg_thenVerifyOutputMsgType() {
DeviceId deviceId = new DeviceId(UUID.randomUUID());
long tsNow = System.currentTimeMillis();
long tsNowMinusMinuteAndMillis = tsNow - Duration.ofMinutes(1).plusMillis(1).toMillis();
return Stream.of(
// default config with presenceMonitoringStrategyOnEachMessage false and msgInside true
Arguments.of(new GpsGeofencingActionTestCase(deviceId, true, false,
new EntityGeofencingState(false, 0, false)), ENTERED),
Arguments.of(new GpsGeofencingActionTestCase(deviceId, true, false,
new EntityGeofencingState(true, tsNow, false)), SUCCESS),
Arguments.of(new GpsGeofencingActionTestCase(deviceId, true, false,
new EntityGeofencingState(true, tsNowMinusMinuteAndMillis, false)), INSIDE),
Arguments.of(new GpsGeofencingActionTestCase(deviceId, true, false,
new EntityGeofencingState(true, tsNow, true)), SUCCESS),
// default config with presenceMonitoringStrategyOnEachMessage false and msgInside false
Arguments.of(new GpsGeofencingActionTestCase(deviceId, false, false,
new EntityGeofencingState(false, 0, false)), LEFT),
Arguments.of(new GpsGeofencingActionTestCase(deviceId, false, false,
new EntityGeofencingState(false, tsNow, false)), SUCCESS),
Arguments.of(new GpsGeofencingActionTestCase(deviceId, false, false,
new EntityGeofencingState(false, tsNowMinusMinuteAndMillis, false)), OUTSIDE),
Arguments.of(new GpsGeofencingActionTestCase(deviceId, false, false,
new EntityGeofencingState(false, tsNow, true)), SUCCESS),
// default config with presenceMonitoringStrategyOnEachMessage true and msgInside true
Arguments.of(new GpsGeofencingActionTestCase(deviceId, true, true,
new EntityGeofencingState(false, 0, false)), ENTERED),
Arguments.of(new GpsGeofencingActionTestCase(deviceId, true, true,
new EntityGeofencingState(true, tsNow, false)), INSIDE),
Arguments.of(new GpsGeofencingActionTestCase(deviceId, true, true,
new EntityGeofencingState(true, tsNowMinusMinuteAndMillis, false)), INSIDE),
// default config with presenceMonitoringStrategyOnEachMessage true and msgInside false
Arguments.of(new GpsGeofencingActionTestCase(deviceId, false, true,
new EntityGeofencingState(false, 0, false)), LEFT),
Arguments.of(new GpsGeofencingActionTestCase(deviceId, false, true,
new EntityGeofencingState(false, tsNow, false)), OUTSIDE),
Arguments.of(new GpsGeofencingActionTestCase(deviceId, false, true,
new EntityGeofencingState(false, tsNowMinusMinuteAndMillis, false)), OUTSIDE)
);
}
@ParameterizedTest
@MethodSource
void givenReportPresenceStatusOnEachMessage_whenOnMsg_thenVerifyOutputMsgType(
GpsGeofencingActionTestCase gpsGeofencingActionTestCase,
String expectedOutput
) throws TbNodeException {
// GIVEN
var config = new TbGpsGeofencingActionNodeConfiguration().defaultConfiguration();
config.setReportPresenceStatusOnEachMessage(gpsGeofencingActionTestCase.isReportPresenceStatusOnEachMessage());
node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
TbMsg msg = gpsGeofencingActionTestCase.isMsgInside() ?
getInsideRectangleTbMsg(gpsGeofencingActionTestCase.getEntityId()) :
getOutsideRectangleTbMsg(gpsGeofencingActionTestCase.getEntityId());
when(ctx.getAttributesService()).thenReturn(attributesService);
ReflectionTestUtils.setField(node, "entityStates", gpsGeofencingActionTestCase.getEntityStates());
// WHEN
node.onMsg(ctx, msg);
// THEN
verify(ctx.getAttributesService(), never()).find(any(), any(), any(), anyString());
verify(ctx, never()).tellFailure(any(), any(Throwable.class));
verify(ctx, never()).enqueueForTellNext(any(), eq(expectedOutput), any(), any());
verify(ctx, never()).ack(any());
if (SUCCESS.equals(expectedOutput)) {
verify(ctx).tellSuccess(eq(msg));
} else {
verify(ctx).tellNext(eq(msg), eq(expectedOutput));
}
}
private TbMsg getOutsideRectangleTbMsg(EntityId entityId) {
return getTbMsg(entityId, getMetadataForNewVersionPolygonPerimeter(),
GeoUtilTest.POINT_OUTSIDE_SIMPLE_RECT.getLatitude(),
GeoUtilTest.POINT_OUTSIDE_SIMPLE_RECT.getLongitude());
}
private TbMsg getInsideRectangleTbMsg(EntityId entityId) {
return getTbMsg(entityId, getMetadataForNewVersionPolygonPerimeter(),
GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLatitude(),
GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLongitude());
}
private TbMsg getTbMsg(EntityId entityId, TbMsgMetaData metadata, double latitude, double longitude) {
String data = "{\"latitude\": " + latitude + ", \"longitude\": " + longitude + "}";
return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, metadata, data);
}
private TbMsgMetaData getMetadataForNewVersionPolygonPerimeter() {
var metadata = new TbMsgMetaData();
metadata.putValue("ss_perimeter", GeoUtilTest.SIMPLE_RECT);
return metadata;
}
// Rule nodes upgrade
private static Stream<Arguments> givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() {
return Stream.of(
// default config for version 0
Arguments.of(0,
"{\n" +
" \"minInsideDuration\": 1,\n" +
" \"minOutsideDuration\": 1,\n" +
" \"minInsideDurationTimeUnit\": \"MINUTES\",\n" +
" \"minOutsideDurationTimeUnit\": \"MINUTES\",\n" +
" \"latitudeKeyName\": \"latitude\",\n" +
" \"longitudeKeyName\": \"longitude\",\n" +
" \"perimeterType\": \"POLYGON\",\n" +
" \"fetchPerimeterInfoFromMessageMetadata\": true,\n" +
" \"perimeterKeyName\": \"ss_perimeter\",\n" +
" \"polygonsDefinition\": null,\n" +
" \"centerLatitude\": null,\n" +
" \"centerLongitude\": null,\n" +
" \"range\": null,\n" +
" \"rangeUnit\": null\n" +
"}\n",
true,
"{\n" +
" \"minInsideDuration\": 1,\n" +
" \"minOutsideDuration\": 1,\n" +
" \"minInsideDurationTimeUnit\": \"MINUTES\",\n" +
" \"minOutsideDurationTimeUnit\": \"MINUTES\",\n" +
" \"reportPresenceStatusOnEachMessage\": false,\n" +
" \"latitudeKeyName\": \"latitude\",\n" +
" \"longitudeKeyName\": \"longitude\",\n" +
" \"perimeterType\": \"POLYGON\",\n" +
" \"fetchPerimeterInfoFromMessageMetadata\": true,\n" +
" \"perimeterKeyName\": \"ss_perimeter\",\n" +
" \"polygonsDefinition\": null,\n" +
" \"centerLatitude\": null,\n" +
" \"centerLongitude\": null,\n" +
" \"range\": null,\n" +
" \"rangeUnit\": null\n" +
"}\n"),
// default config for version 1 with upgrade from version 0
Arguments.of(0,
"{\n" +
" \"minInsideDuration\": 1,\n" +
" \"minOutsideDuration\": 1,\n" +
" \"minInsideDurationTimeUnit\": \"MINUTES\",\n" +
" \"minOutsideDurationTimeUnit\": \"MINUTES\",\n" +
" \"reportPresenceStatusOnEachMessage\": false,\n" +
" \"latitudeKeyName\": \"latitude\",\n" +
" \"longitudeKeyName\": \"longitude\",\n" +
" \"perimeterType\": \"POLYGON\",\n" +
" \"fetchPerimeterInfoFromMessageMetadata\": true,\n" +
" \"perimeterKeyName\": \"ss_perimeter\",\n" +
" \"polygonsDefinition\": null,\n" +
" \"centerLatitude\": null,\n" +
" \"centerLongitude\": null,\n" +
" \"range\": null,\n" +
" \"rangeUnit\": null\n" +
"}\n",
false,
"{\n" +
" \"minInsideDuration\": 1,\n" +
" \"minOutsideDuration\": 1,\n" +
" \"minInsideDurationTimeUnit\": \"MINUTES\",\n" +
" \"minOutsideDurationTimeUnit\": \"MINUTES\",\n" +
" \"reportPresenceStatusOnEachMessage\": false,\n" +
" \"latitudeKeyName\": \"latitude\",\n" +
" \"longitudeKeyName\": \"longitude\",\n" +
" \"perimeterType\": \"POLYGON\",\n" +
" \"fetchPerimeterInfoFromMessageMetadata\": true,\n" +
" \"perimeterKeyName\": \"ss_perimeter\",\n" +
" \"polygonsDefinition\": null,\n" +
" \"centerLatitude\": null,\n" +
" \"centerLongitude\": null,\n" +
" \"range\": null,\n" +
" \"rangeUnit\": null\n" +
"}\n")
);
}
@Override
protected TbNode getTestNode() {
return node;
}
}

18
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoaderTest.java

@ -37,10 +37,12 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.EntityViewId;
@ -52,6 +54,7 @@ import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
@ -95,6 +98,8 @@ public class EntitiesFieldsAsyncLoaderTest {
private RuleChainService ruleChainServiceMock; private RuleChainService ruleChainServiceMock;
@Mock @Mock
private EntityViewService entityViewServiceMock; private EntityViewService entityViewServiceMock;
@Mock
private EdgeService edgeServiceMock;
@BeforeAll @BeforeAll
public static void setup() { public static void setup() {
@ -108,7 +113,8 @@ public class EntitiesFieldsAsyncLoaderTest {
EntityType.DEVICE, EntityType.DEVICE,
EntityType.ALARM, EntityType.ALARM,
EntityType.RULE_CHAIN, EntityType.RULE_CHAIN,
EntityType.ENTITY_VIEW EntityType.ENTITY_VIEW,
EntityType.EDGE
); );
} }
@ -228,6 +234,14 @@ public class EntitiesFieldsAsyncLoaderTest {
when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock); when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock);
doReturn(entityView).when(entityViewServiceMock).findEntityViewByIdAsync(eq(TENANT_ID), any()); doReturn(entityView).when(entityViewServiceMock).findEntityViewByIdAsync(eq(TENANT_ID), any());
break;
case EDGE:
var edge = Futures.immediateFuture(entityDoesNotExist ? null : new Edge(new EdgeId(RANDOM_UUID)));
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
when(ctxMock.getEdgeService()).thenReturn(edgeServiceMock);
doReturn(edge).when(edgeServiceMock).findEdgeByIdAsync(eq(TENANT_ID), any());
break; break;
default: default:
throw new RuntimeException("Unexpected EntityType: " + entityType); throw new RuntimeException("Unexpected EntityType: " + entityType);
@ -252,6 +266,8 @@ public class EntitiesFieldsAsyncLoaderTest {
return new RuleChain((RuleChainId) entityId); return new RuleChain((RuleChainId) entityId);
case ENTITY_VIEW: case ENTITY_VIEW:
return new EntityView((EntityViewId) entityId); return new EntityView((EntityViewId) entityId);
case EDGE:
return new Edge((EdgeId) entityId);
default: default:
throw new RuntimeException("Unexpected EntityType: " + entityId.getEntityType()); throw new RuntimeException("Unexpected EntityType: " + entityId.getEntityType());
} }

1
ui-ngx/package.json

@ -44,6 +44,7 @@
"@ngrx/store-devtools": "^15.4.0", "@ngrx/store-devtools": "^15.4.0",
"@ngx-translate/core": "^14.0.0", "@ngx-translate/core": "^14.0.0",
"@ngx-translate/http-loader": "^7.0.0", "@ngx-translate/http-loader": "^7.0.0",
"@svgdotjs/svg.filter.js": "^3.0.8",
"@svgdotjs/svg.js": "^3.2.0", "@svgdotjs/svg.js": "^3.2.0",
"@tinymce/tinymce-angular": "^7.0.0", "@tinymce/tinymce-angular": "^7.0.0",
"ace-builds": "1.4.13", "ace-builds": "1.4.13",

1
ui-ngx/src/app/core/api/widget-api.models.ts

@ -82,6 +82,7 @@ export interface RpcApi {
export interface IWidgetUtils { export interface IWidgetUtils {
formatValue: (value: any, dec?: number, units?: string, showZeroDecimals?: boolean) => string | undefined; formatValue: (value: any, dec?: number, units?: string, showZeroDecimals?: boolean) => string | undefined;
getEntityDetailsPageURL: (id: string, entityType: EntityType) => string;
} }
export interface WidgetActionsApi { export interface WidgetActionsApi {

37
ui-ngx/src/app/core/http/image.service.ts

@ -18,7 +18,7 @@ import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
import { defaultHttpOptionsFromConfig, defaultHttpUploadOptions, RequestConfig } from '@core/http/http-utils'; import { defaultHttpOptionsFromConfig, defaultHttpUploadOptions, RequestConfig } from '@core/http/http-utils';
import { Observable, of } from 'rxjs'; import { Observable, of, ReplaySubject } from 'rxjs';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { import {
NO_IMAGE_DATA_URI, NO_IMAGE_DATA_URI,
@ -36,6 +36,9 @@ import { ResourcesService } from '@core/services/resources.service';
providedIn: 'root' providedIn: 'root'
}) })
export class ImageService { export class ImageService {
private imagesLoading: { [url: string]: ReplaySubject<Blob> } = {};
constructor( constructor(
private http: HttpClient, private http: HttpClient,
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer,
@ -95,12 +98,34 @@ export class ImageService {
parts[parts.length - 1] = encodeURIComponent(key); parts[parts.length - 1] = encodeURIComponent(key);
const encodedUrl = parts.join('/'); const encodedUrl = parts.join('/');
const imageLink = preview ? (encodedUrl + '/preview') : encodedUrl; const imageLink = preview ? (encodedUrl + '/preview') : encodedUrl;
const options = defaultHttpOptionsFromConfig({ignoreLoading: true, ignoreErrors: true}); return this.loadImageDataUrl(imageLink, asString, emptyUrl);
return this.http }
.get(imageLink, {...options, ...{ responseType: 'blob' } }).pipe(
private loadImageDataUrl(imageLink: string, asString = false, emptyUrl = NO_IMAGE_DATA_URI): Observable<SafeUrl | string> {
let request: ReplaySubject<Blob>;
if (this.imagesLoading[imageLink]) {
request = this.imagesLoading[imageLink];
} else {
request = new ReplaySubject<Blob>(1);
this.imagesLoading[imageLink] = request;
const options = defaultHttpOptionsFromConfig({ignoreLoading: true, ignoreErrors: true});
this.http.get(imageLink, {...options, ...{ responseType: 'blob' } }).subscribe({
next: (value) => {
request.next(value);
request.complete();
},
error: err => {
request.error(err);
},
complete: () => {
delete this.imagesLoading[imageLink];
}
});
}
return request.pipe(
switchMap(val => blobToBase64(val).pipe( switchMap(val => blobToBase64(val).pipe(
map((dataUrl) => asString ? dataUrl : this.sanitizer.bypassSecurityTrustUrl(dataUrl)) map((dataUrl) => asString ? dataUrl : this.sanitizer.bypassSecurityTrustUrl(dataUrl))
)), )),
catchError(() => of(asString ? emptyUrl : this.sanitizer.bypassSecurityTrustUrl(emptyUrl))) catchError(() => of(asString ? emptyUrl : this.sanitizer.bypassSecurityTrustUrl(emptyUrl)))
); );
} }

1
ui-ngx/src/app/core/notification/notification.models.ts

@ -33,6 +33,7 @@ export class NotificationMessage {
horizontalPosition?: NotificationHorizontalPosition; horizontalPosition?: NotificationHorizontalPosition;
verticalPosition?: NotificationVerticalPosition; verticalPosition?: NotificationVerticalPosition;
panelClass?: string | string[]; panelClass?: string | string[];
modern?: boolean;
} }
export class HideNotification { export class HideNotification {

8
ui-ngx/src/app/core/services/item-buffer.service.ts

@ -27,7 +27,7 @@ import {
widgetType widgetType
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { deepClone, isEqual } from '@core/utils'; import { deepClone, isDefinedAndNotNull, isEqual } from '@core/utils';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { Observable, of, throwError } from 'rxjs'; import { Observable, of, throwError } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@ -309,6 +309,12 @@ export class ItemBufferService {
if (origNode.error) { if (origNode.error) {
node.error = origNode.error; node.error = origNode.error;
} }
if (isDefinedAndNotNull(origNode.singletonMode)) {
node.singletonMode = origNode.singletonMode;
}
if (isDefinedAndNotNull(origNode.queueName)) {
node.queueName = origNode.queueName;
}
ruleNodes.nodes.push(node); ruleNodes.nodes.push(node);
if (i === 0) { if (i === 0) {
top = node.y; top = node.y;

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

@ -194,6 +194,17 @@ import * as ScrollGridComponent from '@shared/components/grid/scroll-grid.compon
import * as GalleryImageInputComponent from '@shared/components/image/gallery-image-input.component'; import * as GalleryImageInputComponent from '@shared/components/image/gallery-image-input.component';
import * as MultipleGalleryImageInputComponent from '@shared/components/image/multiple-gallery-image-input.component'; import * as MultipleGalleryImageInputComponent from '@shared/components/image/multiple-gallery-image-input.component';
import * as CssUnitSelectComponent from '@home/components/widget/lib/settings/common/css-unit-select.component';
import * as WidgetActionsPanelComponent from '@home/components/widget/config/basic/common/widget-actions-panel.component';
import * as FontSettingsComponent from '@home/components/widget/lib/settings/common/font-settings.component';
import * as ColorSettingsComponent from '@home/components/widget/lib/settings/common/color-settings.component';
import * as DisplayColumnsPanelComponent from '@home/components/widget/lib/display-columns-panel.component';
import * as AlarmDetailsDialogComponent from '@home/components/alarm/alarm-details-dialog.component';
import * as AlarmAssigneePanelComponent from '@home/components/alarm/alarm-assignee-panel.component';
import * as AlarmCommentDialogComponent from '@home/components/alarm/alarm-comment-dialog.component';
import * as AlarmFilterConfigComponent from '@home/components/alarm/alarm-filter-config.component';
import * as DatasourceComponent from '@home/components/widget/config/datasources.component';
import * as DataKeysPanelComponent from '@home/components/widget/config/basic/common/data-keys-panel.component';
import * as AddEntityDialogComponent from '@home/components/entity/add-entity-dialog.component'; import * as AddEntityDialogComponent from '@home/components/entity/add-entity-dialog.component';
import * as EntitiesTableComponent from '@home/components/entity/entities-table.component'; import * as EntitiesTableComponent from '@home/components/entity/entities-table.component';
import * as DetailsPanelComponent from '@home/components/details-panel.component'; import * as DetailsPanelComponent from '@home/components/details-panel.component';
@ -239,6 +250,10 @@ import * as ImportDialogCsvComponent from '@shared/import-export/import-dialog-c
import * as TableColumnsAssignmentComponent from '@shared/import-export/table-columns-assignment.component'; import * as TableColumnsAssignmentComponent from '@shared/import-export/table-columns-assignment.component';
import * as EventContentDialogComponent from '@home/components/event/event-content-dialog.component'; import * as EventContentDialogComponent from '@home/components/event/event-content-dialog.component';
import * as SharedHomeComponentsModule from '@home/components/shared-home-components.module'; import * as SharedHomeComponentsModule from '@home/components/shared-home-components.module';
import * as WidgetConfigComponentsModule from '@home/components/widget/config/widget-config-components.module';
import * as BasicWidgetConfigModule from '@home/components/widget/config/basic/basic-widget-config.module';
import * as WidgetSettingsCommonModule from '@home/components/widget/lib/settings/common/widget-settings-common.module';
import * as WidgetComponentsModule from '@home/components/widget/widget-components.module';
import * as SelectTargetLayoutDialogComponent from '@home/components/dashboard/select-target-layout-dialog.component'; import * as SelectTargetLayoutDialogComponent from '@home/components/dashboard/select-target-layout-dialog.component';
import * as SelectTargetStateDialogComponent from '@home/components/dashboard/select-target-state-dialog.component'; import * as SelectTargetStateDialogComponent from '@home/components/dashboard/select-target-state-dialog.component';
import * as AliasesEntityAutocompleteComponent from '@home/components/alias/aliases-entity-autocomplete.component'; import * as AliasesEntityAutocompleteComponent from '@home/components/alias/aliases-entity-autocomplete.component';
@ -507,6 +522,17 @@ class ModulesMap implements IModulesMap {
'@shared/components/image/gallery-image-input.component': GalleryImageInputComponent, '@shared/components/image/gallery-image-input.component': GalleryImageInputComponent,
'@shared/components/image/multiple-gallery-image-input.component': MultipleGalleryImageInputComponent, '@shared/components/image/multiple-gallery-image-input.component': MultipleGalleryImageInputComponent,
'@home/components/alarm/alarm-filter-config.component': AlarmFilterConfigComponent,
'@home/components/alarm/alarm-comment-dialog.component': AlarmCommentDialogComponent,
'@home/components/alarm/alarm-assignee-panel.component': AlarmAssigneePanelComponent,
'@home/components/alarm/alarm-details-dialog.component': AlarmDetailsDialogComponent,
'@home/components/widget/lib/display-columns-panel.component': DisplayColumnsPanelComponent,
'@home/components/widget/config/datasources.component': DatasourceComponent,
'@home/components/widget/config/basic/common/data-keys-panel.component': DataKeysPanelComponent,
'@home/components/widget/lib/settings/common/color-settings.component': ColorSettingsComponent,
'@home/components/widget/lib/settings/common/font-settings.component': FontSettingsComponent,
'@home/components/widget/config/basic/common/widget-actions-panel.component': WidgetActionsPanelComponent,
'@home/components/widget/lib/settings/common/css-unit-select.component': CssUnitSelectComponent,
'@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent, '@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent,
'@home/components/entity/entities-table.component': EntitiesTableComponent, '@home/components/entity/entities-table.component': EntitiesTableComponent,
'@home/components/details-panel.component': DetailsPanelComponent, '@home/components/details-panel.component': DetailsPanelComponent,
@ -549,6 +575,10 @@ class ModulesMap implements IModulesMap {
'@home/components/attribute/add-widget-to-dashboard-dialog.component': AddWidgetToDashboardDialogComponent, '@home/components/attribute/add-widget-to-dashboard-dialog.component': AddWidgetToDashboardDialogComponent,
'@home/components/event/event-content-dialog.component': EventContentDialogComponent, '@home/components/event/event-content-dialog.component': EventContentDialogComponent,
'@home/components/shared-home-components.module': SharedHomeComponentsModule, '@home/components/shared-home-components.module': SharedHomeComponentsModule,
'@home/components/widget/config/widget-config-components.module': WidgetConfigComponentsModule,
'@home/components/widget/config/basic/basic-widget-config.module': BasicWidgetConfigModule,
'@home/components/widget/lib/settings/common/widget-settings-common.module': WidgetSettingsCommonModule,
'@home/components/widget/widget-components.module': WidgetComponentsModule,
'@home/components/dashboard/select-target-layout-dialog.component': SelectTargetLayoutDialogComponent, '@home/components/dashboard/select-target-layout-dialog.component': SelectTargetLayoutDialogComponent,
'@home/components/dashboard/select-target-state-dialog.component': SelectTargetStateDialogComponent, '@home/components/dashboard/select-target-state-dialog.component': SelectTargetStateDialogComponent,
'@home/components/alias/aliases-entity-autocomplete.component': AliasesEntityAutocompleteComponent, '@home/components/alias/aliases-entity-autocomplete.component': AliasesEntityAutocompleteComponent,

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

@ -214,7 +214,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
disableAutoPositionOnConflict: false, disableAutoPositionOnConflict: false,
pushItems: false, pushItems: false,
swap: false, swap: false,
maxRows: 100, maxRows: 3000,
minCols: this.columns ? this.columns : 24, minCols: this.columns ? this.columns : 24,
maxCols: 3000, maxCols: 3000,
maxItemCols: 1000, maxItemCols: 1000,
@ -293,12 +293,12 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow); this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow);
} }
if (updateMobileOpts) {
this.updateMobileOpts();
}
if (updateLayoutOpts) { if (updateLayoutOpts) {
this.updateLayoutOpts(); this.updateLayoutOpts();
} }
if (updateMobileOpts) {
this.updateMobileOpts();
}
if (updateEditingOpts) { if (updateEditingOpts) {
this.updateEditingOpts(); this.updateEditingOpts();
} }

2
ui-ngx/src/app/modules/home/components/filter/filters-edit.component.ts

@ -119,7 +119,7 @@ export class FiltersEditComponent implements OnInit, OnDestroy {
const filteredArray = Object.entries(this.filtersInfo); const filteredArray = Object.entries(this.filtersInfo);
if (filteredArray.length === 1) { if (filteredArray.length === 1) {
const singleFilter: Filter = {id: filteredArray[0][0], ...filteredArray[0][1]}; const singleFilter: Filter = {id: filteredArray[0][0], ...deepClone(filteredArray[0][1])};
this.dialog.open<UserFilterDialogComponent, UserFilterDialogData, this.dialog.open<UserFilterDialogComponent, UserFilterDialogData,
Filter>(UserFilterDialogComponent, { Filter>(UserFilterDialogComponent, {
disableClose: true, disableClose: true,

2
ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.html

@ -19,7 +19,7 @@
<div *ngFor="let deviceProfileCommunication of communicationConfigFormArray.controls; let $index = index; <div *ngFor="let deviceProfileCommunication of communicationConfigFormArray.controls; let $index = index;
last as isLast;" fxLayout="row" fxLayoutAlign="start center" last as isLast;" fxLayout="row" fxLayoutAlign="start center"
fxLayoutGap="8px" class="scope-row" [formGroup]="deviceProfileCommunication"> fxLayoutGap="8px" class="scope-row" [formGroup]="deviceProfileCommunication">
<div class="communication-config" fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start"> <div class="communication-config">
<mat-form-field class="spec mat-block" floatLabel="always" hideRequiredMarker> <mat-form-field class="spec mat-block" floatLabel="always" hideRequiredMarker>
<mat-label translate>device-profile.snmp.scope</mat-label> <mat-label translate>device-profile.snmp.scope</mat-label>
<mat-select formControlName="spec" required> <mat-select formControlName="spec" required>

15
ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.scss

@ -13,12 +13,20 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import '../scss/constants';
:host { :host {
.communication-config { .communication-config {
border: 2px groove rgba(0, 0, 0, 0.25); border: 2px groove rgba(0, 0, 0, 0.25);
border-radius: 4px; border-radius: 4px;
padding: 8px; padding: 8px;
min-width: 0; min-width: 0;
flex-direction: column;
display: flex;
place-content: stretch flex-start;
align-items: stretch;
flex: 1;
gap: 0;
} }
.scope-row { .scope-row {
@ -28,6 +36,13 @@
.required-text { .required-text {
margin: 16px 0 margin: 16px 0
} }
@media #{$mat-gt-xmd} {
.communication-config {
flex-direction: row;
gap: 8px;
}
}
} }
:host ::ng-deep { :host ::ng-deep {

6
ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html

@ -19,8 +19,8 @@
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" fxFlex="100"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" fxFlex="100">
<div fxFlex fxLayout="row" fxLayoutGap="8px"> <div fxFlex fxLayout="row" fxLayoutGap="8px">
<label fxFlex="26" class="tb-title no-padding" translate>device-profile.snmp.data-type</label> <label fxFlex="26" class="tb-title no-padding" translate>device-profile.snmp.data-type</label>
<label fxFlex="37" class="tb-title no-padding" translate>device-profile.snmp.data-key</label> <label fxFlex="37" class="tb-title tb-required no-padding" translate>device-profile.snmp.data-key</label>
<label fxFlex="37" class="tb-title no-padding" translate>device-profile.snmp.oid</label> <label fxFlex="37" class="tb-title tb-required no-padding" translate>device-profile.snmp.oid</label>
<span style="min-width: 40px" [fxShow]="!disabled"></span> <span style="min-width: 40px" [fxShow]="!disabled"></span>
</div> </div>
</div> </div>
@ -28,7 +28,7 @@
<div *ngFor="let mappingConfig of mappingsConfigFormArray.controls; let $index = index; <div *ngFor="let mappingConfig of mappingsConfigFormArray.controls; let $index = index;
last as isLast;" fxLayout="row" fxLayoutAlign="start center" last as isLast;" fxLayout="row" fxLayoutAlign="start center"
fxLayoutGap="8px" [formGroup]="mappingConfig" class="mapping-list"> fxLayoutGap="8px" [formGroup]="mappingConfig" class="mapping-list">
<div fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start"> <div class="tb-layout-fill" fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start">
<mat-form-field fxFlex="26"> <mat-form-field fxFlex="26">
<mat-select formControlName="dataType" required> <mat-select formControlName="dataType" required>
<mat-option *ngFor="let dataType of dataTypes" [value]="dataType"> <mat-option *ngFor="let dataType of dataTypes" [value]="dataType">

10
ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.scss

@ -15,13 +15,9 @@
*/ */
:host { :host {
.mapping-config { .mapping-config {
min-width: 518px;
}
.mapping-list {
padding-bottom: 8px;
}
.required-text { .required-text {
margin: 14px 0; margin: 14px 0;
}
} }
} }

6
ui-ngx/src/app/modules/home/components/public-api.ts

@ -15,5 +15,11 @@
/// ///
export * from './home-components.module'; export * from './home-components.module';
export * from './widget/config/basic/basic-widget-config.module';
export * from './widget/lib/settings/common/widget-settings-common.module';
export * from './widget/widget-components.module';
export * from './widget/config/widget-config-components.module';
export * from './widget/config/widget-config.component.models'; export * from './widget/config/widget-config.component.models';
export * from './widget/lib/table-widget.models';
export * from './widget/lib/flot-widget.models';

4
ui-ngx/src/app/modules/home/components/vc/repository-settings.component.html

@ -26,9 +26,9 @@
<span fxFlex></span> <span fxFlex></span>
<div tb-help="repositorySettings"></div> <div tb-help="repositorySettings"></div>
</mat-card-header> </mat-card-header>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> <mat-progress-bar color="warn" mode="indeterminate" *ngIf="!hideLoadingBar && isLoading$ | async">
</mat-progress-bar> </mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> <div style="height: 4px;" *ngIf="hideLoadingBar || !(isLoading$ | async)"></div>
<mat-card-content style="padding-top: 16px;"> <mat-card-content style="padding-top: 16px;">
<form [formGroup]="repositorySettingsForm" #formDirective="ngForm" (ngSubmit)="save()"> <form [formGroup]="repositorySettingsForm" #formDirective="ngForm" (ngSubmit)="save()">
<fieldset [disabled]="isLoading$ | async"> <fieldset [disabled]="isLoading$ | async">

5
ui-ngx/src/app/modules/home/components/vc/repository-settings.component.ts

@ -34,6 +34,7 @@ import { selectHasRepository } from '@core/auth/auth.selectors';
import { catchError, mergeMap, take } from 'rxjs/operators'; import { catchError, mergeMap, take } from 'rxjs/operators';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { TbPopoverComponent } from '@shared/components/popover.component'; import { TbPopoverComponent } from '@shared/components/popover.component';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({ @Component({
selector: 'tb-repository-settings', selector: 'tb-repository-settings',
@ -48,6 +49,10 @@ export class RepositorySettingsComponent extends PageComponent implements OnInit
@Input() @Input()
popoverComponent: TbPopoverComponent; popoverComponent: TbPopoverComponent;
@Input()
@coerceBoolean()
hideLoadingBar = false;
repositorySettingsForm: UntypedFormGroup; repositorySettingsForm: UntypedFormGroup;
settings: RepositorySettings = null; settings: RepositorySettings = null;

6
ui-ngx/src/app/modules/home/components/vc/version-control.component.html

@ -15,9 +15,11 @@
limitations under the License. limitations under the License.
--> -->
<tb-repository-settings #repositorySettingsComponent [detailsMode]="detailsMode" <tb-repository-settings #repositorySettingsComponent
[detailsMode]="detailsMode"
hideLoadingBar
[popoverComponent]="popoverComponent" [popoverComponent]="popoverComponent"
*ngIf="!(hasRepository$ | async); else versionsTable"> *ngIf="!(hasRepository$ | async); else versionsTable">
</tb-repository-settings> </tb-repository-settings>
<ng-template #versionsTable> <ng-template #versionsTable>
<tb-entity-versions-table [singleEntityMode]="singleEntityMode" <tb-entity-versions-table [singleEntityMode]="singleEntityMode"

16
ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts

@ -100,6 +100,10 @@ import {
import { import {
CommandButtonBasicConfigComponent CommandButtonBasicConfigComponent
} from '@home/components/widget/config/basic/button/command-button-basic-config.component'; } from '@home/components/widget/config/basic/button/command-button-basic-config.component';
import {
PowerButtonBasicConfigComponent
} from '@home/components/widget/config/basic/button/power-button-basic-config.component';
import { SliderBasicConfigComponent } from '@home/components/widget/config/basic/rpc/slider-basic-config.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -131,7 +135,9 @@ import {
BarChartWithLabelsBasicConfigComponent, BarChartWithLabelsBasicConfigComponent,
SingleSwitchBasicConfigComponent, SingleSwitchBasicConfigComponent,
ActionButtonBasicConfigComponent, ActionButtonBasicConfigComponent,
CommandButtonBasicConfigComponent CommandButtonBasicConfigComponent,
PowerButtonBasicConfigComponent,
SliderBasicConfigComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -167,7 +173,9 @@ import {
BarChartWithLabelsBasicConfigComponent, BarChartWithLabelsBasicConfigComponent,
SingleSwitchBasicConfigComponent, SingleSwitchBasicConfigComponent,
ActionButtonBasicConfigComponent, ActionButtonBasicConfigComponent,
CommandButtonBasicConfigComponent CommandButtonBasicConfigComponent,
PowerButtonBasicConfigComponent,
SliderBasicConfigComponent
] ]
}) })
export class BasicWidgetConfigModule { export class BasicWidgetConfigModule {
@ -197,5 +205,7 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-bar-chart-with-labels-basic-config': BarChartWithLabelsBasicConfigComponent, 'tb-bar-chart-with-labels-basic-config': BarChartWithLabelsBasicConfigComponent,
'tb-single-switch-basic-config': SingleSwitchBasicConfigComponent, 'tb-single-switch-basic-config': SingleSwitchBasicConfigComponent,
'tb-action-button-basic-config': ActionButtonBasicConfigComponent, 'tb-action-button-basic-config': ActionButtonBasicConfigComponent,
'tb-command-button-basic-config': CommandButtonBasicConfigComponent 'tb-command-button-basic-config': CommandButtonBasicConfigComponent,
'tb-power-button-basic-config': PowerButtonBasicConfigComponent,
'tb-slider-basic-config': SliderBasicConfigComponent
}; };

197
ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.html

@ -0,0 +1,197 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container [formGroup]="powerButtonWidgetConfigForm">
<tb-target-device formControlName="targetDevice"></tb-target-device>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.power-button.behavior</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.initial-state-hint' | translate}}" translate>widgets.rpc-state.initial-state</div>
<tb-get-value-action-settings fxFlex
panelTitle="widgets.rpc-state.initial-state"
[valueType]="valueType.BOOLEAN"
trueLabel="widgets.rpc-state.on"
falseLabel="widgets.rpc-state.off"
stateLabel="widgets.rpc-state.on"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="initialState"></tb-get-value-action-settings>
</div>
<div class="tb-form-row space-between">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.power-button.power-on-hint' | translate}}" translate>widgets.power-button.power-on</div>
<tb-set-value-action-settings fxFlex
panelTitle="widgets.power-button.power-on "
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="onUpdateState"></tb-set-value-action-settings>
</div>
<div class="tb-form-row space-between">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.power-button.power-off-hint' | translate}}" translate>widgets.power-button.power-off</div>
<tb-set-value-action-settings fxFlex
panelTitle="widgets.power-button.power-off"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="offUpdateState"></tb-set-value-action-settings>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.disabled-state-hint' | translate}}" translate>widgets.rpc-state.disabled-state</div>
<tb-get-value-action-settings fxFlex
panelTitle="widgets.rpc-state.disabled-state"
[valueType]="valueType.BOOLEAN"
stateLabel="widgets.rpc-state.disabled"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="disabledState"></tb-get-value-action-settings>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-image-cards-select rowHeight="1:1"
[cols]="{columns: 3,
breakpoints: {
'lt-sm': 1,
'lt-md': 2
}}"
label="{{ 'widgets.power-button.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of powerButtonLayouts"
[value]="layout"
[image]="powerButtonLayoutImageMap.get(layout)">
{{ powerButtonLayoutTranslationMap.get(layout) | translate }}
</tb-image-cards-select-option>
</tb-image-cards-select>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitle">
{{ 'widget-config.title' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="titleFont"
clearButton
[previewText]="powerButtonWidgetConfigForm.get('title').value"
[initialPreviewStyle]="widgetConfig.config.titleStyle">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="titleColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon">
{{ 'widget-config.card-icon' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select fxFlex formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
[color]="powerButtonWidgetConfigForm.get('iconColor').value"
formControlName="icon">
</tb-material-icon-select>
<tb-color-input asBoxInput
colorClearButton
formControlName="iconColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.power-button.power-on-colors' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="12px">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.power-button.main</div>
<tb-color-input asBoxInput
formControlName="mainColorOn">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.power-button.background</div>
<tb-color-input asBoxInput
formControlName="backgroundColorOn">
</tb-color-input>
</div>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.power-button.power-off-colors' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="12px">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.power-button.main</div>
<tb-color-input asBoxInput
formControlName="mainColorOff">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.power-button.background</div>
<tb-color-input asBoxInput
formControlName="backgroundColorOff">
</tb-color-input>
</div>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.power-button.disabled-colors' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="12px">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.power-button.main</div>
<tb-color-input asBoxInput
formControlName="mainColorDisabled">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.power-button.background</div>
<tb-color-input asBoxInput
formControlName="backgroundColorDisabled">
</tb-color-input>
</div>
</div>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.background.background' | translate }}</div>
<tb-background-settings formControlName="background">
</tb-background-settings>
</div>
<div class="tb-form-row space-between column-lt-md">
<div translate>widget-config.show-card-buttons</div>
<mat-chip-listbox multiple formControlName="cardButtons">
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.card-border-radius' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<tb-widget-actions-panel
formControlName="actions">
</tb-widget-actions-panel>
</ng-container>

195
ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.ts

@ -0,0 +1,195 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { TargetDevice, WidgetConfig, } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { isUndefined } from '@core/utils';
import { ValueType } from '@shared/models/constants';
import {
powerButtonDefaultSettings,
powerButtonLayoutImages,
powerButtonLayouts,
powerButtonLayoutTranslations,
PowerButtonWidgetSettings
} from '@home/components/widget/lib/rpc/power-button-widget.models';
import { cssSizeToStrSize, resolveCssSize } from '@shared/models/widget-settings.models';
@Component({
selector: 'tb-power-button-basic-config',
templateUrl: './power-button-basic-config.component.html',
styleUrls: ['../basic-config.scss']
})
export class PowerButtonBasicConfigComponent extends BasicWidgetConfigComponent {
get targetDevice(): TargetDevice {
return this.powerButtonWidgetConfigForm.get('targetDevice').value;
}
powerButtonLayouts = powerButtonLayouts;
powerButtonLayoutTranslationMap = powerButtonLayoutTranslations;
powerButtonLayoutImageMap = powerButtonLayoutImages;
valueType = ValueType;
powerButtonWidgetConfigForm: UntypedFormGroup;
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private fb: UntypedFormBuilder) {
super(store, widgetConfigComponent);
}
protected configForm(): UntypedFormGroup {
return this.powerButtonWidgetConfigForm;
}
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: PowerButtonWidgetSettings = {...powerButtonDefaultSettings, ...(configData.config.settings || {})};
const iconSize = resolveCssSize(configData.config.iconSize);
this.powerButtonWidgetConfigForm = this.fb.group({
targetDevice: [configData.config.targetDevice, []],
initialState: [settings.initialState, []],
onUpdateState: [settings.onUpdateState, []],
offUpdateState: [settings.offUpdateState, []],
disabledState: [settings.disabledState, []],
layout: [settings.layout, []],
showTitle: [configData.config.showTitle, []],
title: [configData.config.title, []],
titleFont: [configData.config.titleFont, []],
titleColor: [configData.config.titleColor, []],
showIcon: [configData.config.showTitleIcon, []],
iconSize: [iconSize[0], [Validators.min(0)]],
iconSizeUnit: [iconSize[1], []],
icon: [configData.config.titleIcon, []],
iconColor: [configData.config.iconColor, []],
mainColorOn: [settings.mainColorOn, []],
backgroundColorOn: [settings.backgroundColorOn, []],
mainColorOff: [settings.mainColorOff, []],
backgroundColorOff: [settings.backgroundColorOff, []],
mainColorDisabled: [settings.mainColorDisabled, []],
backgroundColorDisabled: [settings.backgroundColorDisabled, []],
background: [settings.background, []],
cardButtons: [this.getCardButtons(configData.config), []],
borderRadius: [configData.config.borderRadius, []],
actions: [configData.config.actions || {}, []]
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.targetDevice = config.targetDevice;
this.widgetConfig.config.showTitle = config.showTitle;
this.widgetConfig.config.title = config.title;
this.widgetConfig.config.titleFont = config.titleFont;
this.widgetConfig.config.titleColor = config.titleColor;
this.widgetConfig.config.showTitleIcon = config.showIcon;
this.widgetConfig.config.iconSize = cssSizeToStrSize(config.iconSize, config.iconSizeUnit);
this.widgetConfig.config.titleIcon = config.icon;
this.widgetConfig.config.iconColor = config.iconColor;
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.initialState = config.initialState;
this.widgetConfig.config.settings.onUpdateState = config.onUpdateState;
this.widgetConfig.config.settings.offUpdateState = config.offUpdateState;
this.widgetConfig.config.settings.disabledState = config.disabledState;
this.widgetConfig.config.settings.layout = config.layout;
this.widgetConfig.config.settings.mainColorOn = config.mainColorOn;
this.widgetConfig.config.settings.backgroundColorOn = config.backgroundColorOn;
this.widgetConfig.config.settings.mainColorOff = config.mainColorOff;
this.widgetConfig.config.settings.backgroundColorOff = config.backgroundColorOff;
this.widgetConfig.config.settings.mainColorDisabled = config.mainColorDisabled;
this.widgetConfig.config.settings.backgroundColorDisabled = config.backgroundColorDisabled;
this.widgetConfig.config.settings.background = config.background;
this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.borderRadius = config.borderRadius;
this.widgetConfig.config.actions = config.actions;
return this.widgetConfig;
}
protected validatorTriggers(): string[] {
return ['showTitle', 'showIcon'];
}
protected updateValidators(emitEvent: boolean, trigger?: string) {
const showTitle: boolean = this.powerButtonWidgetConfigForm.get('showTitle').value;
const showIcon: boolean = this.powerButtonWidgetConfigForm.get('showIcon').value;
if (showTitle) {
this.powerButtonWidgetConfigForm.get('title').enable();
this.powerButtonWidgetConfigForm.get('titleFont').enable();
this.powerButtonWidgetConfigForm.get('titleColor').enable();
this.powerButtonWidgetConfigForm.get('showIcon').enable({emitEvent: false});
if (showIcon) {
this.powerButtonWidgetConfigForm.get('iconSize').enable();
this.powerButtonWidgetConfigForm.get('iconSizeUnit').enable();
this.powerButtonWidgetConfigForm.get('icon').enable();
this.powerButtonWidgetConfigForm.get('iconColor').enable();
} else {
this.powerButtonWidgetConfigForm.get('iconSize').disable();
this.powerButtonWidgetConfigForm.get('iconSizeUnit').disable();
this.powerButtonWidgetConfigForm.get('icon').disable();
this.powerButtonWidgetConfigForm.get('iconColor').disable();
}
} else {
this.powerButtonWidgetConfigForm.get('title').disable();
this.powerButtonWidgetConfigForm.get('titleFont').disable();
this.powerButtonWidgetConfigForm.get('titleColor').disable();
this.powerButtonWidgetConfigForm.get('showIcon').disable({emitEvent: false});
this.powerButtonWidgetConfigForm.get('iconSize').disable();
this.powerButtonWidgetConfigForm.get('iconSizeUnit').disable();
this.powerButtonWidgetConfigForm.get('icon').disable();
this.powerButtonWidgetConfigForm.get('iconColor').disable();
}
}
private getCardButtons(config: WidgetConfig): string[] {
const buttons: string[] = [];
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) {
buttons.push('fullscreen');
}
return buttons;
}
private setCardButtons(buttons: string[], config: WidgetConfig) {
config.enableFullscreen = buttons.includes('fullscreen');
}
}

4
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.html

@ -86,7 +86,7 @@
</tb-color-input> </tb-color-input>
</div> </div>
</div> </div>
<div class="tb-form-row"> <div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showValue"> <mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showValue">
{{ 'widgets.progress-bar.value' | translate }} {{ 'widgets.progress-bar.value' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
@ -104,7 +104,7 @@
</tb-color-settings> </tb-color-settings>
</div> </div>
</div> </div>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.progress-bar.range' | translate }}</div> <div>{{ 'widgets.progress-bar.range' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div class="tb-small-label" translate>widgets.progress-bar.min</div> <div class="tb-small-label" translate>widgets.progress-bar.min</div>

50
ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.html

@ -34,7 +34,7 @@
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitle"> <mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitle">
{{ 'widgets.liquid-level-card.title' | translate }} {{ 'widgets.liquid-level-card.title' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> <div class="tb-flex row flex-start align-center">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
@ -49,7 +49,7 @@
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitleIcon"> <mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitleIcon">
{{ 'widgets.liquid-level-card.icon' | translate }} {{ 'widgets.liquid-level-card.icon' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px"> <div class="tb-flex row flex-end align-center">
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
@ -64,7 +64,7 @@
</div> </div>
</div> </div>
<div fxFlex fxLayout="column" class="tb-form-row space-between"> <div fxFlex fxLayout="column" class="tb-form-row space-between">
<div fxFlex fxLayout="row" style="width: 100%;" fxLayoutAlign="space-between center"> <div class="tb-flex row space-between align-center no-gap fill-width">
<div class="fixed-title-width" translate>widgets.liquid-level-card.shape</div> <div class="fixed-title-width" translate>widgets.liquid-level-card.shape</div>
<tb-toggle-select formControlName="tankSelectionType"> <tb-toggle-select formControlName="tankSelectionType">
<tb-toggle-option *ngFor="let type of DataSourceTypes" [value]="type"> <tb-toggle-option *ngFor="let type of DataSourceTypes" [value]="type">
@ -96,7 +96,7 @@
formControlName="shapeAttributeName"> formControlName="shapeAttributeName">
</tb-string-autocomplete> </tb-string-autocomplete>
</div> </div>
<div fxFlex fxLayout="row" fxLayoutAlign="space-between center" style="width: 100%;"> <div class="tb-flex row space-between align-center no-gap fill-width">
<div class="tb-hint" style="padding: 0;" translate>widgets.liquid-level-card.shape-by-attribute</div> <div class="tb-hint" style="padding: 0;" translate>widgets.liquid-level-card.shape-by-attribute</div>
<div class="see-example" <div class="see-example"
[tb-help-popup]="'widget/lib/indicator/shape_attribute_fn'" [tb-help-popup]="'widget/lib/indicator/shape_attribute_fn'"
@ -123,16 +123,16 @@
<div class="tb-form-panel-title" translate>widgets.liquid-level-card.units</div> <div class="tb-form-panel-title" translate>widgets.liquid-level-card.units</div>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div class="fixed-title-width tb-required" translate>widgets.liquid-level-card.datasource-units</div> <div class="fixed-title-width tb-required" translate>widgets.liquid-level-card.datasource-units</div>
<div fxFlex fxLayout="row" fxLayoutAlign="end center"> <div class="tb-flex row flex-end align-center no-gap">
<tb-unit-input required style="max-width: 25%;" class="flex" <tb-unit-input required style="max-width: 25%;" class="flex"
[tagFilter]="unitsType.capacity" [tagFilter]="unitsType.capacity"
formControlName="datasourceUnits"> formControlName="datasourceUnits">
</tb-unit-input> </tb-unit-input>
</div> </div>
</div> </div>
<div class="tb-form-row space-between" *ngIf="levelCardWidgetConfigForm.get('layout').value === LevelCardLayout.absolute"> <div class="tb-form-row space-between column-xs" *ngIf="levelCardWidgetConfigForm.get('layout').value === LevelCardLayout.absolute">
<div class="fixed-title-width tb-required" translate>widgets.liquid-level-card.widget-units</div> <div class="fixed-title-width tb-required" translate>widgets.liquid-level-card.widget-units</div>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> <div class="tb-flex row flex-start align-center">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic" style="max-width: 25%"> <mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic" style="max-width: 25%">
<mat-select formControlName="widgetUnitsSource" placeholder="{{ 'widget-config.set' | translate }}"> <mat-select formControlName="widgetUnitsSource" placeholder="{{ 'widget-config.set' | translate }}">
<mat-option *ngFor="let type of DataSourceTypes" [value]="type"> <mat-option *ngFor="let type of DataSourceTypes" [value]="type">
@ -154,9 +154,9 @@
</ng-template> </ng-template>
</div> </div>
</div> </div>
<div class="tb-form-row" [fxShow]="volumeInput"> <div class="tb-form-row column-xs" [fxShow]="volumeInput">
<div class="fixed-title-width tb-required" translate>widgets.liquid-level-card.total-volume</div> <div class="fixed-title-width tb-required" translate>widgets.liquid-level-card.total-volume</div>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> <div class="tb-flex row flex-start align-center">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic" style="max-width: 25%"> <mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic" style="max-width: 25%">
<mat-select formControlName="volumeSource" placeholder="{{ 'widget-config.set' | translate }}"> <mat-select formControlName="volumeSource" placeholder="{{ 'widget-config.set' | translate }}">
<mat-option *ngFor="let type of DataSourceTypes" [value]="type"> <mat-option *ngFor="let type of DataSourceTypes" [value]="type">
@ -184,10 +184,30 @@
formControlName="volumeAttributeName"> formControlName="volumeAttributeName">
</tb-string-autocomplete> </tb-string-autocomplete>
</ng-template> </ng-template>
<tb-unit-input [tagFilter]="unitsType.capacity" </div>
required style="max-width: 25%" class="flex" </div>
<div class="tb-form-row space-between column-xs" [fxShow]="volumeInput">
<div class="fixed-title-width tb-required" translate>widgets.liquid-level-card.total-volume-units</div>
<div class="tb-flex row flex-start align-center">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic" style="max-width: 25%">
<mat-select formControlName="volumeUnitsSource" placeholder="{{ 'widget-config.set' | translate }}">
<mat-option *ngFor="let type of DataSourceTypes" [value]="type">
{{ DataSourceTypeTranslations.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<tb-unit-input *ngIf="levelCardWidgetConfigForm.get('volumeUnitsSource').value === DataSourceType.static; else selectVolumeUnitsAttributes"
class="flex" required
[tagFilter]="unitsType.capacity"
formControlName="volumeUnits"> formControlName="volumeUnits">
</tb-unit-input> </tb-unit-input>
<ng-template #selectVolumeUnitsAttributes>
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)"
required style="flex: 1"
[errorText]="'widgets.liquid-level-card.attribute-name-required' | translate"
formControlName="volumeUnitsAttributeName">
</tb-string-autocomplete>
</ng-template>
</div> </div>
</div> </div>
</div> </div>
@ -206,7 +226,7 @@
<div class="tb-form-row space-between" <div class="tb-form-row space-between"
*ngIf="levelCardWidgetConfigForm.get('layout').value !== LevelCardLayout.simple"> *ngIf="levelCardWidgetConfigForm.get('layout').value !== LevelCardLayout.simple">
<div class="fixed-title-width" translate>widgets.liquid-level-card.value</div> <div class="fixed-title-width" translate>widgets.liquid-level-card.value</div>
<div fxFlex fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px"> <div class="tb-flex row flex-end align-center">
<mat-form-field style="max-width: 40%;" appearance="outline" class="flex number" subscriptSizing="dynamic"> <mat-form-field style="max-width: 40%;" appearance="outline" class="flex number" subscriptSizing="dynamic">
<input matInput formControlName="decimals" type="number" <input matInput formControlName="decimals" type="number"
min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}"> min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
@ -221,7 +241,7 @@
</div> </div>
<div class="tb-form-row" *ngIf="levelCardWidgetConfigForm.get('layout').value === LevelCardLayout.absolute"> <div class="tb-form-row" *ngIf="levelCardWidgetConfigForm.get('layout').value === LevelCardLayout.absolute">
<div class="fixed-title-width" translate>widgets.liquid-level-card.total-volume</div> <div class="fixed-title-width" translate>widgets.liquid-level-card.total-volume</div>
<div fxFlex fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px"> <div class="tb-flex row flex-end align-center">
<tb-font-settings formControlName="volumeFont" <tb-font-settings formControlName="volumeFont"
[previewText]="totalVolumeValuePreviewFn"> [previewText]="totalVolumeValuePreviewFn">
</tb-font-settings> </tb-font-settings>
@ -247,7 +267,7 @@
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTooltipLevel"> <mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTooltipLevel">
{{ 'widgets.liquid-level-card.level' | translate }} {{ 'widgets.liquid-level-card.level' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px"> <div class="tb-flex row flex-end align-center">
<tb-unit-input required class="flex" <tb-unit-input required class="flex"
[tagFilter]="unitsType.capacity" [tagFilter]="unitsType.capacity"
formControlName="tooltipUnits"> formControlName="tooltipUnits">
@ -268,7 +288,7 @@
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTooltipDate"> <mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTooltipDate">
{{ 'widgets.value-card.date' | translate }} {{ 'widgets.value-card.date' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<div fxFlex.gt-xs fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> <div class="tb-flex row flex-start align-center">
<tb-date-format-select fxFlex formControlName="tooltipDateFormat"></tb-date-format-select> <tb-date-format-select fxFlex formControlName="tooltipDateFormat"></tb-date-format-select>
<tb-font-settings formControlName="tooltipDateFont" <tb-font-settings formControlName="tooltipDateFont"
[previewText]="datePreviewFn"> [previewText]="datePreviewFn">

6
ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.ts

@ -194,7 +194,9 @@ export class LiquidLevelCardBasicConfigComponent extends BasicWidgetConfigCompon
volumeSource: [settings.volumeSource, []], volumeSource: [settings.volumeSource, []],
volumeConstant: [settings.volumeConstant, [Validators.required, Validators.min(0.1)]], volumeConstant: [settings.volumeConstant, [Validators.required, Validators.min(0.1)]],
volumeAttributeName: [settings.volumeAttributeName, [Validators.required]], volumeAttributeName: [settings.volumeAttributeName, [Validators.required]],
volumeUnitsSource: [settings.volumeUnitsSource, []],
volumeUnits: [settings.volumeUnits, [Validators.required]], volumeUnits: [settings.volumeUnits, [Validators.required]],
volumeUnitsAttributeName: [settings.volumeUnitsAttributeName, [Validators.required]],
volumeFont: [settings.volumeFont, []], volumeFont: [settings.volumeFont, []],
volumeColor: [settings.volumeColor, []], volumeColor: [settings.volumeColor, []],
units: [settings.units, [Validators.required]], units: [settings.units, [Validators.required]],
@ -260,6 +262,8 @@ export class LiquidLevelCardBasicConfigComponent extends BasicWidgetConfigCompon
this.widgetConfig.config.settings.volumeSource = config.volumeSource; this.widgetConfig.config.settings.volumeSource = config.volumeSource;
this.widgetConfig.config.settings.volumeConstant = config.volumeConstant; this.widgetConfig.config.settings.volumeConstant = config.volumeConstant;
this.widgetConfig.config.settings.volumeAttributeName = config.volumeAttributeName; this.widgetConfig.config.settings.volumeAttributeName = config.volumeAttributeName;
this.widgetConfig.config.settings.volumeUnitsSource = config.volumeUnitsSource;
this.widgetConfig.config.settings.volumeUnitsAttributeName = config.volumeUnitsAttributeName;
this.widgetConfig.config.settings.volumeUnits = config.volumeUnits; this.widgetConfig.config.settings.volumeUnits = config.volumeUnits;
this.widgetConfig.config.settings.volumeFont = config.volumeFont; this.widgetConfig.config.settings.volumeFont = config.volumeFont;
this.widgetConfig.config.settings.volumeColor = config.volumeColor; this.widgetConfig.config.settings.volumeColor = config.volumeColor;
@ -294,7 +298,7 @@ export class LiquidLevelCardBasicConfigComponent extends BasicWidgetConfigCompon
protected validatorTriggers(): string[] { protected validatorTriggers(): string[] {
return [ return [
'showTooltip', 'showTooltipLevel', 'tankSelectionType', 'datasourceUnits', 'showTitleIcon', 'volumeSource', 'showTooltip', 'showTooltipLevel', 'tankSelectionType', 'datasourceUnits', 'showTitleIcon', 'volumeSource',
'showTooltipDate', 'layout', 'showTitle', 'widgetUnitsSource' 'showTooltipDate', 'layout', 'showTitle', 'widgetUnitsSource', 'volumeUnitsSource'
]; ];
} }

4
ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html

@ -82,7 +82,7 @@
{{ 'widgets.single-switch.auto-scale' | translate }} {{ 'widgets.single-switch.auto-scale' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
<div class="tb-form-row"> <div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showLabel"> <mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showLabel">
{{ 'widgets.single-switch.label' | translate }} {{ 'widgets.single-switch.label' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
@ -99,7 +99,7 @@
</tb-color-input> </tb-color-input>
</div> </div>
</div> </div>
<div class="tb-form-row"> <div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon"> <mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon">
{{ 'widgets.single-switch.icon' | translate }} {{ 'widgets.single-switch.icon' | translate }}
</mat-slide-toggle> </mat-slide-toggle>

271
ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.html

@ -0,0 +1,271 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container [formGroup]="sliderWidgetConfigForm">
<tb-target-device formControlName="targetDevice"></tb-target-device>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.slider.behavior</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.slider.initial-value-hint' | translate}}" translate>widgets.slider.initial-value</div>
<tb-get-value-action-settings fxFlex
panelTitle="widgets.slider.initial-value"
[valueType]="valueType.DOUBLE"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="initialState"></tb-get-value-action-settings>
</div>
<div class="tb-form-row space-between">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.slider.on-value-change-hint' | translate}}" translate>widgets.slider.on-value-change</div>
<tb-set-value-action-settings fxFlex
panelTitle="widgets.slider.on-value-change"
[valueType]="valueType.DOUBLE"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="valueChange"></tb-set-value-action-settings>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.disabled-state-hint' | translate}}" translate>widgets.rpc-state.disabled-state</div>
<tb-get-value-action-settings fxFlex
panelTitle="widgets.rpc-state.disabled-state"
[valueType]="valueType.BOOLEAN"
stateLabel="widgets.rpc-state.disabled"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="disabledState"></tb-get-value-action-settings>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-image-cards-select rowHeight="2:1"
[cols]="{columns: 3,
breakpoints: {
'lt-sm': 1,
'lt-md': 2
}}"
label="{{ 'widgets.slider.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of sliderLayouts"
[value]="layout"
[image]="sliderLayoutImageMap.get(layout)">
{{ sliderLayoutTranslationMap.get(layout) | translate }}
</tb-image-cards-select-option>
</tb-image-cards-select>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="autoScale">
{{ 'widgets.slider.auto-scale' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitle">
{{ 'widget-config.title' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="titleFont"
clearButton
[previewText]="sliderWidgetConfigForm.get('title').value"
[initialPreviewStyle]="widgetConfig.config.titleStyle">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="titleColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon">
{{ 'widgets.slider.icon' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select fxFlex formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
[color]="sliderWidgetConfigForm.get('iconColor').value"
formControlName="icon">
</tb-material-icon-select>
<tb-color-input asBoxInput
colorClearButton
formControlName="iconColor">
</tb-color-input>
</div>
</div>
<div *ngIf="sliderWidgetConfigForm.get('layout').value !== sliderLayout.simplified" class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showValue">
{{ 'widgets.slider.value' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-unit-input class="flex" formControlName="valueUnits"></tb-unit-input>
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic">
<input matInput formControlName="valueDecimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix fxHide.lt-md translate>widget-config.decimals-suffix</div>
</mat-form-field>
<tb-font-settings formControlName="valueFont"
[previewText]="valuePreviewFn">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="valueColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.slider.range' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div class="tb-small-label" translate>widgets.slider.min</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="tickMin" type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<div class="tb-small-label" translate>widgets.slider.max</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="tickMax" type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<div class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide" formControlName="showTicks">
{{ 'widgets.slider.range-ticks' | translate }}
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-font-settings formControlName="ticksFont"
previewText="100">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="ticksColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide" formControlName="showTickMarks">
{{ 'widgets.slider.tick-marks' | translate }}
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="tickMarksCount" type="number" min="2"
step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-color-input asBoxInput
formControlName="tickMarksColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.slider.colors' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="12px">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.slider.main</div>
<tb-color-input asBoxInput
formControlName="mainColor">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.slider.background</div>
<tb-color-input asBoxInput
formControlName="backgroundColor">
</tb-color-input>
</div>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.rpc-state.disabled-state' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="12px">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.slider.main</div>
<tb-color-input asBoxInput
formControlName="mainColorDisabled">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.slider.background</div>
<tb-color-input asBoxInput
formControlName="backgroundColorDisabled">
</tb-color-input>
</div>
</div>
</div>
<div *ngIf="sliderWidgetConfigForm.get('layout').value === sliderLayout.extended"
class="tb-form-row column-xs">
<div class="fixed-title-width">
{{ 'widgets.slider.left-icon' | translate }}
</div>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="leftIconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select fxFlex formControlName="leftIconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
[color]="sliderWidgetConfigForm.get('leftIconColor').value"
formControlName="leftIcon">
</tb-material-icon-select>
<tb-color-input asBoxInput
formControlName="leftIconColor">
</tb-color-input>
</div>
</div>
<div *ngIf="sliderWidgetConfigForm.get('layout').value === sliderLayout.extended"
class="tb-form-row column-xs">
<div class="fixed-title-width">
{{ 'widgets.slider.right-icon' | translate }}
</div>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="rightIconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select fxFlex formControlName="rightIconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
[color]="sliderWidgetConfigForm.get('rightIconColor').value"
formControlName="rightIcon">
</tb-material-icon-select>
<tb-color-input asBoxInput
formControlName="rightIconColor">
</tb-color-input>
</div>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.background.background' | translate }}</div>
<tb-background-settings formControlName="background">
</tb-background-settings>
</div>
<div class="tb-form-row space-between column-lt-md">
<div translate>widget-config.show-card-buttons</div>
<mat-chip-listbox multiple formControlName="cardButtons">
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.card-border-radius' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<tb-widget-actions-panel
formControlName="actions">
</tb-widget-actions-panel>
</ng-container>

309
ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.ts

@ -0,0 +1,309 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { TargetDevice, WidgetConfig, } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { formatValue, isUndefined } from '@core/utils';
import { ValueType } from '@shared/models/constants';
import {
SliderLayout,
sliderLayoutImages,
sliderLayouts,
sliderLayoutTranslations,
sliderWidgetDefaultSettings,
SliderWidgetSettings
} from '@home/components/widget/lib/rpc/slider-widget.models';
import { cssSizeToStrSize, resolveCssSize } from '@shared/models/widget-settings.models';
@Component({
selector: 'tb-slider-basic-config',
templateUrl: './slider-basic-config.component.html',
styleUrls: ['../basic-config.scss']
})
export class SliderBasicConfigComponent extends BasicWidgetConfigComponent {
get targetDevice(): TargetDevice {
return this.sliderWidgetConfigForm.get('targetDevice').value;
}
sliderLayout = SliderLayout;
sliderLayouts = sliderLayouts;
sliderLayoutTranslationMap = sliderLayoutTranslations;
sliderLayoutImageMap = sliderLayoutImages;
valueType = ValueType;
sliderWidgetConfigForm: UntypedFormGroup;
valuePreviewFn = this._valuePreviewFn.bind(this);
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private fb: UntypedFormBuilder) {
super(store, widgetConfigComponent);
}
protected configForm(): UntypedFormGroup {
return this.sliderWidgetConfigForm;
}
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: SliderWidgetSettings = {...sliderWidgetDefaultSettings, ...(configData.config.settings || {})};
const iconSize = resolveCssSize(configData.config.iconSize);
this.sliderWidgetConfigForm = this.fb.group({
targetDevice: [configData.config.targetDevice, []],
initialState: [settings.initialState, []],
valueChange: [settings.valueChange, []],
disabledState: [settings.disabledState, []],
layout: [settings.layout, []],
autoScale: [settings.autoScale, []],
showTitle: [configData.config.showTitle, []],
title: [configData.config.title, []],
titleFont: [configData.config.titleFont, []],
titleColor: [configData.config.titleColor, []],
showIcon: [configData.config.showTitleIcon, []],
iconSize: [iconSize[0], [Validators.min(0)]],
iconSizeUnit: [iconSize[1], []],
icon: [configData.config.titleIcon, []],
iconColor: [configData.config.iconColor, []],
showValue: [settings.showValue, []],
valueUnits: [settings.valueUnits, []],
valueDecimals: [settings.valueDecimals, []],
valueFont: [settings.valueFont, []],
valueColor: [settings.valueColor, []],
tickMin: [settings.tickMin, []],
tickMax: [settings.tickMax, []],
showTicks: [settings.showTicks, []],
ticksFont: [settings.ticksFont, []],
ticksColor: [settings.ticksColor, []],
showTickMarks: [settings.showTickMarks, []],
tickMarksCount: [settings.tickMarksCount, [Validators.min(2)]],
tickMarksColor: [settings.tickMarksColor, []],
mainColor: [settings.mainColor, []],
backgroundColor: [settings.backgroundColor, []],
mainColorDisabled: [settings.mainColorDisabled, []],
backgroundColorDisabled: [settings.backgroundColorDisabled, []],
leftIconSize: [settings.leftIconSize, [Validators.min(0)]],
leftIconSizeUnit: [settings.leftIconSizeUnit, []],
leftIcon: [settings.leftIcon, []],
leftIconColor: [settings.leftIconColor, []],
rightIconSize: [settings.rightIconSize, [Validators.min(0)]],
rightIconSizeUnit: [settings.rightIconSizeUnit, []],
rightIcon: [settings.rightIcon, []],
rightIconColor: [settings.rightIconColor, []],
background: [settings.background, []],
cardButtons: [this.getCardButtons(configData.config), []],
borderRadius: [configData.config.borderRadius, []],
actions: [configData.config.actions || {}, []]
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.targetDevice = config.targetDevice;
this.widgetConfig.config.showTitle = config.showTitle;
this.widgetConfig.config.title = config.title;
this.widgetConfig.config.titleFont = config.titleFont;
this.widgetConfig.config.titleColor = config.titleColor;
this.widgetConfig.config.showTitleIcon = config.showIcon;
this.widgetConfig.config.iconSize = cssSizeToStrSize(config.iconSize, config.iconSizeUnit);
this.widgetConfig.config.titleIcon = config.icon;
this.widgetConfig.config.iconColor = config.iconColor;
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.initialState = config.initialState;
this.widgetConfig.config.settings.valueChange = config.valueChange;
this.widgetConfig.config.settings.disabledState = config.disabledState;
this.widgetConfig.config.settings.layout = config.layout;
this.widgetConfig.config.settings.autoScale = config.autoScale;
this.widgetConfig.config.settings.showValue = config.showValue;
this.widgetConfig.config.settings.valueUnits = config.valueUnits;
this.widgetConfig.config.settings.valueDecimals = config.valueDecimals;
this.widgetConfig.config.settings.valueFont = config.valueFont;
this.widgetConfig.config.settings.valueColor = config.valueColor;
this.widgetConfig.config.settings.tickMin = config.tickMin;
this.widgetConfig.config.settings.tickMax = config.tickMax;
this.widgetConfig.config.settings.showTicks = config.showTicks;
this.widgetConfig.config.settings.ticksFont = config.ticksFont;
this.widgetConfig.config.settings.ticksColor = config.ticksColor;
this.widgetConfig.config.settings.showTickMarks = config.showTickMarks;
this.widgetConfig.config.settings.tickMarksCount = config.tickMarksCount;
this.widgetConfig.config.settings.tickMarksColor = config.tickMarksColor;
this.widgetConfig.config.settings.mainColor = config.mainColor;
this.widgetConfig.config.settings.backgroundColor = config.backgroundColor;
this.widgetConfig.config.settings.mainColorDisabled = config.mainColorDisabled;
this.widgetConfig.config.settings.backgroundColorDisabled = config.backgroundColorDisabled;
this.widgetConfig.config.settings.leftIconSize = config.leftIconSize;
this.widgetConfig.config.settings.leftIconSizeUnit = config.leftIconSizeUnit;
this.widgetConfig.config.settings.leftIcon = config.leftIcon;
this.widgetConfig.config.settings.leftIconColor = config.leftIconColor;
this.widgetConfig.config.settings.rightIconSize = config.rightIconSize;
this.widgetConfig.config.settings.rightIconSizeUnit = config.rightIconSizeUnit;
this.widgetConfig.config.settings.rightIcon = config.rightIcon;
this.widgetConfig.config.settings.rightIconColor = config.rightIconColor;
this.widgetConfig.config.settings.background = config.background;
this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.borderRadius = config.borderRadius;
this.widgetConfig.config.actions = config.actions;
return this.widgetConfig;
}
protected validatorTriggers(): string[] {
return ['showTitle', 'showIcon', 'showValue', 'showTicks', 'showTickMarks', 'layout'];
}
protected updateValidators(emitEvent: boolean, trigger?: string) {
const showTitle: boolean = this.sliderWidgetConfigForm.get('showTitle').value;
const showIcon: boolean = this.sliderWidgetConfigForm.get('showIcon').value;
const showValue: boolean = this.sliderWidgetConfigForm.get('showValue').value;
const showTicks: boolean = this.sliderWidgetConfigForm.get('showTicks').value;
const showTickMarks: boolean = this.sliderWidgetConfigForm.get('showTickMarks').value;
const layout: SliderLayout = this.sliderWidgetConfigForm.get('layout').value;
const valueEnabled = layout !== SliderLayout.simplified;
const leftRightIconsEnabled = layout === SliderLayout.extended;
if (showTitle) {
this.sliderWidgetConfigForm.get('title').enable();
this.sliderWidgetConfigForm.get('titleFont').enable();
this.sliderWidgetConfigForm.get('titleColor').enable();
this.sliderWidgetConfigForm.get('showIcon').enable({emitEvent: false});
if (showIcon) {
this.sliderWidgetConfigForm.get('iconSize').enable();
this.sliderWidgetConfigForm.get('iconSizeUnit').enable();
this.sliderWidgetConfigForm.get('icon').enable();
this.sliderWidgetConfigForm.get('iconColor').enable();
} else {
this.sliderWidgetConfigForm.get('iconSize').disable();
this.sliderWidgetConfigForm.get('iconSizeUnit').disable();
this.sliderWidgetConfigForm.get('icon').disable();
this.sliderWidgetConfigForm.get('iconColor').disable();
}
} else {
this.sliderWidgetConfigForm.get('title').disable();
this.sliderWidgetConfigForm.get('titleFont').disable();
this.sliderWidgetConfigForm.get('titleColor').disable();
this.sliderWidgetConfigForm.get('showIcon').disable({emitEvent: false});
this.sliderWidgetConfigForm.get('iconSize').disable();
this.sliderWidgetConfigForm.get('iconSizeUnit').disable();
this.sliderWidgetConfigForm.get('icon').disable();
this.sliderWidgetConfigForm.get('iconColor').disable();
}
if (valueEnabled && showValue) {
this.sliderWidgetConfigForm.get('valueUnits').enable();
this.sliderWidgetConfigForm.get('valueDecimals').enable();
this.sliderWidgetConfigForm.get('valueFont').enable();
this.sliderWidgetConfigForm.get('valueColor').enable();
} else {
this.sliderWidgetConfigForm.get('valueUnits').disable();
this.sliderWidgetConfigForm.get('valueDecimals').disable();
this.sliderWidgetConfigForm.get('valueFont').disable();
this.sliderWidgetConfigForm.get('valueColor').disable();
}
if (showTicks) {
this.sliderWidgetConfigForm.get('ticksFont').enable();
this.sliderWidgetConfigForm.get('ticksColor').enable();
} else {
this.sliderWidgetConfigForm.get('ticksFont').disable();
this.sliderWidgetConfigForm.get('ticksColor').disable();
}
if (showTickMarks) {
this.sliderWidgetConfigForm.get('tickMarksCount').enable();
this.sliderWidgetConfigForm.get('tickMarksColor').enable();
} else {
this.sliderWidgetConfigForm.get('tickMarksCount').disable();
this.sliderWidgetConfigForm.get('tickMarksColor').disable();
}
if (leftRightIconsEnabled) {
this.sliderWidgetConfigForm.get('leftIconSize').enable();
this.sliderWidgetConfigForm.get('leftIconSizeUnit').enable();
this.sliderWidgetConfigForm.get('leftIcon').enable();
this.sliderWidgetConfigForm.get('leftIconColor').enable();
this.sliderWidgetConfigForm.get('rightIconSize').enable();
this.sliderWidgetConfigForm.get('rightIconSizeUnit').enable();
this.sliderWidgetConfigForm.get('rightIcon').enable();
this.sliderWidgetConfigForm.get('rightIconColor').enable();
} else {
this.sliderWidgetConfigForm.get('leftIconSize').disable();
this.sliderWidgetConfigForm.get('leftIconSizeUnit').disable();
this.sliderWidgetConfigForm.get('leftIcon').disable();
this.sliderWidgetConfigForm.get('leftIconColor').disable();
this.sliderWidgetConfigForm.get('rightIconSize').disable();
this.sliderWidgetConfigForm.get('rightIconSizeUnit').disable();
this.sliderWidgetConfigForm.get('rightIcon').disable();
this.sliderWidgetConfigForm.get('rightIconColor').disable();
}
}
private getCardButtons(config: WidgetConfig): string[] {
const buttons: string[] = [];
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) {
buttons.push('fullscreen');
}
return buttons;
}
private setCardButtons(buttons: string[], config: WidgetConfig) {
config.enableFullscreen = buttons.includes('fullscreen');
}
private _valuePreviewFn(): string {
const units: string = this.sliderWidgetConfigForm.get('valueUnits').value;
const decimals: number = this.sliderWidgetConfigForm.get('valueDecimals').value;
return formatValue(48, decimals, units, false);
}
}

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

@ -62,8 +62,6 @@ export abstract class BasicActionWidgetComponent implements OnInit, OnDestroy, A
loading$ = this.loadingSubject.asObservable().pipe(share()); loading$ = this.loadingSubject.asObservable().pipe(share());
error = '';
protected constructor(protected cd: ChangeDetectorRef) { protected constructor(protected cd: ChangeDetectorRef) {
} }
@ -91,14 +89,15 @@ export abstract class BasicActionWidgetComponent implements OnInit, OnDestroy, A
ngOnDestroy() { ngOnDestroy() {
this.valueActions.forEach(v => v.destroy()); this.valueActions.forEach(v => v.destroy());
this.loadingSubject.complete();
this.loadingSubject.unsubscribe();
} }
public onInit() { public onInit() {
} }
public clearError() { public clearError() {
this.error = ''; this.ctx.hideToast(this.ctx.toastTargetId);
this.cd.markForCheck();
} }
protected createValueGetter<V>(getValueSettings: GetValueSettings<V>, protected createValueGetter<V>(getValueSettings: GetValueSettings<V>,
@ -111,7 +110,8 @@ export abstract class BasicActionWidgetComponent implements OnInit, OnDestroy, A
} }
}, },
error: (err: any) => { error: (err: any) => {
this.onError(err); const message = parseError(this.ctx, err);
this.onError(message);
if (valueObserver?.error) { if (valueObserver?.error) {
valueObserver.error(err); valueObserver.error(err);
} }
@ -130,8 +130,7 @@ export abstract class BasicActionWidgetComponent implements OnInit, OnDestroy, A
} }
private onError(error: string) { private onError(error: string) {
this.error = error; this.ctx.showErrorToast(error, 'bottom', 'center', this.ctx.toastTargetId, true);
this.cd.markForCheck();
} }
protected updateValue<V>(valueSetter: ValueSetter<V>, protected updateValue<V>(valueSetter: ValueSetter<V>,
@ -293,6 +292,8 @@ export class ValueToDataConverter<V> {
constructor(protected settings: ValueToDataSettings) { constructor(protected settings: ValueToDataSettings) {
switch (settings.type) { switch (settings.type) {
case ValueToDataType.VALUE:
break;
case ValueToDataType.CONSTANT: case ValueToDataType.CONSTANT:
this.constantValue = this.settings.constantValue; this.constantValue = this.settings.constantValue;
break; break;
@ -310,6 +311,8 @@ export class ValueToDataConverter<V> {
valueToData(value: V): any { valueToData(value: V): any {
switch (this.settings.type) { switch (this.settings.type) {
case ValueToDataType.VALUE:
return value;
case ValueToDataType.CONSTANT: case ValueToDataType.CONSTANT:
return this.constantValue; return this.constantValue;
case ValueToDataType.FUNCTION: case ValueToDataType.FUNCTION:

31
ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.scss

@ -19,34 +19,3 @@
left: 0; left: 0;
right: 0; right: 0;
} }
.tb-action-widget-error-container {
position: absolute;
bottom: 0;
left: 0;
right: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 1;
.tb-action-widget-error-panel {
display: flex;
padding: 4px 4px 4px 12px;
justify-content: center;
align-items: center;
gap: 4px;
border-radius: 4px;
background-color: #fff2f3;
box-shadow: -2px 2px 4px 0px rgba(0,0,0,0.2);
.tb-action-widget-error-text {
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
color: rgba(209, 39, 48, 1);
}
.tb-action-widget-error-clear {
color: rgba(209, 39, 48, 1);
}
}
}

6
ui-ngx/src/app/modules/home/components/widget/lib/button/action-button-widget.component.html

@ -27,10 +27,4 @@
[ctx]="ctx" [ctx]="ctx"
(clicked)="onClick($event)"> (clicked)="onClick($event)">
</tb-widget-button> </tb-widget-button>
<div *ngIf="error" class="tb-action-widget-error-container">
<div class="tb-action-widget-error-panel">
<div class="tb-action-widget-error-text" [innerHTML]="error | safe: 'html'"></div>
<button class="tb-action-widget-error-clear tb-mat-20" mat-icon-button (click)="clearError()"><mat-icon>close</mat-icon></button>
</div>
</div>
</div> </div>

6
ui-ngx/src/app/modules/home/components/widget/lib/button/command-button-widget.component.html

@ -27,10 +27,4 @@
(clicked)="onClick($event)"> (clicked)="onClick($event)">
</tb-widget-button> </tb-widget-button>
<mat-progress-bar class="tb-action-widget-progress" style="height: 4px;" color="accent" mode="indeterminate" *ngIf="loading$ | async"></mat-progress-bar> <mat-progress-bar class="tb-action-widget-progress" style="height: 4px;" color="accent" mode="indeterminate" *ngIf="loading$ | async"></mat-progress-bar>
<div *ngIf="error" class="tb-action-widget-error-container">
<div class="tb-action-widget-error-panel">
<div class="tb-action-widget-error-text" [innerHTML]="error | safe: 'html'"></div>
<button class="tb-action-widget-error-clear tb-mat-20" mat-icon-button (click)="clearError()"><mat-icon>close</mat-icon></button>
</div>
</div>
</div> </div>

2
ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts

@ -241,8 +241,8 @@ export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit
} else { } else {
aggValue.value = 'N/A'; aggValue.value = 'N/A';
} }
aggValue.color.update(value);
const numeric = formatNumberValue(value, (aggValue.key.decimals || this.ctx.decimals)); const numeric = formatNumberValue(value, (aggValue.key.decimals || this.ctx.decimals));
aggValue.color.update(numeric);
if (aggValue.showArrow && isDefined(numeric)) { if (aggValue.showArrow && isDefined(numeric)) {
aggValue.upArrow = numeric > 0; aggValue.upArrow = numeric > 0;
aggValue.downArrow = numeric < 0; aggValue.downArrow = numeric < 0;

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

@ -127,11 +127,11 @@ export class GatewayConfigurationComponent implements OnInit {
type: [StorageTypes.MEMORY, [Validators.required]], type: [StorageTypes.MEMORY, [Validators.required]],
read_records_count: [100, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required, Validators.pattern(/^[^.\s]+$/)]], read_records_count: [100, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required, Validators.pattern(/^[^.\s]+$/)]],
max_records_count: [100000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required, Validators.pattern(/^[^.\s]+$/)]], max_records_count: [100000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required, Validators.pattern(/^[^.\s]+$/)]],
data_folder_path: ['./data/', [Validators.pattern(/^[^\s]+$/)]], data_folder_path: ['./data/', [Validators.required]],
max_file_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], max_file_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
max_read_records_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], max_read_records_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
max_records_per_file: [10000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], max_records_per_file: [10000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
data_file_path: ['./data/data.db', [Validators.pattern(/^[^\s]+$/)]], data_file_path: ['./data/data.db', [Validators.required]],
messages_ttl_check_in_hours: [1, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], messages_ttl_check_in_hours: [1, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
messages_ttl_in_days: [7, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], messages_ttl_in_days: [7, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
@ -240,7 +240,7 @@ export class GatewayConfigurationComponent implements OnInit {
storageGroup.get('read_records_count').updateValueAndValidity({emitEvent: false}); storageGroup.get('read_records_count').updateValueAndValidity({emitEvent: false});
storageGroup.get('max_records_count').updateValueAndValidity({emitEvent: false}); storageGroup.get('max_records_count').updateValueAndValidity({emitEvent: false});
} else if (type === StorageTypes.FILE) { } else if (type === StorageTypes.FILE) {
storageGroup.get('data_folder_path').addValidators([Validators.required, Validators.pattern(/^[^.\s]+$/)]); storageGroup.get('data_folder_path').addValidators([Validators.required]);
storageGroup.get('max_file_count').addValidators( storageGroup.get('max_file_count').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('max_read_records_count').addValidators( storageGroup.get('max_read_records_count').addValidators(
@ -252,7 +252,7 @@ export class GatewayConfigurationComponent implements OnInit {
storageGroup.get('max_read_records_count').updateValueAndValidity({emitEvent: false}); storageGroup.get('max_read_records_count').updateValueAndValidity({emitEvent: false});
storageGroup.get('max_records_per_file').updateValueAndValidity({emitEvent: false}); storageGroup.get('max_records_per_file').updateValueAndValidity({emitEvent: false});
} else if (type === StorageTypes.SQLITE) { } else if (type === StorageTypes.SQLITE) {
storageGroup.get('data_file_path').addValidators([Validators.required, Validators.pattern(/^[^.\s]+$/)]); storageGroup.get('data_file_path').addValidators([Validators.required]);
storageGroup.get('messages_ttl_check_in_hours').addValidators( storageGroup.get('messages_ttl_check_in_hours').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('messages_ttl_in_days').addValidators( storageGroup.get('messages_ttl_in_days').addValidators(

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

@ -80,7 +80,7 @@
</ng-container> </ng-container>
<ng-container matColumnDef="actions" stickyEnd> <ng-container matColumnDef="actions" stickyEnd>
<mat-header-cell *matHeaderCellDef <mat-header-cell *matHeaderCellDef
[ngStyle.gt-md]="{ minWidth: '144px', maxWidth: '144px', width: '144px'}"> [ngStyle.gt-md]="{ minWidth: '144px', maxWidth: '144px', width: '144px', textAlign: 'center'}">
{{ 'gateway.connectors-table-actions' | translate }} {{ 'gateway.connectors-table-actions' | translate }}
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let attribute" <mat-cell *matCellDef="let attribute"

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

@ -30,6 +30,13 @@
flex-direction: column; flex-direction: column;
} }
& > section:not(.table-section) {
max-width: unset;
@media #{$mat-gt-md} {
max-width: 50%;
}
}
.table-section { .table-section {
min-height: 35vh; min-height: 35vh;
overflow: hidden; overflow: hidden;

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

@ -366,7 +366,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
type: 'success', type: 'success',
duration: 1000, duration: 1000,
verticalPosition: 'top', verticalPosition: 'top',
horizontalPosition: 'right', horizontalPosition: 'left',
target: 'dashboardRoot', target: 'dashboardRoot',
forceDismiss: true forceDismiss: true
})); }));
@ -381,6 +381,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
this.initialConnector = attribute.value;
const title = `Delete connector ${attribute.key}?`; const title = `Delete connector ${attribute.key}?`;
const content = `All connector data will be deleted.`; const content = `All connector data will be deleted.`;
this.dialogService.confirm(title, content, 'Cancel', 'Delete').subscribe(result => { this.dialogService.confirm(title, content, 'Cancel', 'Delete').subscribe(result => {

63
ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts

@ -106,6 +106,7 @@ export class LiquidLevelWidgetComponent implements OnInit {
private volume: number; private volume: number;
private tooltipContent: string; private tooltipContent: string;
private widgetUnits: string; private widgetUnits: string;
private volumeUnits: string;
private capacityUnits = Object.values(CapacityUnits); private capacityUnits = Object.values(CapacityUnits);
@ -128,7 +129,7 @@ export class LiquidLevelWidgetComponent implements OnInit {
this.getData().subscribe(data => { this.getData().subscribe(data => {
if (data) { if (data) {
const { svg, volume, units } = data; const { svg, volume, units, volumeUnits } = data;
if (svg && isNotEmptyStr(svg) && this.liquidLevelContent.nativeElement) { if (svg && isNotEmptyStr(svg) && this.liquidLevelContent.nativeElement) {
const jQueryContainerElement = $(this.liquidLevelContent.nativeElement); const jQueryContainerElement = $(this.liquidLevelContent.nativeElement);
jQueryContainerElement.html(svg); jQueryContainerElement.html(svg);
@ -145,6 +146,10 @@ export class LiquidLevelWidgetComponent implements OnInit {
this.volume = Number(volume); this.volume = Number(volume);
} }
if (volumeUnits) {
this.volumeUnits = volumeUnits;
}
if (units) { if (units) {
this.widgetUnits = units; this.widgetUnits = units;
} }
@ -164,7 +169,7 @@ export class LiquidLevelWidgetComponent implements OnInit {
this.tooltipDateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.tooltipDateFormat); this.tooltipDateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.tooltipDateFormat);
} }
private getData(): Observable<{ svg: string; volume: number; units: string }> { private getData(): Observable<{ svg: string; volume: number; units: string; volumeUnits: string}> {
if (this.ctx.datasources?.length) { if (this.ctx.datasources?.length) {
const entityId: EntityId = { const entityId: EntityId = {
entityType: this.ctx.datasources[0].entityType, entityType: this.ctx.datasources[0].entityType,
@ -308,7 +313,7 @@ export class LiquidLevelWidgetComponent implements OnInit {
.pipe(map(attributes => { .pipe(map(attributes => {
const shape = extractValue<Shapes>(attributes, this.settings.shapeAttributeName); const shape = extractValue<Shapes>(attributes, this.settings.shapeAttributeName);
if (!shape || !svgMapping.has(shape)) { if (!shape || !svgMapping.has(shape)) {
this.createdErrorMgs(this.settings.shapeAttributeName, isUndefinedOrNull(shape) || isEmptyStr(shape)); this.createdErrorMsg(this.settings.shapeAttributeName, isUndefinedOrNull(shape) || isEmptyStr(shape));
return this.settings.selectedShape; return this.settings.selectedShape;
} }
return shape; return shape;
@ -318,12 +323,15 @@ export class LiquidLevelWidgetComponent implements OnInit {
return of(this.settings.selectedShape); return of(this.settings.selectedShape);
} }
private getTankersParams(entityId: EntityId): Observable<{ volume: number; units: string }> { private getTankersParams(entityId: EntityId): Observable<{ volume: number; units: string; volumeUnits: string }> {
const isVolumeStatic = this.settings.layout !== LevelCardLayout.absolute const isVolumeStatic = this.settings.layout !== LevelCardLayout.absolute
&& this.settings.datasourceUnits === CapacityUnits.percent && this.settings.datasourceUnits === CapacityUnits.percent
|| this.settings.volumeSource === LiquidWidgetDataSourceType.static; || this.settings.volumeSource === LiquidWidgetDataSourceType.static;
const isUnitStatic = this.settings.layout !== LevelCardLayout.absolute || const isUnitStatic = this.settings.layout !== LevelCardLayout.absolute ||
this.settings.widgetUnitsSource === LiquidWidgetDataSourceType.static; this.settings.widgetUnitsSource === LiquidWidgetDataSourceType.static;
const isVolumeUnitStatic = this.settings.layout !== LevelCardLayout.absolute
&& this.settings.datasourceUnits === CapacityUnits.percent
|| this.settings.volumeUnitsSource === LiquidWidgetDataSourceType.static;
const attributeKeys: string[] = []; const attributeKeys: string[] = [];
@ -335,20 +343,29 @@ export class LiquidLevelWidgetComponent implements OnInit {
attributeKeys.push(this.settings.widgetUnitsAttributeName); attributeKeys.push(this.settings.widgetUnitsAttributeName);
} }
if (!isVolumeUnitStatic) {
attributeKeys.push(this.settings.volumeUnitsAttributeName);
}
if (!attributeKeys.length || entityId.id === NULL_UUID) { if (!attributeKeys.length || entityId.id === NULL_UUID) {
return of({ return of({
volume: this.settings.volumeConstant, volume: this.settings.volumeConstant,
volumeUnits: this.settings.volumeUnits,
units: this.settings.units units: this.settings.units
}); });
} }
return this.ctx.attributeService.getEntityAttributes(entityId, null, attributeKeys).pipe( return this.ctx.attributeService.getEntityAttributes(entityId, null, attributeKeys).pipe(
map(attributes => { map(attributes => {
let volume = isVolumeStatic ? this.settings.volumeConstant : extractValue<number>(attributes, this.settings.volumeAttributeName); let volume = isVolumeStatic ? this.settings.volumeConstant :
let units = isUnitStatic ? this.settings.units : extractValue<string>(attributes, this.settings.widgetUnitsAttributeName); extractValue<number>(attributes, this.settings.volumeAttributeName);
let volumeUnits = isVolumeUnitStatic ? this.settings.volumeUnits :
extractValue<string>(attributes, this.settings.volumeUnitsAttributeName);
let units = isUnitStatic ? this.settings.units :
extractValue<string>(attributes, this.settings.widgetUnitsAttributeName);
if (!isVolumeStatic && (!volume || !isNumeric(volume) || volume < 0.1)) { if (!isVolumeStatic && (!volume || !isNumeric(volume) || volume < 0.1)) {
this.createdErrorMgs(this.settings.volumeAttributeName, isUndefinedOrNull(volume) || isEmptyStr(volume)); this.createdErrorMsg(this.settings.volumeAttributeName, isUndefinedOrNull(volume) || isEmptyStr(volume));
volume = this.settings.volumeConstant; volume = this.settings.volumeConstant;
} }
@ -358,20 +375,33 @@ export class LiquidLevelWidgetComponent implements OnInit {
units = this.capacityUnits.find(unit => unit.normalize() === normalizeUnits); units = this.capacityUnits.find(unit => unit.normalize() === normalizeUnits);
} }
if (isUndefinedOrNull(units) || !isNotEmptyStr(units)) { if (isUndefinedOrNull(units) || !isNotEmptyStr(units)) {
this.createdErrorMgs(this.settings.widgetUnitsAttributeName, isUndefinedOrNull(units) || isEmptyStr(units)); this.createdErrorMsg(this.settings.widgetUnitsAttributeName, isUndefinedOrNull(units) || isEmptyStr(units));
units = this.settings.units; units = this.settings.units;
} }
} }
if (!isVolumeUnitStatic) {
if (isNotEmptyStr(volumeUnits)) {
const normalizeUnits = volumeUnits.normalize().trim();
volumeUnits = this.capacityUnits.find(unit => unit.normalize() === normalizeUnits);
}
if (isUndefinedOrNull(volumeUnits) || !isNotEmptyStr(volumeUnits)) {
this.createdErrorMsg(this.settings.widgetUnitsAttributeName,
isUndefinedOrNull(volumeUnits) || isEmptyStr(volumeUnits));
volumeUnits = this.settings.volumeUnits;
}
}
return { return {
volume, volume,
volumeUnits,
units units
}; };
}) })
); );
} }
private createdErrorMgs(attributeName: string, isEmpty = false) { private createdErrorMsg(attributeName: string, isEmpty = false) {
if (isEmpty) { if (isEmpty) {
this.errorsMsg.push(this.translate.instant('widgets.liquid-level-card.attribute-key-not-set', {attributeName})); this.errorsMsg.push(this.translate.instant('widgets.liquid-level-card.attribute-key-not-set', {attributeName}));
} else { } else {
@ -474,9 +504,14 @@ export class LiquidLevelWidgetComponent implements OnInit {
} }
if (this.settings.layout === LevelCardLayout.absolute) { if (this.settings.layout === LevelCardLayout.absolute) {
const volumeInLiters: number = convertLiters(this.volume, this.settings.volumeUnits as CapacityUnits, ConversionType.to); let volume: number | string;
const volume = convertLiters(volumeInLiters, this.widgetUnits as CapacityUnits, ConversionType.from) if (this.widgetUnits !== CapacityUnits.percent) {
.toFixed(this.settings.decimals || 0); const volumeInLiters: number = convertLiters(this.volume, this.volumeUnits as CapacityUnits, ConversionType.to);
volume = convertLiters(volumeInLiters, this.widgetUnits as CapacityUnits, ConversionType.from)
.toFixed(this.settings.decimals || 0);
} else {
volume = this.volume.toFixed(this.settings.decimals || 0);
}
const volumeTextStyle = cssTextFromInlineStyle({...inlineTextStyle(this.settings.volumeFont), const volumeTextStyle = cssTextFromInlineStyle({...inlineTextStyle(this.settings.volumeFont),
color: this.settings.volumeColor}); color: this.settings.volumeColor});
@ -553,7 +588,7 @@ export class LiquidLevelWidgetComponent implements OnInit {
private convertInputData(value: any): number { private convertInputData(value: any): number {
if (this.settings.datasourceUnits !== CapacityUnits.percent) { if (this.settings.datasourceUnits !== CapacityUnits.percent) {
return (convertLiters(Number(value), this.settings.datasourceUnits, ConversionType.to) / return (convertLiters(Number(value), this.settings.datasourceUnits, ConversionType.to) /
convertLiters(this.volume, this.settings.volumeUnits, ConversionType.to)) * 100; convertLiters(this.volume, this.volumeUnits as CapacityUnits, ConversionType.to)) * 100;
} }
return Number(value); return Number(value);
@ -561,7 +596,7 @@ export class LiquidLevelWidgetComponent implements OnInit {
private convertOutputData(value: number): number { private convertOutputData(value: number): number {
if (this.widgetUnits !== CapacityUnits.percent) { if (this.widgetUnits !== CapacityUnits.percent) {
return convertLiters(this.volume * (value / 100), this.settings.volumeUnits, ConversionType.to); return convertLiters(this.volume * (value / 100), this.volumeUnits as CapacityUnits, ConversionType.to);
} }
return value; return value;

31
ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.models.ts

@ -56,6 +56,8 @@ export interface LevelCardWidgetSettings extends WidgetConfig {
volumeSource: LiquidWidgetDataSourceType; volumeSource: LiquidWidgetDataSourceType;
volumeConstant: number; volumeConstant: number;
volumeAttributeName: string; volumeAttributeName: string;
volumeUnitsSource: LiquidWidgetDataSourceType;
volumeUnitsAttributeName: string;
volumeUnits: CapacityUnits; volumeUnits: CapacityUnits;
volumeFont: Font; volumeFont: Font;
volumeColor: string; volumeColor: string;
@ -257,8 +259,10 @@ export const levelCardDefaultSettings: LevelCardWidgetSettings = {
iconColor: '#5469FF', iconColor: '#5469FF',
volumeSource: LiquidWidgetDataSourceType.static, volumeSource: LiquidWidgetDataSourceType.static,
volumeConstant: 500, volumeConstant: 500,
volumeUnits: CapacityUnits.liters,
volumeAttributeName: 'volume', volumeAttributeName: 'volume',
volumeUnitsSource: LiquidWidgetDataSourceType.static,
volumeUnitsAttributeName: 'volumeUnits',
volumeUnits: CapacityUnits.liters,
volumeFont: { volumeFont: {
family: 'Roboto', family: 'Roboto',
size: 14, size: 14,
@ -375,9 +379,7 @@ export const convertLiters = (value: number, units: CapacityUnits, conversionTyp
return conversionType === ConversionType.to ? value / factor : value * factor; return conversionType === ConversionType.to ? value / factor : value * factor;
}; };
export const extractValue = <T>(attributes: Array<AttributeData>, attributeName: string): T | undefined => { export const extractValue = <T>(attributes: Array<AttributeData>, attributeName: string): T | undefined => attributes.find(attr => attr.key === attributeName)?.value;
return attributes.find(attr => attr.key === attributeName)?.value;
};
export const valueContainerStyleDefaults = cssTextFromInlineStyle({ export const valueContainerStyleDefaults = cssTextFromInlineStyle({
width: '100%', width: '100%',
@ -494,6 +496,7 @@ export const updatedFormSettingsValidators = (formGroup: FormGroup) => {
const datasourceUnits: string = formGroup.get('datasourceUnits').value; const datasourceUnits: string = formGroup.get('datasourceUnits').value;
const layout: LevelCardLayout = formGroup.get('layout').value; const layout: LevelCardLayout = formGroup.get('layout').value;
const volumeSource: LiquidWidgetDataSourceType = formGroup.get('volumeSource').value; const volumeSource: LiquidWidgetDataSourceType = formGroup.get('volumeSource').value;
const volumeUnitsSource: LiquidWidgetDataSourceType = formGroup.get('volumeUnitsSource').value;
const widgetUnitsSource: LiquidWidgetDataSourceType = formGroup.get('widgetUnitsSource').value; const widgetUnitsSource: LiquidWidgetDataSourceType = formGroup.get('widgetUnitsSource').value;
const showTooltipLevel: boolean = formGroup.get('showTooltipLevel').value; const showTooltipLevel: boolean = formGroup.get('showTooltipLevel').value;
const showTooltipDate: boolean = formGroup.get('showTooltipDate').value; const showTooltipDate: boolean = formGroup.get('showTooltipDate').value;
@ -517,7 +520,7 @@ export const updatedFormSettingsValidators = (formGroup: FormGroup) => {
if (datasourceUnits !== CapacityUnits.percent) { if (datasourceUnits !== CapacityUnits.percent) {
formGroup.get('volumeSource').enable({emitEvent: false}); formGroup.get('volumeSource').enable({emitEvent: false});
formGroup.get('volumeUnits').enable({emitEvent: false}); formGroup.get('volumeUnitsSource').enable({emitEvent: false});
if (volumeSource === LiquidWidgetDataSourceType.static) { if (volumeSource === LiquidWidgetDataSourceType.static) {
formGroup.get('volumeConstant').enable({emitEvent: false}); formGroup.get('volumeConstant').enable({emitEvent: false});
formGroup.get('volumeAttributeName').disable({emitEvent: false}); formGroup.get('volumeAttributeName').disable({emitEvent: false});
@ -525,11 +528,20 @@ export const updatedFormSettingsValidators = (formGroup: FormGroup) => {
formGroup.get('volumeConstant').disable({emitEvent: false}); formGroup.get('volumeConstant').disable({emitEvent: false});
formGroup.get('volumeAttributeName').enable({emitEvent: false}); formGroup.get('volumeAttributeName').enable({emitEvent: false});
} }
if (volumeUnitsSource === LiquidWidgetDataSourceType.static) {
formGroup.get('volumeUnits').enable({emitEvent: false});
formGroup.get('volumeUnitsAttributeName').disable({emitEvent: false});
} else {
formGroup.get('volumeUnits').disable({emitEvent: false});
formGroup.get('volumeUnitsAttributeName').enable({emitEvent: false});
}
} else { } else {
formGroup.get('volumeSource').disable({emitEvent: false}); formGroup.get('volumeSource').disable({emitEvent: false});
formGroup.get('volumeConstant').disable({emitEvent: false}); formGroup.get('volumeConstant').disable({emitEvent: false});
formGroup.get('volumeAttributeName').disable({emitEvent: false}); formGroup.get('volumeAttributeName').disable({emitEvent: false});
formGroup.get('volumeUnitsSource').disable({emitEvent: false});
formGroup.get('volumeUnits').disable({emitEvent: false}); formGroup.get('volumeUnits').disable({emitEvent: false});
formGroup.get('volumeUnitsAttributeName').disable({emitEvent: false});
} }
if (layout === LevelCardLayout.simple) { if (layout === LevelCardLayout.simple) {
@ -557,7 +569,7 @@ export const updatedFormSettingsValidators = (formGroup: FormGroup) => {
} }
formGroup.get('volumeSource').enable({emitEvent: false}); formGroup.get('volumeSource').enable({emitEvent: false});
formGroup.get('volumeUnits').enable({emitEvent: false}); formGroup.get('volumeUnitsSource').enable({emitEvent: false});
if (volumeSource === LiquidWidgetDataSourceType.static) { if (volumeSource === LiquidWidgetDataSourceType.static) {
formGroup.get('volumeConstant').enable({emitEvent: false}); formGroup.get('volumeConstant').enable({emitEvent: false});
formGroup.get('volumeAttributeName').disable({emitEvent: false}); formGroup.get('volumeAttributeName').disable({emitEvent: false});
@ -565,6 +577,13 @@ export const updatedFormSettingsValidators = (formGroup: FormGroup) => {
formGroup.get('volumeConstant').disable({emitEvent: false}); formGroup.get('volumeConstant').disable({emitEvent: false});
formGroup.get('volumeAttributeName').enable({emitEvent: false}); formGroup.get('volumeAttributeName').enable({emitEvent: false});
} }
if (volumeUnitsSource === LiquidWidgetDataSourceType.static) {
formGroup.get('volumeUnits').enable({emitEvent: false});
formGroup.get('volumeUnitsAttributeName').disable({emitEvent: false});
} else {
formGroup.get('volumeUnits').disable({emitEvent: false});
formGroup.get('volumeUnitsAttributeName').enable({emitEvent: false});
}
if (formGroup.get('decimals')) { if (formGroup.get('decimals')) {
formGroup.get('decimals').enable({emitEvent: false}); formGroup.get('decimals').enable({emitEvent: false});

23
ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts

@ -32,7 +32,7 @@ import {
WidgetUnitedMapSettings WidgetUnitedMapSettings
} from './map-models'; } from './map-models';
import { Marker } from './markers'; import { Marker } from './markers';
import { map, Observable, of, switchMap } from 'rxjs'; import { map, Observable, of } from 'rxjs';
import { Polyline } from './polyline'; import { Polyline } from './polyline';
import { Polygon } from './polygon'; import { Polygon } from './polygon';
import { Circle } from './circle'; import { Circle } from './circle';
@ -64,6 +64,7 @@ import { MatDialog } from '@angular/material/dialog';
import { FormattedData, ReplaceInfo } from '@shared/models/widget.models'; import { FormattedData, ReplaceInfo } from '@shared/models/widget.models';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import { ImagePipe } from '@shared/pipe/image.pipe'; import { ImagePipe } from '@shared/pipe/image.pipe';
import { take, tap } from 'rxjs/operators';
export default abstract class LeafletMap { export default abstract class LeafletMap {
@ -940,7 +941,12 @@ export default abstract class LeafletMap {
this.markersData = markersData; this.markersData = markersData;
if (this.options.useClusterMarkers) { if (this.options.useClusterMarkers) {
if (createdMarkers.length) { if (createdMarkers.length) {
this.markersCluster.addLayers(createdMarkers.map(marker => marker.leafletMarker)); createdMarkers.forEach((marker) => {
marker.createMarkerIconSubject.pipe(
tap(() => this.markersCluster.addLayer(marker.leafletMarker)),
take(1)
).subscribe();
});
} }
if (updatedMarkers.length) { if (updatedMarkers.length) {
this.markersCluster.refreshClusters(updatedMarkers.map(marker => marker.leafletMarker)); this.markersCluster.refreshClusters(updatedMarkers.map(marker => marker.leafletMarker));
@ -971,10 +977,15 @@ export default abstract class LeafletMap {
} }
this.markers.set(key, newMarker); this.markers.set(key, newMarker);
if (!this.options.useClusterMarkers) { if (!this.options.useClusterMarkers) {
this.map.addLayer(newMarker.leafletMarker); newMarker.createMarkerIconSubject.pipe(
if (this.map.pm.globalDragModeEnabled() && newMarker.leafletMarker.pm) { tap(() => {
newMarker.leafletMarker.pm.enableLayerDrag(); this.map.addLayer(newMarker.leafletMarker);
} if (this.map.pm.globalDragModeEnabled() && newMarker.leafletMarker.pm) {
newMarker.leafletMarker.pm.enableLayerDrag();
}
}),
take(1)
).subscribe();
} }
return newMarker; return newMarker;
} }

3
ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts

@ -23,6 +23,7 @@ import { fillDataPattern, isDefined, isDefinedAndNotNull, processDataPattern, sa
import LeafletMap from './leaflet-map'; import LeafletMap from './leaflet-map';
import { FormattedData } from '@shared/models/widget.models'; import { FormattedData } from '@shared/models/widget.models';
import { ImagePipe } from '@shared/pipe/image.pipe'; import { ImagePipe } from '@shared/pipe/image.pipe';
import { ReplaySubject } from 'rxjs';
export class Marker { export class Marker {
@ -33,6 +34,7 @@ export class Marker {
tooltipOffset: L.LatLngTuple; tooltipOffset: L.LatLngTuple;
markerOffset: L.LatLngTuple; markerOffset: L.LatLngTuple;
tooltip: L.Popup; tooltip: L.Popup;
createMarkerIconSubject = new ReplaySubject<MarkerIconInfo>();
constructor(private map: LeafletMap, constructor(private map: LeafletMap,
private location: L.LatLng, private location: L.LatLng,
@ -148,6 +150,7 @@ export class Marker {
this.labelOffset = [0, -iconInfo.size[1] * this.markerOffset[1] + 10]; this.labelOffset = [0, -iconInfo.size[1] * this.markerOffset[1] + 10];
} }
this.updateMarkerLabel(settings); this.updateMarkerLabel(settings);
this.createMarkerIconSubject.next(iconInfo);
}); });
} }

26
ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.html

@ -0,0 +1,26 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-power-button-panel" [style]="backgroundStyle$ | async">
<div class="tb-power-button-overlay" [style]="overlayStyle"></div>
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
<div class="tb-power-button-content">
<div #powerButtonShape class="tb-power-button-shape" [class.tb-power-button-pointer]="!disabled && (loading$ | async) === false">
</div>
</div>
<mat-progress-bar class="tb-action-widget-progress" style="height: 4px;" color="accent" mode="indeterminate" *ngIf="loading$ | async"></mat-progress-bar>
</div>

64
ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.scss

@ -0,0 +1,64 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-power-button-panel {
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
gap: 16px;
padding: 20px 24px 24px 24px;
> div:not(.tb-power-button-overlay) {
z-index: 1;
}
.tb-power-button-overlay {
position: absolute;
top: 12px;
left: 12px;
bottom: 12px;
right: 12px;
}
div.tb-widget-title {
padding: 0;
}
.tb-power-button-content {
flex: 1;
min-width: 0;
min-height: 0;
.tb-power-button-shape {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
svg {
.tb-small-shadow {
filter: drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.2));
}
.tb-shadow {
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.15));
}
}
&.tb-power-button-pointer {
svg {
.tb-hover-circle {
cursor: pointer;
}
}
}
}
}
}

203
ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.ts

@ -0,0 +1,203 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
OnDestroy,
OnInit,
Renderer2,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { BasicActionWidgetComponent, ValueSetter } from '@home/components/widget/lib/action/action-widget.models';
import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/widget-settings.models';
import { Observable } from 'rxjs';
import { ResizeObserver } from '@juggle/resize-observer';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import { ValueType } from '@shared/models/constants';
import {
powerButtonDefaultSettings,
PowerButtonShape,
powerButtonShapeSize,
PowerButtonWidgetSettings
} from '@home/components/widget/lib/rpc/power-button-widget.models';
import { SVG, Svg } from '@svgdotjs/svg.js';
@Component({
selector: 'tb-power-button-widget',
templateUrl: './power-button-widget.component.html',
styleUrls: ['../action/action-widget.scss', './power-button-widget.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class PowerButtonWidgetComponent extends
BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('powerButtonShape', {static: false})
powerButtonShape: ElementRef<HTMLElement>;
settings: PowerButtonWidgetSettings;
backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {};
value = false;
disabled = false;
private shapeResize$: ResizeObserver;
private drawSvgShapePending = false;
private svgShape: Svg;
private powerButtonSvgShape: PowerButtonShape;
private disabledState = false;
private onValueSetter: ValueSetter<boolean>;
private offValueSetter: ValueSetter<boolean>;
constructor(protected imagePipe: ImagePipe,
protected sanitizer: DomSanitizer,
private renderer: Renderer2,
protected cd: ChangeDetectorRef) {
super(cd);
}
ngOnInit(): void {
super.ngOnInit();
this.settings = {...powerButtonDefaultSettings, ...this.ctx.settings};
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer);
this.overlayStyle = overlayStyle(this.settings.background.overlay);
const getInitialStateSettings =
{...this.settings.initialState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.initial-state')};
this.createValueGetter(getInitialStateSettings, ValueType.BOOLEAN, {
next: (value) => this.onValue(value)
});
const disabledStateSettings =
{...this.settings.disabledState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.disabled-state')};
this.createValueGetter(disabledStateSettings, ValueType.BOOLEAN, {
next: (value) => this.onDisabled(value)
});
const onUpdateStateSettings = {...this.settings.onUpdateState,
actionLabel: this.ctx.translate.instant('widgets.power-button.power-on')};
this.onValueSetter = this.createValueSetter(onUpdateStateSettings);
const offUpdateStateSettings = {...this.settings.offUpdateState,
actionLabel: this.ctx.translate.instant('widgets.power-button.power-off')};
this.offValueSetter = this.createValueSetter(offUpdateStateSettings);
this.loading$.subscribe((loading) => {
this.updateDisabledState(loading || this.disabled);
this.cd.markForCheck();
});
}
ngAfterViewInit(): void {
if (this.drawSvgShapePending) {
this.drawSvg();
}
super.ngAfterViewInit();
}
ngOnDestroy() {
if (this.shapeResize$) {
this.shapeResize$.disconnect();
}
super.ngOnDestroy();
}
public onInit() {
super.onInit();
const borderRadius = this.ctx.$widgetElement.css('borderRadius');
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}};
if (this.powerButtonShape) {
this.drawSvg();
} else {
this.drawSvgShapePending = true;
}
this.cd.detectChanges();
}
private onValue(value: boolean): void {
const newValue = !!value;
if (this.value !== newValue) {
this.value = newValue;
this.powerButtonSvgShape?.setValue(this.value);
this.cd.markForCheck();
}
}
private onDisabled(value: boolean): void {
const newDisabled = !!value;
if (this.disabled !== newDisabled) {
this.disabled = newDisabled;
this.updateDisabledState(this.disabled);
this.cd.markForCheck();
}
}
private onClick() {
if (!this.ctx.isEdit && !this.ctx.isPreview && !this.disabledState) {
this.onValue(!this.value);
const targetValue = this.value;
const targetSetter = targetValue ? this.onValueSetter : this.offValueSetter;
this.powerButtonSvgShape?.setPressed(true);
this.updateValue(targetSetter, targetValue, {
next: () => {
this.powerButtonSvgShape?.setPressed(false);
this.onValue(targetValue);
},
error: () => {
this.powerButtonSvgShape?.setPressed(false);
this.onValue(!targetValue);
}
});
}
}
private drawSvg() {
this.svgShape = SVG().addTo(this.powerButtonShape.nativeElement).size(powerButtonShapeSize, powerButtonShapeSize);
this.renderer.setStyle(this.svgShape.node, 'overflow', 'visible');
this.renderer.setStyle(this.svgShape.node, 'user-select', 'none');
this.powerButtonSvgShape = PowerButtonShape.fromSettings(this.ctx, this.svgShape,
this.settings, this.value, this.disabledState, () => this.onClick());
this.shapeResize$ = new ResizeObserver(() => {
this.onResize();
});
this.shapeResize$.observe(this.powerButtonShape.nativeElement);
this.onResize();
}
private updateDisabledState(disabled: boolean) {
this.disabledState = disabled;
this.powerButtonSvgShape?.setDisabled(this.disabledState);
}
private onResize() {
const shapeWidth = this.powerButtonShape.nativeElement.getBoundingClientRect().width;
const shapeHeight = this.powerButtonShape.nativeElement.getBoundingClientRect().height;
const size = Math.min(shapeWidth, shapeHeight);
const scale = size / powerButtonShapeSize;
this.renderer.setStyle(this.svgShape.node, 'transform', `scale(${scale})`);
}
}

953
ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts

@ -0,0 +1,953 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { BackgroundSettings, BackgroundType } from '@shared/models/widget-settings.models';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import {
DataToValueType,
GetValueAction,
GetValueSettings,
SetValueAction,
SetValueSettings,
ValueToDataType
} from '@shared/models/action-widget-settings.models';
import { Circle, Effect, Element, G, Gradient, Runner, Svg, Text, Timeline } from '@svgdotjs/svg.js';
import '@svgdotjs/svg.filter.js';
import tinycolor from 'tinycolor2';
import { WidgetContext } from '@home/models/widget-component.models';
export enum PowerButtonLayout {
default = 'default',
simplified = 'simplified',
outlined = 'outlined',
default_volume = 'default_volume',
simplified_volume = 'simplified_volume',
outlined_volume = 'outlined_volume'
}
export const powerButtonLayouts = Object.keys(PowerButtonLayout) as PowerButtonLayout[];
export const powerButtonLayoutTranslations = new Map<PowerButtonLayout, string>(
[
[PowerButtonLayout.default, 'widgets.power-button.layout-default'],
[PowerButtonLayout.simplified, 'widgets.power-button.layout-simplified'],
[PowerButtonLayout.outlined, 'widgets.power-button.layout-outlined'],
[PowerButtonLayout.default_volume, 'widgets.power-button.layout-default-volume'],
[PowerButtonLayout.simplified_volume, 'widgets.power-button.layout-simplified-volume'],
[PowerButtonLayout.outlined_volume, 'widgets.power-button.layout-outlined-volume']
]
);
export const powerButtonLayoutImages = new Map<PowerButtonLayout, string>(
[
[PowerButtonLayout.default, 'assets/widget/power-button/default-layout.svg'],
[PowerButtonLayout.simplified, 'assets/widget/power-button/simplified-layout.svg'],
[PowerButtonLayout.outlined, 'assets/widget/power-button/outlined-layout.svg'],
[PowerButtonLayout.default_volume, 'assets/widget/power-button/default-volume-layout.svg'],
[PowerButtonLayout.simplified_volume, 'assets/widget/power-button/simplified-volume-layout.svg'],
[PowerButtonLayout.outlined_volume, 'assets/widget/power-button/outlined-volume-layout.svg']
]
);
export interface PowerButtonWidgetSettings {
initialState: GetValueSettings<boolean>;
disabledState: GetValueSettings<boolean>;
onUpdateState: SetValueSettings;
offUpdateState: SetValueSettings;
layout: PowerButtonLayout;
mainColorOn: string;
backgroundColorOn: string;
mainColorOff: string;
backgroundColorOff: string;
mainColorDisabled: string;
backgroundColorDisabled: string;
background: BackgroundSettings;
}
export const powerButtonDefaultSettings: PowerButtonWidgetSettings = {
initialState: {
action: GetValueAction.EXECUTE_RPC,
defaultValue: false,
executeRpc: {
method: 'getState',
requestTimeout: 5000,
requestPersistent: false,
persistentPollingInterval: 1000
},
getAttribute: {
key: 'state',
scope: null
},
getTimeSeries: {
key: 'state'
},
dataToValue: {
type: DataToValueType.NONE,
compareToValue: true,
dataToValueFunction: '/* Should return boolean value */\nreturn data;'
}
},
disabledState: {
action: GetValueAction.DO_NOTHING,
defaultValue: false,
getAttribute: {
key: 'state',
scope: null
},
getTimeSeries: {
key: 'state'
},
dataToValue: {
type: DataToValueType.NONE,
compareToValue: true,
dataToValueFunction: '/* Should return boolean value */\nreturn data;'
}
},
onUpdateState: {
action: SetValueAction.EXECUTE_RPC,
executeRpc: {
method: 'setState',
requestTimeout: 5000,
requestPersistent: false,
persistentPollingInterval: 1000
},
setAttribute: {
key: 'state',
scope: AttributeScope.SHARED_SCOPE
},
putTimeSeries: {
key: 'state'
},
valueToData: {
type: ValueToDataType.CONSTANT,
constantValue: true,
valueToDataFunction: '/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;'
}
},
offUpdateState: {
action: SetValueAction.EXECUTE_RPC,
executeRpc: {
method: 'setState',
requestTimeout: 5000,
requestPersistent: false,
persistentPollingInterval: 1000
},
setAttribute: {
key: 'state',
scope: AttributeScope.SHARED_SCOPE
},
putTimeSeries: {
key: 'state'
},
valueToData: {
type: ValueToDataType.CONSTANT,
constantValue: false,
valueToDataFunction: '/* Convert input boolean value to RPC parameters or attribute/time-series value */ \n return value;'
}
},
layout: PowerButtonLayout.default,
mainColorOn: '#3F52DD',
backgroundColorOn: '#FFFFFF',
mainColorOff: '#A2A2A2',
backgroundColorOff: '#FFFFFF',
mainColorDisabled: 'rgba(0,0,0,0.12)',
backgroundColorDisabled: '#FFFFFF',
background: {
type: BackgroundType.color,
color: '#fff',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
}
};
interface PowerButtonColor {
hex: string;
opacity: number;
}
type PowerButtonState = 'on' | 'off' | 'disabled';
interface PowerButtonColorState {
mainColor: PowerButtonColor;
backgroundColor: PowerButtonColor;
}
type PowerButtonShapeColors = Record<PowerButtonState, PowerButtonColorState>;
const createPowerButtonShapeColors = (settings: PowerButtonWidgetSettings): PowerButtonShapeColors => {
const mainColorOn = tinycolor(settings.mainColorOn);
const backgroundColorOn = tinycolor(settings.backgroundColorOn);
const mainColorOff = tinycolor(settings.mainColorOff);
const backgroundColorOff = tinycolor(settings.backgroundColorOff);
const mainColorDisabled = tinycolor(settings.mainColorDisabled);
const backgroundColorDisabled = tinycolor(settings.backgroundColorDisabled);
return {
on: {
mainColor: {hex: mainColorOn.toHexString(), opacity: mainColorOn.getAlpha()},
backgroundColor: {hex: backgroundColorOn.toHexString(), opacity: backgroundColorOn.getAlpha()},
},
off: {
mainColor: {hex: mainColorOff.toHexString(), opacity: mainColorOff.getAlpha()},
backgroundColor: {hex: backgroundColorOff.toHexString(), opacity: backgroundColorOff.getAlpha()},
},
disabled: {
mainColor: {hex: mainColorDisabled.toHexString(), opacity: mainColorDisabled.getAlpha()},
backgroundColor: {hex: backgroundColorDisabled.toHexString(), opacity: backgroundColorDisabled.getAlpha()},
}
};
};
export const powerButtonShapeSize = 110;
const cx = powerButtonShapeSize / 2;
const cy = powerButtonShapeSize / 2;
const powerButtonAnimation = (element: Element): Runner => element.animate(200, 0, 'now');
export abstract class PowerButtonShape {
static fromSettings(ctx: WidgetContext,
svgShape: Svg,
settings: PowerButtonWidgetSettings,
value: boolean,
disabled: boolean,
onClick: () => void): PowerButtonShape {
switch (settings.layout) {
case PowerButtonLayout.default:
return new DefaultPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
case PowerButtonLayout.simplified:
return new SimplifiedPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
case PowerButtonLayout.outlined:
return new OutlinedPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
case PowerButtonLayout.default_volume:
return new DefaultVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
case PowerButtonLayout.simplified_volume:
return new SimplifiedVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
case PowerButtonLayout.outlined_volume:
return new OutlinedVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
}
}
protected readonly colors: PowerButtonShapeColors;
protected readonly onLabel: string;
protected readonly offLabel: string;
protected backgroundShape: Circle;
protected hoverShape: Circle;
protected hovered = false;
protected pressed = false;
protected forcePressed = false;
protected constructor(protected widgetContext: WidgetContext,
protected svgShape: Svg,
protected settings: PowerButtonWidgetSettings,
protected value: boolean,
protected disabled: boolean,
protected onClick: () => void) {
this.colors = createPowerButtonShapeColors(this.settings);
this.onLabel = this.widgetContext.translate.instant('widgets.power-button.on-label').toUpperCase();
this.offLabel = this.widgetContext.translate.instant('widgets.power-button.off-label').toUpperCase();
this._drawShape();
}
public setValue(value: boolean) {
if (this.value !== value) {
this.value = value;
this._drawState();
}
}
public setDisabled(disabled: boolean) {
if (this.disabled !== disabled) {
this.disabled = disabled;
this._drawState();
}
}
public setPressed(pressed: boolean) {
if (this.forcePressed !== pressed) {
this.forcePressed = pressed;
if (this.forcePressed && !this.pressed) {
this.onPressStart();
} else if (!this.forcePressed && !this.pressed) {
this.onPressEnd();
}
}
}
private _drawShape() {
this.backgroundShape = this.svgShape.circle(powerButtonShapeSize).center(cx, cy)
.fill({opacity: 0}).stroke({width: 0});
this.drawShape();
this.hoverShape = this.svgShape.circle(powerButtonShapeSize).center(cx, cy).addClass('tb-hover-circle')
.fill({color: '#000000', opacity: 0});
this.hoverShape.on('mouseover', () => {
this.hovered = true;
if (!this.disabled) {
this.hoverShape.timeline().finish();
this.hoverShape.animate(200).attr({'fill-opacity': 0.06});
}
});
this.hoverShape.on('mouseout', () => {
this.hovered = false;
this.hoverShape.timeline().finish();
this.hoverShape.animate(200).attr({'fill-opacity': 0});
this._cancelPressed();
});
this.hoverShape.on('touchmove', (event: TouchEvent) => {
const touch = event.touches[0];
const element = document.elementFromPoint(touch.pageX,touch.pageY);
if (this.hoverShape.node !== element) {
this._cancelPressed();
}
});
this.hoverShape.on('touchcancel', () => {
this._cancelPressed();
});
this.hoverShape.on('mousedown touchstart', (event: Event) => {
if (event.type === 'mousedown') {
if ((event as MouseEvent).button !== 0) {
return;
}
}
if (!this.disabled && !this.pressed) {
this.pressed = true;
if (!this.forcePressed) {
this.onPressStart();
}
}
});
this.hoverShape.on('mouseup touchend touchcancel', () => {
if (this.pressed && !this.disabled) {
this.onClick();
}
this._cancelPressed();
});
this._drawState();
}
private _cancelPressed() {
if (this.pressed) {
this.pressed = false;
if (!this.forcePressed) {
this.onPressEnd();
}
}
}
private _drawState() {
let colorState: PowerButtonColorState;
if (this.disabled) {
colorState = this.colors.disabled;
} else {
colorState = this.value ? this.colors.on : this.colors.off;
}
this.drawBackgroundState(colorState.backgroundColor);
this.drawColorState(colorState.mainColor);
if (this.value) {
this.drawOn();
} else {
this.drawOff();
}
if (this.disabled) {
this.hoverShape.timeline().finish();
this.hoverShape.attr({'fill-opacity': 0});
} else if (this.hovered) {
this.hoverShape.timeline().finish();
this.hoverShape.animate(200).attr({'fill-opacity': 0.06});
}
}
private drawBackgroundState(backgroundColor: PowerButtonColor) {
this.backgroundShape.attr({ fill: backgroundColor.hex, 'fill-opacity': backgroundColor.opacity});
}
protected drawShape() {}
protected drawColorState(_mainColor: PowerButtonColor) {}
protected drawOff() {}
protected drawOn() {}
protected onPressStart() {}
protected onPressEnd() {}
protected createMask(shape: Element, maskElements: Element[]) {
const mask =
this.svgShape.mask().add(this.svgShape.rect().width('100%').height('100%').fill('#fff'));
maskElements.forEach(e => {
mask.add(e.fill('#000').attr({'fill-opacity': 1}));
});
shape.maskWith(mask);
}
protected createOnLabel(fontWeight = '500'): Text {
return this.createLabel(this.onLabel, fontWeight);
}
protected createOffLabel(fontWeight = '500'): Text {
return this.createLabel(this.offLabel, fontWeight);
}
private createLabel(text: string, fontWeight = '500'): Text {
return this.svgShape.text(text).font({
family: 'Roboto',
weight: fontWeight,
style: 'normal',
size: '22px'
}).attr({x: '50%', y: '50%', 'text-anchor': 'middle', 'dominant-baseline': 'middle'});
}
}
class InnerShadowCircle {
private shadowCircle: Circle;
private blurEffect: Effect;
private offsetEffect: Effect;
private floodEffect: Effect;
constructor(private svgShape: Svg,
private diameter: number,
private centerX: number,
private centerY: number,
private blur = 6,
private shadowOpacity = 0.6,
private dx = 0,
private dy = 0,
private shadowColor = '#000') {
this.shadowCircle = this.svgShape.circle(this.diameter).center(this.centerX, this.centerY)
.fill({color: '#fff', opacity: 1}).stroke({width: 0});
this.shadowCircle.filterWith(add => {
add.x('-50%').y('-50%').width('200%').height('200%');
let effect: Effect = add.componentTransfer(components => {
components.funcA({ type: 'table', tableValues: '1 0' });
}).in(add.$fill);
effect = effect.gaussianBlur(this.blur, this.blur).attr({stdDeviation: this.blur});
this.blurEffect = effect;
effect = effect.offset(this.dx, this.dy);
this.offsetEffect = effect;
effect = effect.flood(this.shadowColor, this.shadowOpacity);
this.floodEffect = effect;
effect = effect.composite(this.offsetEffect, 'in');
effect.composite(add.$sourceAlpha, 'in');
add.merge(m => {
m.mergeNode(add.$fill);
m.mergeNode();
});
});
}
public timeline(tl: Timeline): void {
this.blurEffect.timeline(tl);
this.offsetEffect.timeline(tl);
this.floodEffect.timeline(tl);
}
public animate(blur: number, opacity: number, dx = 0, dy = 0): Runner {
powerButtonAnimation(this.blurEffect).attr({stdDeviation: blur});
powerButtonAnimation(this.offsetEffect).attr({dx, dy});
return powerButtonAnimation(this.floodEffect).attr({'flood-opacity': opacity});
}
public animateRestore(): Runner {
return this.animate(this.blur, this.shadowOpacity, this.dx, this.dy);
}
public show(): void {
this.shadowCircle.show();
}
public hide(): void {
this.shadowCircle.hide();
}
}
class DefaultPowerButtonShape extends PowerButtonShape {
private outerBorder: Circle;
private outerBorderMask: Circle;
private offLabelShape: Text;
private onCircleShape: Circle;
private onLabelShape: Text;
private pressedShadow: InnerShadowCircle;
private pressedTimeline: Timeline;
private centerGroup: G;
protected drawShape() {
this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy)
.fill({opacity: 0}).stroke({width: 0});
this.outerBorderMask = this.svgShape.circle(powerButtonShapeSize - 20).center(cx, cy);
this.createMask(this.outerBorder, [this.outerBorderMask]);
this.centerGroup = this.svgShape.group();
this.offLabelShape = this.createOffLabel().addTo(this.centerGroup);
this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 20)
.center(cx, cy);
this.onLabelShape = this.createOnLabel();
this.createMask(this.onCircleShape, [this.onLabelShape]);
this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 20, cx, cy, 0, 0);
this.pressedTimeline = new Timeline();
this.centerGroup.timeline(this.pressedTimeline);
this.onLabelShape.timeline(this.pressedTimeline);
this.pressedShadow.timeline(this.pressedTimeline);
}
protected drawColorState(mainColor: PowerButtonColor) {
this.outerBorder.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
}
protected drawOff() {
this.outerBorderMask.radius((powerButtonShapeSize - 20)/2);
this.onCircleShape.hide();
this.centerGroup.show();
}
protected drawOn() {
this.outerBorderMask.radius((powerButtonShapeSize - 2)/2);
this.centerGroup.hide();
this.onCircleShape.show();
}
protected onPressStart() {
this.pressedTimeline.finish();
const pressedScale = 0.75;
powerButtonAnimation(this.centerGroup).transform({scale: pressedScale});
powerButtonAnimation(this.onLabelShape).transform({scale: pressedScale});
this.pressedShadow.animate(6, 0.6);
}
protected onPressEnd() {
this.pressedTimeline.finish();
powerButtonAnimation(this.centerGroup).transform({scale: 1});
powerButtonAnimation(this.onLabelShape).transform({scale: 1});
this.pressedShadow.animateRestore();
}
}
class SimplifiedPowerButtonShape extends PowerButtonShape {
private outerBorder: Circle;
private outerBorderMask: Circle;
private onCircleShape: Circle;
private offLabelShape: Text;
private onLabelShape: Text;
private pressedShadow: InnerShadowCircle;
private pressedTimeline: Timeline;
private centerGroup: G;
protected drawShape() {
this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy)
.fill({opacity: 0}).stroke({width: 0});
this.outerBorderMask = this.svgShape.circle(powerButtonShapeSize - 4).center(cx, cy);
this.createMask(this.outerBorder, [this.outerBorderMask]);
this.centerGroup = this.svgShape.group();
this.offLabelShape = this.createOffLabel().addTo(this.centerGroup);
this.onCircleShape = this.svgShape.circle(powerButtonShapeSize).center(cx, cy);
this.onLabelShape = this.createOnLabel();
this.createMask(this.onCircleShape, [this.onLabelShape]);
this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 4, cx, cy, 0, 0);
this.pressedTimeline = new Timeline();
this.centerGroup.timeline(this.pressedTimeline);
this.onLabelShape.timeline(this.pressedTimeline);
this.pressedShadow.timeline(this.pressedTimeline);
}
protected drawColorState(mainColor: PowerButtonColor) {
this.outerBorder.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
}
protected drawOff() {
this.onCircleShape.hide();
this.outerBorder.show();
this.centerGroup.show();
}
protected drawOn() {
this.centerGroup.hide();
this.outerBorder.hide();
this.onCircleShape.show();
}
protected onPressStart() {
this.pressedTimeline.finish();
const pressedScale = 0.75;
powerButtonAnimation(this.centerGroup).transform({scale: pressedScale});
powerButtonAnimation(this.onLabelShape).transform({scale: pressedScale});
this.pressedShadow.animate(6, 0.6);
}
protected onPressEnd() {
this.pressedTimeline.finish();
powerButtonAnimation(this.centerGroup).transform({scale: 1});
powerButtonAnimation(this.onLabelShape).transform({scale: 1});
this.pressedShadow.animateRestore();
}
}
class OutlinedPowerButtonShape extends PowerButtonShape {
private outerBorder: Circle;
private outerBorderMask: Circle;
private innerBorder: Circle;
private innerBorderMask: Circle;
private offLabelShape: Text;
private onCircleShape: Circle;
private onLabelShape: Text;
private pressedShadow: InnerShadowCircle;
private pressedTimeline: Timeline;
private centerGroup: G;
private onCenterGroup: G;
protected drawShape() {
this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy)
.fill({opacity: 0}).stroke({width: 0});
this.outerBorderMask = this.svgShape.circle(powerButtonShapeSize - 2).center(cx, cy);
this.createMask(this.outerBorder, [this.outerBorderMask]);
this.innerBorder = this.svgShape.circle(powerButtonShapeSize - 20).center(cx, cy)
.fill({opacity: 0}).stroke({width: 0});
this.innerBorderMask = this.svgShape.circle(powerButtonShapeSize - 24).center(cx, cy);
this.createMask(this.innerBorder, [this.innerBorderMask]);
this.centerGroup = this.svgShape.group();
this.offLabelShape = this.createOffLabel().addTo(this.centerGroup);
this.onCenterGroup = this.svgShape.group();
this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 28).center(cx, cy)
.addTo(this.onCenterGroup);
this.onLabelShape = this.createOnLabel();
this.createMask(this.onCircleShape, [this.onLabelShape]);
this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 24, cx, cy, 0, 0);
this.pressedTimeline = new Timeline();
this.centerGroup.timeline(this.pressedTimeline);
this.onCenterGroup.timeline(this.pressedTimeline);
this.onLabelShape.timeline(this.pressedTimeline);
this.pressedShadow.timeline(this.pressedTimeline);
}
protected drawColorState(mainColor: PowerButtonColor) {
this.outerBorder.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.innerBorder.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
}
protected drawOff() {
this.onCenterGroup.hide();
this.centerGroup.show();
}
protected drawOn() {
this.centerGroup.hide();
this.onCenterGroup.show();
}
protected onPressStart() {
this.pressedTimeline.finish();
const pressedScale = 0.75;
powerButtonAnimation(this.centerGroup).transform({scale: pressedScale});
powerButtonAnimation(this.onCenterGroup).transform({scale: 0.98});
powerButtonAnimation(this.onLabelShape).transform({scale: pressedScale / 0.98});
this.pressedShadow.animate(6, 0.6);
}
protected onPressEnd() {
this.pressedTimeline.finish();
powerButtonAnimation(this.centerGroup).transform({scale: 1});
powerButtonAnimation(this.onCenterGroup).transform({scale: 1});
powerButtonAnimation(this.onLabelShape).transform({scale: 1});
this.pressedShadow.animateRestore();
}
}
class DefaultVolumePowerButtonShape extends PowerButtonShape {
private outerBorder: Circle;
private outerBorderMask: Circle;
private outerBorderGradient: Gradient;
private innerBorder: Circle;
private innerBorderMask: Circle;
private innerBorderGradient: Gradient;
private innerShadow: InnerShadowCircle;
//private innerShadowGradient: Gradient;
//private innerShadowGradientStop: Stop;
private offLabelShape: Text;
private onCircleShape: Circle;
private onLabelShape: Text;
private pressedTimeline: Timeline;
private centerGroup: G;
protected drawShape() {
this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy)
.fill({opacity: 0}).stroke({width: 0});
this.outerBorderMask = this.svgShape.circle(powerButtonShapeSize - 20).center(cx, cy);
this.createMask(this.outerBorder, [this.outerBorderMask]);
this.outerBorderGradient = this.svgShape.gradient('linear', (add) => {
add.stop(0, '#CCCCCC', 1);
add.stop(1, '#FFFFFF', 1);
}).from(0.268, 0.92).to(0.832, 0.1188);
this.innerBorder = this.svgShape.circle(powerButtonShapeSize - 20).center(cx, cy)
.fill({opacity: 0}).stroke({width: 0});
this.innerBorderMask = this.svgShape.circle(powerButtonShapeSize - 24).center(cx, cy);
this.createMask(this.innerBorder, [this.innerBorderMask]);
this.innerBorderGradient = this.svgShape.gradient('linear', (add) => {
add.stop(0, '#CCCCCC', 1);
add.stop(1, '#FFFFFF', 1);
}).from(0.832, 0.1188).to(0.268, 0.92);
this.centerGroup = this.svgShape.group();
this.offLabelShape = this.createOffLabel('400').addTo(this.centerGroup);
this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 24).center(cx, cy);
this.onLabelShape = this.createOnLabel('400');
this.createMask(this.onCircleShape, [this.onLabelShape]);
this.innerShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 24, cx, cy, 3, 0.3);
this.pressedTimeline = new Timeline();
this.centerGroup.timeline(this.pressedTimeline);
this.onLabelShape.timeline(this.pressedTimeline);
this.innerShadow.timeline(this.pressedTimeline);
}
protected drawColorState(mainColor: PowerButtonColor){
if (this.disabled) {
this.backgroundShape.removeClass('tb-small-shadow');
if (!this.forcePressed) {
this.innerShadow.hide();
}
this.outerBorder.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.innerBorder.attr({fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
} else {
this.backgroundShape.addClass('tb-small-shadow');
this.innerShadow.show();
this.outerBorder.fill(this.outerBorderGradient);
this.outerBorder.attr({ 'fill-opacity': 1 });
this.innerBorder.fill(this.innerBorderGradient);
this.innerBorder.attr({ 'fill-opacity': 1 });
}
this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
}
protected drawOff() {
this.onCircleShape.hide();
this.centerGroup.show();
this.innerBorder.show();
}
protected drawOn() {
if (this.disabled) {
this.innerBorder.hide();
} else {
this.innerBorder.show();
}
this.centerGroup.hide();
this.onCircleShape.show();
}
protected onPressStart() {
this.pressedTimeline.finish();
this.innerShadow.show();
const pressedScale = 0.75;
powerButtonAnimation(this.centerGroup).transform({scale: pressedScale});
powerButtonAnimation(this.onLabelShape).transform({scale: pressedScale});
this.innerShadow.animate(6, 0.6);
}
protected onPressEnd() {
this.pressedTimeline.finish();
powerButtonAnimation(this.centerGroup).transform({scale: 1});
powerButtonAnimation(this.onLabelShape).transform({scale: 1});
this.innerShadow.animateRestore().after(() => {
if (this.disabled) {
this.innerShadow.hide();
}
});
}
}
class SimplifiedVolumePowerButtonShape extends PowerButtonShape {
private outerBorder: Circle;
private outerBorderMask: Circle;
private offLabelShape: Text;
private onLabelShape: Text;
private innerShadow: InnerShadowCircle;
private pressedShadow: InnerShadowCircle;
private pressedTimeline: Timeline;
private centerGroup: G;
private onCenterGroup: G;
protected drawShape() {
this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy)
.fill({color: '#FAFAFA', opacity: 1}).stroke({width: 0});
this.outerBorderMask = this.svgShape.circle(powerButtonShapeSize - 4).center(cx, cy);
this.createMask(this.outerBorder, [this.outerBorderMask]);
this.centerGroup = this.svgShape.group();
this.offLabelShape = this.createOffLabel().addTo(this.centerGroup);
this.onCenterGroup = this.svgShape.group();
this.onLabelShape = this.createOnLabel().addTo(this.onCenterGroup);
this.innerShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 4, cx, cy, 3, 0.3);
this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 4, cx, cy, 0, 0);
this.pressedTimeline = new Timeline();
this.centerGroup.timeline(this.pressedTimeline);
this.onCenterGroup.timeline(this.pressedTimeline);
this.pressedShadow.timeline(this.pressedTimeline);
}
protected drawColorState(mainColor: PowerButtonColor){
this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.onLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
}
protected drawOff() {
if (!this.pressed) {
this.backgroundShape.addClass('tb-shadow');
}
this.innerShadow.hide();
this.onCenterGroup.hide();
this.centerGroup.show();
}
protected drawOn() {
this.backgroundShape.removeClass('tb-shadow');
this.centerGroup.hide();
this.onCenterGroup.show();
this.innerShadow.show();
}
protected onPressStart() {
this.pressedTimeline.finish();
const pressedScale = 0.75;
if (!this.value) {
this.backgroundShape.removeClass('tb-shadow');
}
powerButtonAnimation(this.centerGroup).transform({scale: pressedScale});
powerButtonAnimation(this.onCenterGroup).transform({scale: pressedScale});
this.pressedShadow.animate(8, 0.4);
}
protected onPressEnd() {
this.pressedTimeline.finish();
powerButtonAnimation(this.centerGroup).transform({scale: 1});
powerButtonAnimation(this.onCenterGroup).transform({scale: 1});
this.pressedShadow.animateRestore().after(() => {
if (!this.value) {
this.backgroundShape.addClass('tb-shadow');
}
});
}
}
class OutlinedVolumePowerButtonShape extends PowerButtonShape {
private outerBorder: Circle;
private outerBorderMask: Circle;
private outerBorderGradient: Gradient;
private innerBorder: Circle;
private innerBorderMask: Circle;
private offLabelShape: Text;
private onCircleShape: Circle;
private onLabelShape: Text;
private pressedShadow: InnerShadowCircle;
private pressedTimeline: Timeline;
private centerGroup: G;
private onCenterGroup: G;
protected drawShape() {
this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy)
.fill({opacity: 0}).stroke({width: 0});
this.outerBorderMask = this.svgShape.circle(powerButtonShapeSize - 20).center(cx, cy);
this.createMask(this.outerBorder, [this.outerBorderMask]);
this.outerBorderGradient = this.svgShape.gradient('linear', (add) => {
add.stop(0, '#CCCCCC', 1);
add.stop(1, '#FFFFFF', 1);
}).from(0.268, 0.92).to(0.832, 0.1188);
this.innerBorder = this.svgShape.circle(powerButtonShapeSize - 20).center(cx, cy)
.fill({opacity: 0}).stroke({width: 0});
this.innerBorderMask = this.svgShape.circle(powerButtonShapeSize - 30).center(cx, cy);
this.createMask(this.innerBorder, [this.innerBorderMask]);
this.centerGroup = this.svgShape.group();
this.offLabelShape = this.createOffLabel('800').addTo(this.centerGroup);
this.onCenterGroup = this.svgShape.group();
this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 30).center(cx, cy)
.addTo(this.onCenterGroup);
this.onLabelShape = this.createOnLabel('800');
this.createMask(this.onCircleShape, [this.onLabelShape]);
this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 30, cx, cy, 0, 0);
this.backgroundShape.addClass('tb-small-shadow');
this.pressedTimeline = new Timeline();
this.centerGroup.timeline(this.pressedTimeline);
this.onCenterGroup.timeline(this.pressedTimeline);
this.onLabelShape.timeline(this.pressedTimeline);
this.pressedShadow.timeline(this.pressedTimeline);
}
protected drawColorState(mainColor: PowerButtonColor){
if (this.disabled) {
this.outerBorder.attr({ fill: '#000000', 'fill-opacity': 0.03});
} else {
this.outerBorder.fill(this.outerBorderGradient);
this.outerBorder.attr({ 'fill-opacity': 1 });
}
this.innerBorder.attr({fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
}
protected drawOff() {
this.onCenterGroup.hide();
this.centerGroup.show();
this.innerBorder.show();
}
protected drawOn() {
this.innerBorder.hide();
this.centerGroup.hide();
this.onCenterGroup.show();
}
protected onPressStart() {
this.pressedTimeline.finish();
const pressedScale = 0.75;
powerButtonAnimation(this.centerGroup).transform({scale: pressedScale});
powerButtonAnimation(this.onCenterGroup).transform({scale: 0.98});
powerButtonAnimation(this.onLabelShape).transform({scale: pressedScale / 0.98});
this.pressedShadow.animate(6, 0.6);
}
protected onPressEnd() {
this.pressedTimeline.finish();
powerButtonAnimation(this.centerGroup).transform({scale: 1});
powerButtonAnimation(this.onCenterGroup).transform({scale: 1});
powerButtonAnimation(this.onLabelShape).transform({scale: 1});
this.pressedShadow.animateRestore();
}
}

6
ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html

@ -37,10 +37,4 @@
</div> </div>
</div> </div>
<mat-progress-bar class="tb-action-widget-progress" style="height: 4px;" color="accent" mode="indeterminate" *ngIf="loading$ | async"></mat-progress-bar> <mat-progress-bar class="tb-action-widget-progress" style="height: 4px;" color="accent" mode="indeterminate" *ngIf="loading$ | async"></mat-progress-bar>
<div *ngIf="error" class="tb-action-widget-error-container">
<div class="tb-action-widget-error-panel">
<div class="tb-action-widget-error-text" [innerHTML]="error | safe: 'html'"></div>
<button class="tb-action-widget-error-clear tb-mat-20" mat-icon-button (click)="clearError()"><mat-icon>close</mat-icon></button>
</div>
</div>
</div> </div>

59
ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.html

@ -0,0 +1,59 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-slider-panel" [style.pointer-events]="ctx.isEdit ? 'none' : 'all'" [style]="backgroundStyle$ | async">
<div class="tb-slider-overlay" [style]="overlayStyle"></div>
<div *ngIf="showWidgetTitlePanel" class="tb-slider-title-panel">
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
</div>
<div #sliderContent class="tb-slider-content">
<div #sliderValueContainer class="tb-slider-value-container" *ngIf="showValue" >
<div #sliderValue class="tb-slider-value" [style]="valueStyle">{{ valueText }}</div>
</div>
<div class="tb-slider-container" [class.tb-min-height]="!showValue">
<div #leftSliderIconContainer *ngIf="showLeftRightIcon" >
<tb-icon #leftSliderIcon [style]="leftIconStyle"
[style.color]="(disabled || (loading$ | async)) ? settings.mainColorDisabled : settings.leftIconColor">{{ leftIcon }}</tb-icon>
</div>
<div class="tb-slider-column">
<mat-slider class="tb-slider"
[disabled]="disabled || (loading$ | async)"
[displayWith]="sliderValueText"
[showTickMarks]="settings.showTickMarks"
[step]="sliderStep"
[min]="settings.tickMin"
[max]="settings.tickMax"
discrete>
<input matSliderThumb [(ngModel)]="value" (valueChange)="onSliderChange()">
</mat-slider>
<div *ngIf="showTicks" class="tb-slider-ticks" [style]="ticksStyle">
<div #sliderTickMinContainer>
<div #sliderTickMin>{{ settings.tickMin }}</div>
</div>
<div #sliderTickMaxContainer>
<div #sliderTickMax>{{ settings.tickMax }}</div>
</div>
</div>
</div>
<div #rightSliderIconContainer *ngIf="showLeftRightIcon" >
<tb-icon #rightSliderIcon [style]="rightIconStyle"
[style.color]="(disabled || (loading$ | async)) ? settings.mainColorDisabled : settings.rightIconColor">{{ rightIcon }}</tb-icon>
</div>
</div>
</div>
<mat-progress-bar class="tb-action-widget-progress" style="height: 4px;" color="accent" mode="indeterminate" *ngIf="loading$ | async"></mat-progress-bar>
</div>

135
ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.scss

@ -0,0 +1,135 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
$mainColor: var(--tb-slider-main-color, #5469FF);
$hoverRippleColor: var(--tb-slider-hover-ripple-color, rgba(84, 105, 255, 0.05));
$focusRippleColor: var(--tb-slider-focus-ripple-color, rgba(84, 105, 255, 0.2));
$backgroundColor: var(--tb-slider-background-color, #CCD2FF);
$tickMarksColor: var(--tb-slider-tick-marks-color, #5469FF);
$mainColorDisabled: var(--tb-slider-main-color-disabled, #9BA2B0);
$backgroundColorDisabled: var(--tb-slider-background-color-disabled, #D5D7E5);
.tb-slider-panel {
.mat-mdc-slider.mat-primary.tb-slider {
--mdc-slider-active-track-color: #{$mainColor};
--mdc-slider-handle-color: #{$mainColor};
--mdc-slider-focus-handle-color: #{$mainColor};
--mdc-slider-hover-handle-color: #{$mainColor};
--mdc-slider-with-tick-marks-inactive-container-color: #{$tickMarksColor};
--mat-mdc-slider-ripple-color: #{$mainColor};
--mat-mdc-slider-hover-ripple-color: #{$hoverRippleColor};
--mat-mdc-slider-focus-ripple-color: #{$focusRippleColor};
--mdc-slider-inactive-track-color: #{$backgroundColor};
--mdc-slider-disabled-active-track-color: #{$mainColorDisabled};
--mdc-slider-disabled-handle-color: #{$mainColorDisabled};
--mdc-slider-disabled-inactive-track-color: #{$backgroundColorDisabled};
--mdc-slider-with-tick-marks-disabled-container-color: #{$mainColorDisabled};
--mdc-slider-handle-width: 16px;
--mdc-slider-handle-height: 16px;
}
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
padding: 20px 24px 24px 24px;
gap: 8px;
> div:not(.tb-slider-overlay), > tb-icon {
z-index: 1;
}
.tb-slider-overlay {
position: absolute;
inset: 12px;
}
div.tb-slider-title-panel {
z-index: 2;
}
.tb-slider-content {
flex: 1;
min-height: 0;
display: flex;
position: relative;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
.tb-slider-value-container {
min-height: 0;
}
.tb-slider-value {
white-space: nowrap;
}
.tb-slider-container {
align-self: stretch;
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 8px;
&.tb-min-height {
height: 6px;
}
.tb-slider-column {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
.mat-mdc-slider.tb-slider {
margin: 0;
height: 6px;
min-height: 6px;
min-width: 0;
&.mdc-slider--disabled {
opacity: 1;
}
.mdc-slider__track--inactive {
opacity: 1;
}
.mdc-slider__tick-marks {
.mdc-slider__tick-mark--active {
display: none;
}
.mdc-slider__tick-mark--inactive {
opacity: 1;
}
}
.mdc-slider__thumb.mat-mdc-slider-visual-thumb {
top: -21px;
.mat-ripple {
overflow: visible;
}
}
.mdc-slider__value-indicator-text {
white-space: nowrap;
}
}
.tb-slider-ticks {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
}
}
}
}
}

347
ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.ts

@ -0,0 +1,347 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
OnDestroy,
OnInit,
Renderer2,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { BasicActionWidgetComponent, ValueSetter } from '@home/components/widget/lib/action/action-widget.models';
import {
backgroundStyle,
ComponentStyle,
iconStyle,
overlayStyle,
textStyle
} from '@shared/models/widget-settings.models';
import { Observable } from 'rxjs';
import { ResizeObserver } from '@juggle/resize-observer';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import { ValueType } from '@shared/models/constants';
import { UtilsService } from '@core/services/utils.service';
import {
SliderLayout,
sliderWidgetDefaultSettings,
SliderWidgetSettings
} from '@home/components/widget/lib/rpc/slider-widget.models';
import { formatValue, isDefinedAndNotNull, isNumeric } from '@core/utils';
import { WidgetComponent } from '@home/components/widget/widget.component';
import tinycolor from 'tinycolor2';
@Component({
selector: 'tb-slider-widget',
templateUrl: './slider-widget.component.html',
styleUrls: ['../action/action-widget.scss', './slider-widget.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class SliderWidgetComponent extends
BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('sliderContent', {static: false})
sliderContent: ElementRef<HTMLElement>;
@ViewChild('sliderValueContainer', {static: false})
sliderValueContainer: ElementRef<HTMLElement>;
@ViewChild('sliderValue', {static: false})
sliderValue: ElementRef<HTMLElement>;
@ViewChild('sliderTickMinContainer', {static: false})
sliderTickMinContainer: ElementRef<HTMLElement>;
@ViewChild('sliderTickMin', {static: false})
sliderTickMin: ElementRef<HTMLElement>;
@ViewChild('sliderTickMaxContainer', {static: false})
sliderTickMaxContainer: ElementRef<HTMLElement>;
@ViewChild('sliderTickMax', {static: false})
sliderTickMax: ElementRef<HTMLElement>;
@ViewChild('leftSliderIconContainer', {static: false, read: ElementRef})
leftSliderIconContainer: ElementRef<HTMLElement>;
@ViewChild('leftSliderIcon', {static: false, read: ElementRef})
leftSliderIcon: ElementRef<HTMLElement>;
@ViewChild('rightSliderIconContainer', {static: false, read: ElementRef})
rightSliderIconContainer: ElementRef<HTMLElement>;
@ViewChild('rightSliderIcon', {static: false, read: ElementRef})
rightSliderIcon: ElementRef<HTMLElement>;
settings: SliderWidgetSettings;
backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {};
value: number = null;
private prevValue: number = null;
disabled = false;
layout: SliderLayout;
showValue = true;
valueText = 'N/A';
valueStyle: ComponentStyle = {};
showLeftRightIcon = false;
leftIcon = '';
leftIconStyle: ComponentStyle = {};
rightIcon = '';
rightIconStyle: ComponentStyle = {};
showTicks = true;
ticksStyle: ComponentStyle = {};
sliderStep: number = undefined;
autoScale = false;
showWidgetTitlePanel = this.widgetComponent.dashboardWidget.showWidgetTitlePanel;
sliderValueText = this._sliderValueText.bind(this);
private panelResize$: ResizeObserver;
private valueSetter: ValueSetter<number>;
private sliderCssClass: string;
constructor(protected imagePipe: ImagePipe,
protected sanitizer: DomSanitizer,
private renderer: Renderer2,
private utils: UtilsService,
private widgetComponent: WidgetComponent,
protected cd: ChangeDetectorRef,
private elementRef: ElementRef) {
super(cd);
}
ngOnInit(): void {
super.ngOnInit();
this.settings = {...sliderWidgetDefaultSettings, ...this.ctx.settings};
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer);
this.overlayStyle = overlayStyle(this.settings.background.overlay);
this.layout = this.settings.layout;
this.autoScale = this.settings.autoScale;
this.showValue = this.layout !== SliderLayout.simplified && this.settings.showValue;
this.valueStyle = textStyle(this.settings.valueFont);
this.valueStyle.color = this.settings.valueColor;
this.showLeftRightIcon = this.layout === SliderLayout.extended;
if (this.showLeftRightIcon) {
this.leftIcon = this.settings.leftIcon;
this.leftIconStyle = iconStyle(this.settings.leftIconSize, this.settings.leftIconSizeUnit );
this.rightIcon = this.settings.rightIcon;
this.rightIconStyle = iconStyle(this.settings.rightIconSize, this.settings.rightIconSizeUnit );
if (!this.autoScale) {
const leftIconMargin = this.settings.leftIconSize / 2 + (this.settings.leftIconSizeUnit || 'px');
this.leftIconStyle.marginTop = `calc(-${leftIconMargin} + 3px)`;
const rightIconMargin = this.settings.rightIconSize / 2 + (this.settings.rightIconSizeUnit || 'px');
this.rightIconStyle.marginTop = `calc(-${rightIconMargin} + 3px)`;
}
}
this.showTicks = this.settings.showTicks;
if (this.showTicks) {
this.ticksStyle = textStyle(this.settings.ticksFont);
this.ticksStyle.color = this.settings.ticksColor;
}
if (this.settings.showTickMarks) {
const range = this.settings.tickMax - this.settings.tickMin;
this.sliderStep = range / (this.settings.tickMarksCount - 1);
}
const mainColorInstance = tinycolor(this.settings.mainColor);
const hoverRippleColor = mainColorInstance.clone().setAlpha(mainColorInstance.getAlpha() * 0.05).toRgbString();
const focusRippleColor = mainColorInstance.clone().setAlpha(mainColorInstance.getAlpha() * 0.2).toRgbString();
const sliderVariablesCss = `.tb-slider-panel {\n`+
`--tb-slider-main-color: ${this.settings.mainColor};\n`+
`--tb-slider-background-color: ${this.settings.backgroundColor};\n`+
`--tb-slider-hover-ripple-color: ${hoverRippleColor};\n`+
`--tb-slider-focus-ripple-color: ${focusRippleColor};\n`+
`--tb-slider-tick-marks-color: ${this.settings.tickMarksColor};\n`+
`--tb-slider-main-color-disabled: ${this.settings.mainColorDisabled};\n`+
`--tb-slider-background-disabled: ${this.settings.backgroundColorDisabled};\n`+
`}`;
this.sliderCssClass =
this.utils.applyCssToElement(this.renderer, this.elementRef.nativeElement, 'tb-slider', sliderVariablesCss);
const getInitialStateSettings =
{...this.settings.initialState, actionLabel: this.ctx.translate.instant('widgets.slider.initial-value')};
this.createValueGetter(getInitialStateSettings, ValueType.INTEGER, {
next: (value) => this.onValue(value)
});
const disabledStateSettings =
{...this.settings.disabledState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.disabled-state')};
this.createValueGetter(disabledStateSettings, ValueType.BOOLEAN, {
next: (value) => this.onDisabled(value)
});
const valueChangeSettings = {...this.settings.valueChange,
actionLabel: this.ctx.translate.instant('widgets.slider.on-value-change')};
this.valueSetter = this.createValueSetter(valueChangeSettings);
}
ngAfterViewInit(): void {
if (this.autoScale) {
this.panelResize$ = new ResizeObserver(() => {
this.onResize();
});
this.panelResize$.observe(this.sliderContent.nativeElement);
if (this.showValue) {
this.panelResize$.observe(this.sliderValueContainer.nativeElement);
}
this.onResize();
}
super.ngAfterViewInit();
}
ngOnDestroy() {
if (this.panelResize$) {
this.panelResize$.disconnect();
}
if (this.sliderCssClass) {
this.utils.clearCssElement(this.renderer, this.sliderCssClass);
}
super.ngOnDestroy();
}
public onInit() {
super.onInit();
const borderRadius = this.ctx.$widgetElement.css('borderRadius');
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}};
this.cd.detectChanges();
}
public onSliderChange() {
this.updateValueText();
if (!this.ctx.isEdit && !this.ctx.isPreview) {
const prevValue = this.prevValue;
const targetValue = this.value;
this.updateValue(this.valueSetter, targetValue, {
next: () => this.onValue(targetValue),
error: () => this.onValue(prevValue)
});
}
}
private _sliderValueText(value: number): string {
return formatValue(value, this.settings.valueDecimals, this.settings.valueUnits, false);
}
private onValue(value: number): void {
this.value = value;
this.prevValue = value;
this.updateValueText();
this.cd.markForCheck();
}
private updateValueText() {
if (isDefinedAndNotNull(this.value) && isNumeric(this.value)) {
this.valueText = formatValue(this.value, this.settings.valueDecimals, this.settings.valueUnits, false);
} else {
this.valueText = 'N/A';
}
}
private onDisabled(value: boolean): void {
this.disabled = !!value;
this.cd.markForCheck();
}
private onResize() {
const panelWidth = this.sliderContent.nativeElement.getBoundingClientRect().width;
const panelHeight = this.sliderContent.nativeElement.getBoundingClientRect().height;
if (this.showValue) {
this.resetScale(this.sliderValueContainer.nativeElement, this.sliderValue.nativeElement);
}
if (this.showLeftRightIcon) {
this.resetScale(this.leftSliderIconContainer.nativeElement, this.leftSliderIcon.nativeElement);
this.resetScale(this.rightSliderIconContainer.nativeElement, this.rightSliderIcon.nativeElement);
}
if (this.showTicks) {
this.resetScale(this.sliderTickMinContainer.nativeElement, this.sliderTickMin.nativeElement);
this.resetScale(this.sliderTickMaxContainer.nativeElement, this.sliderTickMax.nativeElement);
}
let minAspect = 0.2;
let avgContentHeight = 35;
if (this.showTicks) {
minAspect += 0.1;
avgContentHeight += 20;
}
if (this.showValue) {
minAspect += 0.1;
avgContentHeight += 50;
}
const aspect = Math.min(panelHeight / panelWidth, minAspect);
const targetHeight = panelWidth * aspect;
const scale = targetHeight / avgContentHeight;
if (this.showValue) {
this.updateScale(this.sliderValueContainer.nativeElement, this.sliderValue.nativeElement, scale);
}
if (this.showLeftRightIcon) {
const leftIconContainerRect = this.leftSliderIconContainer.nativeElement.getBoundingClientRect();
const leftIconContainerMarginTop = -(leftIconContainerRect.width * scale) / 2 + 3;
this.renderer.setStyle(this.leftSliderIconContainer.nativeElement, 'marginTop', `${leftIconContainerMarginTop}px`);
this.updateScale(this.leftSliderIconContainer.nativeElement, this.leftSliderIcon.nativeElement, scale, true);
const rightIconContainerRect = this.rightSliderIconContainer.nativeElement.getBoundingClientRect();
const rightIconContainerMarginTop = -(rightIconContainerRect.width * scale) / 2 + 3;
this.renderer.setStyle(this.rightSliderIconContainer.nativeElement, 'marginTop', `${rightIconContainerMarginTop}px`);
this.updateScale(this.rightSliderIconContainer.nativeElement, this.rightSliderIcon.nativeElement, scale, true);
}
if (this.showTicks) {
this.updateScale(this.sliderTickMinContainer.nativeElement, this.sliderTickMin.nativeElement, scale);
this.updateScale(this.sliderTickMaxContainer.nativeElement, this.sliderTickMax.nativeElement, scale);
}
}
private resetScale(container: HTMLElement, element: HTMLElement): void {
this.renderer.setStyle(container, 'width', '');
this.renderer.setStyle(container, 'height', '');
this.renderer.setStyle(element, 'transform', '');
}
private updateScale(container: HTMLElement, element: HTMLElement, scale: number, sameHeight = false): void {
const rect = container.getBoundingClientRect();
this.renderer.setStyle(container, 'width', `${rect.width * scale}px`);
this.renderer.setStyle(container, 'height', `${(sameHeight ? rect.width : rect.height) * scale}px`);
this.renderer.setStyle(element, 'transform', `scale(${scale})`);
this.renderer.setStyle(element, 'transform-origin', 'left top');
}
}

196
ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.models.ts

@ -0,0 +1,196 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
DataToValueType,
GetValueAction,
GetValueSettings,
SetValueAction,
SetValueSettings,
ValueToDataType
} from '@shared/models/action-widget-settings.models';
import { BackgroundSettings, BackgroundType, cssUnit, Font } from '@shared/models/widget-settings.models';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
export enum SliderLayout {
default = 'default',
extended = 'extended',
simplified = 'simplified'
}
export const sliderLayouts = Object.keys(SliderLayout) as SliderLayout[];
export const sliderLayoutTranslations = new Map<SliderLayout, string>(
[
[SliderLayout.default, 'widgets.slider.layout-default'],
[SliderLayout.extended, 'widgets.slider.layout-extended'],
[SliderLayout.simplified, 'widgets.slider.layout-simplified']
]
);
export const sliderLayoutImages = new Map<SliderLayout, string>(
[
[SliderLayout.default, 'assets/widget/slider/default-layout.svg'],
[SliderLayout.extended, 'assets/widget/slider/extended-layout.svg'],
[SliderLayout.simplified, 'assets/widget/slider/simplified-layout.svg']
]
);
export interface SliderWidgetSettings {
initialState: GetValueSettings<number>;
disabledState: GetValueSettings<boolean>;
valueChange: SetValueSettings;
layout: SliderLayout;
autoScale: boolean;
showValue: boolean;
valueUnits: string;
valueDecimals: number;
valueFont: Font;
valueColor: string;
showTicks: boolean;
tickMin: number;
tickMax: number;
ticksFont: Font;
ticksColor: string;
showTickMarks: boolean;
tickMarksCount: number;
tickMarksColor: string;
mainColor: string;
backgroundColor: string;
mainColorDisabled: string;
backgroundColorDisabled: string;
leftIcon: string;
leftIconSize: number;
leftIconSizeUnit: cssUnit;
leftIconColor: string;
rightIcon: string;
rightIconSize: number;
rightIconSizeUnit: cssUnit;
rightIconColor: string;
background: BackgroundSettings;
}
export const sliderWidgetDefaultSettings: SliderWidgetSettings = {
initialState: {
action: GetValueAction.EXECUTE_RPC,
defaultValue: 0,
executeRpc: {
method: 'getState',
requestTimeout: 5000,
requestPersistent: false,
persistentPollingInterval: 1000
},
getAttribute: {
key: 'state',
scope: null
},
getTimeSeries: {
key: 'state'
},
dataToValue: {
type: DataToValueType.NONE,
compareToValue: true,
dataToValueFunction: '/* Should return integer value */\nreturn data;'
}
},
disabledState: {
action: GetValueAction.DO_NOTHING,
defaultValue: false,
getAttribute: {
key: 'state',
scope: null
},
getTimeSeries: {
key: 'state'
},
dataToValue: {
type: DataToValueType.NONE,
compareToValue: true,
dataToValueFunction: '/* Should return boolean value */\nreturn data;'
}
},
valueChange: {
action: SetValueAction.EXECUTE_RPC,
executeRpc: {
method: 'setState',
requestTimeout: 5000,
requestPersistent: false,
persistentPollingInterval: 1000
},
setAttribute: {
key: 'state',
scope: AttributeScope.SHARED_SCOPE
},
putTimeSeries: {
key: 'state'
},
valueToData: {
type: ValueToDataType.VALUE,
constantValue: 0,
valueToDataFunction: '/* Convert input integer value to RPC parameters or attribute/time-series value */\nreturn value;'
}
},
layout: SliderLayout.default,
autoScale: true,
showValue: true,
valueUnits: '%',
valueDecimals: 0,
valueFont: {
family: 'Roboto',
size: 36,
sizeUnit: 'px',
style: 'normal',
weight: '500',
lineHeight: '36px'
},
valueColor: 'rgba(0, 0, 0, 0.87)',
showTicks: true,
tickMin: 0,
tickMax: 100,
ticksFont: {
family: 'Roboto',
size: 11,
sizeUnit: 'px',
style: 'normal',
weight: '400',
lineHeight: '16px'
},
ticksColor: 'rgba(0,0,0,0.54)',
showTickMarks: true,
tickMarksCount: 11,
tickMarksColor: '#5469FF',
mainColor: '#5469FF',
backgroundColor: '#CCD2FF',
mainColorDisabled: '#9BA2B0',
backgroundColorDisabled: '#D5D7E5',
leftIcon: 'lightbulb',
leftIconSize: 24,
leftIconSizeUnit: 'px',
leftIconColor: '#5469FF',
rightIcon: 'mdi:lightbulb-on',
rightIconSize: 24,
rightIconSizeUnit: 'px',
rightIconColor: '#5469FF',
background: {
type: BackgroundType.color,
color: '#fff',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
}
};

142
ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.html

@ -0,0 +1,142 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container [formGroup]="powerButtonWidgetSettingsForm">
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.power-button.behavior</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.initial-state-hint' | translate}}" translate>widgets.rpc-state.initial-state</div>
<tb-get-value-action-settings fxFlex
panelTitle="widgets.rpc-state.initial-state"
[valueType]="valueType.BOOLEAN"
trueLabel="widgets.rpc-state.on"
falseLabel="widgets.rpc-state.off"
stateLabel="widgets.rpc-state.on"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="initialState"></tb-get-value-action-settings>
</div>
<div class="tb-form-row space-between">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.power-button.power-on-hint' | translate}}" translate>widgets.power-button.power-on</div>
<tb-set-value-action-settings fxFlex
panelTitle="widgets.power-button.power-on "
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="onUpdateState"></tb-set-value-action-settings>
</div>
<div class="tb-form-row space-between">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.power-button.power-off-hint' | translate}}" translate>widgets.power-button.power-off</div>
<tb-set-value-action-settings fxFlex
panelTitle="widgets.power-button.power-off"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="offUpdateState"></tb-set-value-action-settings>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.disabled-state-hint' | translate}}" translate>widgets.rpc-state.disabled-state</div>
<tb-get-value-action-settings fxFlex
panelTitle="widgets.rpc-state.disabled-state"
[valueType]="valueType.BOOLEAN"
stateLabel="widgets.rpc-state.disabled"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="disabledState"></tb-get-value-action-settings>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-style</div>
<tb-image-cards-select rowHeight="1:1"
[cols]="{columns: 3,
breakpoints: {
'lt-sm': 1,
'lt-md': 2
}}"
label="{{ 'widgets.power-button.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of powerButtonLayouts"
[value]="layout"
[image]="powerButtonLayoutImageMap.get(layout)">
{{ powerButtonLayoutTranslationMap.get(layout) | translate }}
</tb-image-cards-select-option>
</tb-image-cards-select>
<div class="tb-form-row space-between">
<div>{{ 'widgets.background.background' | translate }}</div>
<tb-background-settings formControlName="background">
</tb-background-settings>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.power-button.button</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.power-button.power-on-colors' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="12px">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.power-button.main</div>
<tb-color-input asBoxInput
formControlName="mainColorOn">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.power-button.background</div>
<tb-color-input asBoxInput
formControlName="backgroundColorOn">
</tb-color-input>
</div>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.power-button.power-off-colors' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="12px">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.power-button.main</div>
<tb-color-input asBoxInput
formControlName="mainColorOff">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.power-button.background</div>
<tb-color-input asBoxInput
formControlName="backgroundColorOff">
</tb-color-input>
</div>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.power-button.disabled-colors' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="12px">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.power-button.main</div>
<tb-color-input asBoxInput
formControlName="mainColorDisabled">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.power-button.background</div>
<tb-color-input asBoxInput
formControlName="backgroundColorDisabled">
</tb-color-input>
</div>
</div>
</div>
</div>
</ng-container>

88
ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.ts

@ -0,0 +1,88 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } from '@angular/core';
import { TargetDevice, WidgetSettings, WidgetSettingsComponent, widgetType } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { ValueType } from '@shared/models/constants';
import {
powerButtonDefaultSettings,
powerButtonLayoutImages,
powerButtonLayouts,
powerButtonLayoutTranslations
} from '@home/components/widget/lib/rpc/power-button-widget.models';
@Component({
selector: 'tb-power-button-widget-settings',
templateUrl: './power-button-widget-settings.component.html',
styleUrls: ['./../widget-settings.scss']
})
export class PowerButtonWidgetSettingsComponent extends WidgetSettingsComponent {
get targetDevice(): TargetDevice {
return this.widgetConfig?.config?.targetDevice;
}
get widgetType(): widgetType {
return this.widgetConfig?.widgetType;
}
powerButtonLayouts = powerButtonLayouts;
powerButtonLayoutTranslationMap = powerButtonLayoutTranslations;
powerButtonLayoutImageMap = powerButtonLayoutImages;
valueType = ValueType;
powerButtonWidgetSettingsForm: UntypedFormGroup;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.powerButtonWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {...powerButtonDefaultSettings};
}
protected onSettingsSet(settings: WidgetSettings) {
this.powerButtonWidgetSettingsForm = this.fb.group({
initialState: [settings.initialState, []],
onUpdateState: [settings.onUpdateState, []],
offUpdateState: [settings.offUpdateState, []],
disabledState: [settings.disabledState, []],
layout: [settings.layout, []],
mainColorOn: [settings.mainColorOn, []],
backgroundColorOn: [settings.backgroundColorOn, []],
mainColorOff: [settings.mainColorOff, []],
backgroundColorOff: [settings.backgroundColorOff, []],
mainColorDisabled: [settings.mainColorDisabled, []],
backgroundColorDisabled: [settings.backgroundColorDisabled, []],
background: [settings.background, []]
});
}
}

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.html

@ -35,7 +35,7 @@
{{ 'widgets.progress-bar.auto-scale' | translate }} {{ 'widgets.progress-bar.auto-scale' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
<div class="tb-form-row space-between column-xs"> <div class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide" formControlName="showValue"> <mat-slide-toggle class="mat-slide" formControlName="showValue">
{{ 'widgets.progress-bar.value' | translate }} {{ 'widgets.progress-bar.value' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
@ -48,7 +48,7 @@
</tb-color-settings> </tb-color-settings>
</div> </div>
</div> </div>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.progress-bar.range' | translate }}</div> <div>{{ 'widgets.progress-bar.range' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div class="tb-small-label" translate>widgets.progress-bar.min</div> <div class="tb-small-label" translate>widgets.progress-bar.min</div>

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.html

@ -97,7 +97,8 @@
<div class="fixed-title-width">{{ (setValueSettingsFormGroup.get('action').value === setValueAction.EXECUTE_RPC ? <div class="fixed-title-width">{{ (setValueSettingsFormGroup.get('action').value === setValueAction.EXECUTE_RPC ?
'widgets.value-action.parameters' : 'widgets.value-action.value') | translate }}</div> 'widgets.value-action.parameters' : 'widgets.value-action.value') | translate }}</div>
<tb-toggle-select fxFlex formControlName="type"> <tb-toggle-select fxFlex formControlName="type">
<tb-toggle-option [value]="valueToDataType.CONSTANT">{{ 'widgets.value-action.converter-constant' | translate }}</tb-toggle-option> <tb-toggle-option [value]="valueToDataType.VALUE">{{ 'widgets.value-action.converter-value' | translate }}</tb-toggle-option>
<tb-toggle-option *ngIf="valueType === ValueType.BOOLEAN" [value]="valueToDataType.CONSTANT">{{ 'widgets.value-action.converter-constant' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="valueToDataType.FUNCTION">{{ 'widgets.value-action.converter-function' | translate }}</tb-toggle-option> <tb-toggle-option [value]="valueToDataType.FUNCTION">{{ 'widgets.value-action.converter-function' | translate }}</tb-toggle-option>
<tb-toggle-option *ngIf="setValueSettingsFormGroup.get('action').value === setValueAction.EXECUTE_RPC" <tb-toggle-option *ngIf="setValueSettingsFormGroup.get('action').value === setValueAction.EXECUTE_RPC"
[value]="valueToDataType.NONE">{{ 'widgets.value-action.converter-none' | translate }}</tb-toggle-option> [value]="valueToDataType.NONE">{{ 'widgets.value-action.converter-none' | translate }}</tb-toggle-option>

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.ts

@ -32,6 +32,7 @@ import { TargetDevice, widgetType } from '@shared/models/widget.models';
import { AttributeScope, DataKeyType, telemetryTypeTranslationsShort } from '@shared/models/telemetry/telemetry.models'; import { AttributeScope, DataKeyType, telemetryTypeTranslationsShort } from '@shared/models/telemetry/telemetry.models';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { WidgetService } from '@core/http/widget.service'; import { WidgetService } from '@core/http/widget.service';
import { ValueType } from '@shared/models/constants';
@Component({ @Component({
selector: 'tb-set-value-action-settings-panel', selector: 'tb-set-value-action-settings-panel',
@ -45,6 +46,9 @@ export class SetValueActionSettingsPanelComponent extends PageComponent implemen
@Input() @Input()
panelTitle: string; panelTitle: string;
@Input()
valueType = ValueType.BOOLEAN;
@Input() @Input()
setValueSettings: SetValueSettings; setValueSettings: SetValueSettings;
@ -79,6 +83,8 @@ export class SetValueActionSettingsPanelComponent extends PageComponent implemen
functionScopeVariables = this.widgetService.getWidgetScopeVariables(); functionScopeVariables = this.widgetService.getWidgetScopeVariables();
ValueType = ValueType;
setValueSettingsFormGroup: UntypedFormGroup; setValueSettingsFormGroup: UntypedFormGroup;
constructor(private fb: UntypedFormBuilder, constructor(private fb: UntypedFormBuilder,

8
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings.component.ts

@ -36,6 +36,7 @@ import { isDefinedAndNotNull } from '@core/utils';
import { import {
SetValueActionSettingsPanelComponent SetValueActionSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component'; } from '@home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component';
import { ValueType } from '@shared/models/constants';
@Component({ @Component({
selector: 'tb-set-value-action-settings', selector: 'tb-set-value-action-settings',
@ -58,6 +59,9 @@ export class SetValueActionSettingsComponent implements OnInit, ControlValueAcce
@Input() @Input()
panelTitle: string; panelTitle: string;
@Input()
valueType = ValueType.BOOLEAN;
@Input() @Input()
aliasController: IAliasController; aliasController: IAliasController;
@ -114,6 +118,7 @@ export class SetValueActionSettingsComponent implements OnInit, ControlValueAcce
const ctx: any = { const ctx: any = {
setValueSettings: this.modelValue, setValueSettings: this.modelValue,
panelTitle: this.panelTitle, panelTitle: this.panelTitle,
valueType: this.valueType,
aliasController: this.aliasController, aliasController: this.aliasController,
targetDevice: this.targetDevice, targetDevice: this.targetDevice,
widgetType: this.widgetType widgetType: this.widgetType
@ -137,6 +142,9 @@ export class SetValueActionSettingsComponent implements OnInit, ControlValueAcce
private updateDisplayValue() { private updateDisplayValue() {
let value: any; let value: any;
switch (this.modelValue.valueToData.type) { switch (this.modelValue.valueToData.type) {
case ValueToDataType.VALUE:
value = 'value';
break;
case ValueToDataType.CONSTANT: case ValueToDataType.CONSTANT:
value = this.modelValue.valueToData.constantValue; value = this.modelValue.valueToData.constantValue;
break; break;

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

@ -80,6 +80,22 @@
</mat-form-field> </mat-form-field>
</div> </div>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="widgetActionType.openURL">
<div class="tb-form-row">
<div class="tb-required">{{ 'widget-action.URL' | translate }}</div>
<mat-form-field fxFlex style="margin-left: 9px" appearance="outline" subscriptSizing="dynamic">
<input matInput placeholder="{{ 'widget-config.set' | translate }}" formControlName="url">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'widget-action.url-required' | translate"
*ngIf="actionTypeFormGroup.get('url').invalid && actionTypeFormGroup.get('url').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</ng-template>
<ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState || <ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState ||
widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState ? widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState ?
widgetActionFormGroup.get('type').value : ''"> widgetActionFormGroup.get('type').value : ''">
@ -89,7 +105,8 @@
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ? <ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ||
widgetActionFormGroup.get('type').value === widgetActionType.openURL ?
widgetActionFormGroup.get('type').value : ''"> widgetActionFormGroup.get('type').value : ''">
<div class="tb-form-row"> <div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="openNewBrowserTab"> <mat-slide-toggle class="mat-slide" formControlName="openNewBrowserTab">

10
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts

@ -279,6 +279,16 @@ export class WidgetActionComponent implements ControlValueAccessor, OnInit, Vali
this.fb.control(action ? action.mobileAction : null, [Validators.required]) this.fb.control(action ? action.mobileAction : null, [Validators.required])
); );
break; break;
case WidgetActionType.openURL:
this.actionTypeFormGroup.addControl(
'openNewBrowserTab',
this.fb.control(action ? action.openNewBrowserTab : false, [])
);
this.actionTypeFormGroup.addControl(
'url',
this.fb.control(action ? action.url : null, [Validators.required])
);
break;
} }
} }
this.actionTypeFormGroupSubscriptions.push( this.actionTypeFormGroupSubscriptions.push(

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-appearance.component.html

@ -56,7 +56,7 @@
</tb-material-icon-select> </tb-material-icon-select>
</div> </div>
</div> </div>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.button.color-palette' | translate }}</div> <div>{{ 'widgets.button.color-palette' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="12px"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="12px">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html

@ -81,7 +81,7 @@
{{ 'widgets.single-switch.auto-scale' | translate }} {{ 'widgets.single-switch.auto-scale' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
<div class="tb-form-row"> <div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showLabel"> <mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showLabel">
{{ 'widgets.single-switch.label' | translate }} {{ 'widgets.single-switch.label' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
@ -98,7 +98,7 @@
</tb-color-input> </tb-color-input>
</div> </div>
</div> </div>
<div class="tb-form-row"> <div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon"> <mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon">
{{ 'widgets.single-switch.icon' | translate }} {{ 'widgets.single-switch.icon' | translate }}
</mat-slide-toggle> </mat-slide-toggle>

216
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slider-widget-settings.component.html

@ -0,0 +1,216 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container [formGroup]="sliderWidgetSettingsForm">
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.slider.behavior</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.slider.initial-value-hint' | translate}}" translate>widgets.slider.initial-value</div>
<tb-get-value-action-settings fxFlex
panelTitle="widgets.slider.initial-value"
[valueType]="valueType.DOUBLE"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="initialState"></tb-get-value-action-settings>
</div>
<div class="tb-form-row space-between">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.slider.on-value-change-hint' | translate}}" translate>widgets.slider.on-value-change</div>
<tb-set-value-action-settings fxFlex
panelTitle="widgets.slider.on-value-change"
[valueType]="valueType.DOUBLE"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="valueChange"></tb-set-value-action-settings>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.disabled-state-hint' | translate}}" translate>widgets.rpc-state.disabled-state</div>
<tb-get-value-action-settings fxFlex
panelTitle="widgets.rpc-state.disabled-state"
[valueType]="valueType.BOOLEAN"
stateLabel="widgets.rpc-state.disabled"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="disabledState"></tb-get-value-action-settings>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-style</div>
<tb-image-cards-select rowHeight="2:1"
[cols]="{columns: 3,
breakpoints: {
'lt-sm': 1,
'lt-md': 2
}}"
label="{{ 'widgets.slider.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of sliderLayouts"
[value]="layout"
[image]="sliderLayoutImageMap.get(layout)">
{{ sliderLayoutTranslationMap.get(layout) | translate }}
</tb-image-cards-select-option>
</tb-image-cards-select>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="autoScale">
{{ 'widgets.slider.auto-scale' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.background.background' | translate }}</div>
<tb-background-settings formControlName="background">
</tb-background-settings>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.slider.slider</div>
<div *ngIf="sliderWidgetSettingsForm.get('layout').value !== sliderLayout.simplified" class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showValue">
{{ 'widgets.slider.value' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-unit-input class="flex" formControlName="valueUnits"></tb-unit-input>
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic">
<input matInput formControlName="valueDecimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix fxHide.lt-md translate>widget-config.decimals-suffix</div>
</mat-form-field>
<tb-font-settings formControlName="valueFont"
[previewText]="valuePreviewFn">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="valueColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.slider.range' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div class="tb-small-label" translate>widgets.slider.min</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="tickMin" type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<div class="tb-small-label" translate>widgets.slider.max</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="tickMax" type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<div class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide" formControlName="showTicks">
{{ 'widgets.slider.range-ticks' | translate }}
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-font-settings formControlName="ticksFont"
previewText="100">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="ticksColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide" formControlName="showTickMarks">
{{ 'widgets.slider.tick-marks' | translate }}
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="tickMarksCount" type="number" min="2"
step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-color-input asBoxInput
formControlName="tickMarksColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.slider.colors' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="12px">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.slider.main</div>
<tb-color-input asBoxInput
formControlName="mainColor">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.slider.background</div>
<tb-color-input asBoxInput
formControlName="backgroundColor">
</tb-color-input>
</div>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.rpc-state.disabled-state' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="12px">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.slider.main</div>
<tb-color-input asBoxInput
formControlName="mainColorDisabled">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.slider.background</div>
<tb-color-input asBoxInput
formControlName="backgroundColorDisabled">
</tb-color-input>
</div>
</div>
</div>
<div *ngIf="sliderWidgetSettingsForm.get('layout').value === sliderLayout.extended"
class="tb-form-row column-xs">
<div class="fixed-title-width">
{{ 'widgets.slider.left-icon' | translate }}
</div>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="leftIconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select fxFlex formControlName="leftIconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
[color]="sliderWidgetSettingsForm.get('leftIconColor').value"
formControlName="leftIcon">
</tb-material-icon-select>
<tb-color-input asBoxInput
formControlName="leftIconColor">
</tb-color-input>
</div>
</div>
<div *ngIf="sliderWidgetSettingsForm.get('layout').value === sliderLayout.extended"
class="tb-form-row column-xs">
<div class="fixed-title-width">
{{ 'widgets.slider.right-icon' | translate }}
</div>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="rightIconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select fxFlex formControlName="rightIconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
[color]="sliderWidgetSettingsForm.get('rightIconColor').value"
formControlName="rightIcon">
</tb-material-icon-select>
<tb-color-input asBoxInput
formControlName="rightIconColor">
</tb-color-input>
</div>
</div>
</div>
</ng-container>

186
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slider-widget-settings.component.ts

@ -0,0 +1,186 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } from '@angular/core';
import { TargetDevice, WidgetSettings, WidgetSettingsComponent, widgetType } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { ValueType } from '@shared/models/constants';
import {
SliderLayout,
sliderLayoutImages,
sliderLayouts,
sliderLayoutTranslations,
sliderWidgetDefaultSettings
} from '@home/components/widget/lib/rpc/slider-widget.models';
import { formatValue } from '@core/utils';
@Component({
selector: 'tb-slider-widget-settings',
templateUrl: './slider-widget-settings.component.html',
styleUrls: ['./../widget-settings.scss']
})
export class SliderWidgetSettingsComponent extends WidgetSettingsComponent {
get targetDevice(): TargetDevice {
return this.widgetConfig?.config?.targetDevice;
}
get widgetType(): widgetType {
return this.widgetConfig?.widgetType;
}
sliderLayout = SliderLayout;
sliderLayouts = sliderLayouts;
sliderLayoutTranslationMap = sliderLayoutTranslations;
sliderLayoutImageMap = sliderLayoutImages;
valueType = ValueType;
sliderWidgetSettingsForm: UntypedFormGroup;
valuePreviewFn = this._valuePreviewFn.bind(this);
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.sliderWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {...sliderWidgetDefaultSettings};
}
protected onSettingsSet(settings: WidgetSettings) {
this.sliderWidgetSettingsForm = this.fb.group({
initialState: [settings.initialState, []],
valueChange: [settings.valueChange, []],
disabledState: [settings.disabledState, []],
layout: [settings.layout, []],
autoScale: [settings.autoScale, []],
showValue: [settings.showValue, []],
valueUnits: [settings.valueUnits, []],
valueDecimals: [settings.valueDecimals, []],
valueFont: [settings.valueFont, []],
valueColor: [settings.valueColor, []],
tickMin: [settings.tickMin, []],
tickMax: [settings.tickMax, []],
showTicks: [settings.showTicks, []],
ticksFont: [settings.ticksFont, []],
ticksColor: [settings.ticksColor, []],
showTickMarks: [settings.showTickMarks, []],
tickMarksCount: [settings.tickMarksCount, [Validators.min(2)]],
tickMarksColor: [settings.tickMarksColor, []],
mainColor: [settings.mainColor, []],
backgroundColor: [settings.backgroundColor, []],
mainColorDisabled: [settings.mainColorDisabled, []],
backgroundColorDisabled: [settings.backgroundColorDisabled, []],
leftIconSize: [settings.leftIconSize, [Validators.min(0)]],
leftIconSizeUnit: [settings.leftIconSizeUnit, []],
leftIcon: [settings.leftIcon, []],
leftIconColor: [settings.leftIconColor, []],
rightIconSize: [settings.rightIconSize, [Validators.min(0)]],
rightIconSizeUnit: [settings.rightIconSizeUnit, []],
rightIcon: [settings.rightIcon, []],
rightIconColor: [settings.rightIconColor, []],
background: [settings.background, []],
});
}
protected validatorTriggers(): string[] {
return ['showValue', 'showTicks', 'showTickMarks', 'layout'];
}
protected updateValidators(_emitEvent: boolean): void {
const showValue: boolean = this.sliderWidgetSettingsForm.get('showValue').value;
const showTicks: boolean = this.sliderWidgetSettingsForm.get('showTicks').value;
const showTickMarks: boolean = this.sliderWidgetSettingsForm.get('showTickMarks').value;
const layout: SliderLayout = this.sliderWidgetSettingsForm.get('layout').value;
const valueEnabled = layout !== SliderLayout.simplified;
const leftRightIconsEnabled = layout === SliderLayout.extended;
if (valueEnabled && showValue) {
this.sliderWidgetSettingsForm.get('valueUnits').enable();
this.sliderWidgetSettingsForm.get('valueDecimals').enable();
this.sliderWidgetSettingsForm.get('valueFont').enable();
this.sliderWidgetSettingsForm.get('valueColor').enable();
} else {
this.sliderWidgetSettingsForm.get('valueUnits').disable();
this.sliderWidgetSettingsForm.get('valueDecimals').disable();
this.sliderWidgetSettingsForm.get('valueFont').disable();
this.sliderWidgetSettingsForm.get('valueColor').disable();
}
if (showTicks) {
this.sliderWidgetSettingsForm.get('ticksFont').enable();
this.sliderWidgetSettingsForm.get('ticksColor').enable();
} else {
this.sliderWidgetSettingsForm.get('ticksFont').disable();
this.sliderWidgetSettingsForm.get('ticksColor').disable();
}
if (showTickMarks) {
this.sliderWidgetSettingsForm.get('tickMarksCount').enable();
this.sliderWidgetSettingsForm.get('tickMarksColor').enable();
} else {
this.sliderWidgetSettingsForm.get('tickMarksCount').disable();
this.sliderWidgetSettingsForm.get('tickMarksColor').disable();
}
if (leftRightIconsEnabled) {
this.sliderWidgetSettingsForm.get('leftIconSize').enable();
this.sliderWidgetSettingsForm.get('leftIconSizeUnit').enable();
this.sliderWidgetSettingsForm.get('leftIcon').enable();
this.sliderWidgetSettingsForm.get('leftIconColor').enable();
this.sliderWidgetSettingsForm.get('rightIconSize').enable();
this.sliderWidgetSettingsForm.get('rightIconSizeUnit').enable();
this.sliderWidgetSettingsForm.get('rightIcon').enable();
this.sliderWidgetSettingsForm.get('rightIconColor').enable();
} else {
this.sliderWidgetSettingsForm.get('leftIconSize').disable();
this.sliderWidgetSettingsForm.get('leftIconSizeUnit').disable();
this.sliderWidgetSettingsForm.get('leftIcon').disable();
this.sliderWidgetSettingsForm.get('leftIconColor').disable();
this.sliderWidgetSettingsForm.get('rightIconSize').disable();
this.sliderWidgetSettingsForm.get('rightIconSizeUnit').disable();
this.sliderWidgetSettingsForm.get('rightIcon').disable();
this.sliderWidgetSettingsForm.get('rightIconColor').disable();
}
}
private _valuePreviewFn(): string {
const units: string = this.sliderWidgetSettingsForm.get('valueUnits').value;
const decimals: number = this.sliderWidgetSettingsForm.get('valueDecimals').value;
return formatValue(48, decimals, units, false);
}
}

46
ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.html

@ -19,8 +19,8 @@
<div class="tb-form-panel no-padding no-border"> <div class="tb-form-panel no-padding no-border">
<div class="tb-form-panel"> <div class="tb-form-panel">
<div fxFlex fxLayout="column" class="tb-form-row space-between"> <div fxFlex fxLayout="column" class="tb-form-row space-between">
<div fxFlex fxLayout="row" style="width: 100%;" fxLayoutAlign="space-between center"> <div class="tb-flex row space-between align-center no-gap fill-width">
<div class="fixed-title-width" translate>widgets.liquid-level-card.shape</div> <div class="fixed-title-width" translate>widgets.liquid-level-card.shape</div>
<tb-toggle-select formControlName="tankSelectionType"> <tb-toggle-select formControlName="tankSelectionType">
<tb-toggle-option *ngFor="let type of DataSourceTypes" [value]="type"> <tb-toggle-option *ngFor="let type of DataSourceTypes" [value]="type">
{{ DataSourceTypeTranslations.get(type) | translate }} {{ DataSourceTypeTranslations.get(type) | translate }}
@ -51,7 +51,7 @@
formControlName="shapeAttributeName"> formControlName="shapeAttributeName">
</tb-string-autocomplete> </tb-string-autocomplete>
</div> </div>
<div fxFlex fxLayout="row" fxLayoutAlign="space-between center" style="width: 100%;"> <div class="tb-flex row space-between align-center no-gap fill-width">
<div class="tb-hint" style="padding: 0;" translate>widgets.liquid-level-card.shape-by-attribute</div> <div class="tb-hint" style="padding: 0;" translate>widgets.liquid-level-card.shape-by-attribute</div>
<div class="see-example" <div class="see-example"
[tb-help-popup]="'widget/lib/indicator/shape_attribute_fn'" [tb-help-popup]="'widget/lib/indicator/shape_attribute_fn'"
@ -87,9 +87,9 @@
formControlName="datasourceUnits"> formControlName="datasourceUnits">
</tb-unit-input> </tb-unit-input>
</div> </div>
<div class="tb-form-row space-between" *ngIf="levelCardWidgetSettingsForm.get('layout').value === LevelCardLayout.absolute"> <div class="tb-form-row space-between column-xs" *ngIf="levelCardWidgetSettingsForm.get('layout').value === LevelCardLayout.absolute">
<div class="fixed-title-width tb-required" translate>widgets.liquid-level-card.widget-units</div> <div class="fixed-title-width tb-required" translate>widgets.liquid-level-card.widget-units</div>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> <div class="tb-flex row flex-start align-center">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic" style="max-width: 25%"> <mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic" style="max-width: 25%">
<mat-select formControlName="widgetUnitsSource" placeholder="{{ 'widget-config.set' | translate }}"> <mat-select formControlName="widgetUnitsSource" placeholder="{{ 'widget-config.set' | translate }}">
<mat-option *ngFor="let type of DataSourceTypes" [value]="type"> <mat-option *ngFor="let type of DataSourceTypes" [value]="type">
@ -111,9 +111,9 @@
</ng-template> </ng-template>
</div> </div>
</div> </div>
<div class="tb-form-row" *ngIf="volumeInput"> <div class="tb-form-row column-xs" *ngIf="volumeInput">
<div class="fixed-title-width tb-required" translate>widgets.liquid-level-card.total-volume</div> <div class="fixed-title-width tb-required" translate>widgets.liquid-level-card.total-volume</div>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> <div class="tb-flex row flex-start align-center">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic" style="max-width: 25%"> <mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic" style="max-width: 25%">
<mat-select formControlName="volumeSource" placeholder="{{ 'widget-config.set' | translate }}"> <mat-select formControlName="volumeSource" placeholder="{{ 'widget-config.set' | translate }}">
<mat-option *ngFor="let type of DataSourceTypes" [value]="type"> <mat-option *ngFor="let type of DataSourceTypes" [value]="type">
@ -141,10 +141,30 @@
formControlName="volumeAttributeName"> formControlName="volumeAttributeName">
</tb-string-autocomplete> </tb-string-autocomplete>
</ng-template> </ng-template>
<tb-unit-input [tagFilter]="unitsType.capacity" </div>
required style="max-width: 25%" class="flex" </div>
<div class="tb-form-row space-between column-xs" [fxShow]="volumeInput">
<div class="fixed-title-width tb-required" translate>widgets.liquid-level-card.total-volume-units</div>
<div class="tb-flex row flex-start align-center">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic" style="max-width: 25%">
<mat-select formControlName="volumeUnitsSource" placeholder="{{ 'widget-config.set' | translate }}">
<mat-option *ngFor="let type of DataSourceTypes" [value]="type">
{{ DataSourceTypeTranslations.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<tb-unit-input *ngIf="levelCardWidgetSettingsForm.get('volumeUnitsSource').value === DataSourceType.static; else selectVolumeUnitsAttributes"
class="flex" required
[tagFilter]="unitsType.capacity"
formControlName="volumeUnits"> formControlName="volumeUnits">
</tb-unit-input> </tb-unit-input>
<ng-template #selectVolumeUnitsAttributes>
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)"
required style="flex: 1"
[errorText]="'widgets.liquid-level-card.attribute-name-required' | translate"
formControlName="volumeUnitsAttributeName">
</tb-string-autocomplete>
</ng-template>
</div> </div>
</div> </div>
</div> </div>
@ -168,7 +188,7 @@
<div class="tb-form-row space-between" <div class="tb-form-row space-between"
*ngIf="levelCardWidgetSettingsForm.get('layout').value !== LevelCardLayout.simple"> *ngIf="levelCardWidgetSettingsForm.get('layout').value !== LevelCardLayout.simple">
<div class="fixed-title-width" translate>widgets.liquid-level-card.value</div> <div class="fixed-title-width" translate>widgets.liquid-level-card.value</div>
<div fxFlex fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px"> <div class="tb-flex row flex-end align-center">
<tb-font-settings formControlName="valueFont" <tb-font-settings formControlName="valueFont"
[previewText]="valuePreviewFn"> [previewText]="valuePreviewFn">
</tb-font-settings> </tb-font-settings>
@ -178,7 +198,7 @@
</div> </div>
<div class="tb-form-row" *ngIf="levelCardWidgetSettingsForm.get('layout').value === LevelCardLayout.absolute"> <div class="tb-form-row" *ngIf="levelCardWidgetSettingsForm.get('layout').value === LevelCardLayout.absolute">
<div class="fixed-title-width" translate>widgets.liquid-level-card.total-volume</div> <div class="fixed-title-width" translate>widgets.liquid-level-card.total-volume</div>
<div fxFlex fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px"> <div class="tb-flex row flex-end align-center">
<tb-font-settings formControlName="volumeFont" <tb-font-settings formControlName="volumeFont"
[previewText]="totalVolumeValuePreviewFn"> [previewText]="totalVolumeValuePreviewFn">
</tb-font-settings> </tb-font-settings>
@ -212,7 +232,7 @@
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTooltipLevel"> <mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTooltipLevel">
{{ 'widgets.liquid-level-card.level' | translate }} {{ 'widgets.liquid-level-card.level' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> <div class="tb-flex row flex-start align-center">
<tb-unit-input [tagFilter]="unitsType.capacity" <tb-unit-input [tagFilter]="unitsType.capacity"
required class="flex" formControlName="tooltipUnits"> required class="flex" formControlName="tooltipUnits">
</tb-unit-input> </tb-unit-input>
@ -232,7 +252,7 @@
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTooltipDate"> <mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTooltipDate">
{{ 'widgets.value-card.date' | translate }} {{ 'widgets.value-card.date' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<div fxFlex.gt-xs fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> <div class="tb-flex row flex-start align-center">
<tb-date-format-select fxFlex formControlName="tooltipDateFormat"></tb-date-format-select> <tb-date-format-select fxFlex formControlName="tooltipDateFormat"></tb-date-format-select>
<tb-font-settings formControlName="tooltipDateFont" <tb-font-settings formControlName="tooltipDateFont"
[previewText]="datePreviewFn"> [previewText]="datePreviewFn">

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.ts

@ -158,7 +158,9 @@ export class LiquidLevelCardWidgetSettingsComponent extends WidgetSettingsCompon
volumeSource: [settings.volumeSource, []], volumeSource: [settings.volumeSource, []],
volumeConstant: [settings.volumeConstant, [Validators.required, Validators.min(0.1)]], volumeConstant: [settings.volumeConstant, [Validators.required, Validators.min(0.1)]],
volumeAttributeName: [settings.volumeAttributeName, [Validators.required]], volumeAttributeName: [settings.volumeAttributeName, [Validators.required]],
volumeUnitsSource: [settings.volumeUnitsSource, []],
volumeUnits: [settings.volumeUnits, [Validators.required]], volumeUnits: [settings.volumeUnits, [Validators.required]],
volumeUnitsAttributeName: [settings.volumeUnitsAttributeName, [Validators.required]],
volumeFont: [settings.volumeFont, []], volumeFont: [settings.volumeFont, []],
volumeColor: [settings.volumeColor, []], volumeColor: [settings.volumeColor, []],
valueFont: [settings.valueFont, []], valueFont: [settings.valueFont, []],
@ -195,7 +197,7 @@ export class LiquidLevelCardWidgetSettingsComponent extends WidgetSettingsCompon
protected validatorTriggers(): string[] { protected validatorTriggers(): string[] {
return [ return [
'showBackgroundOverlay', 'showTooltip', 'showTooltipLevel', 'tankSelectionType', 'datasourceUnits', 'showBackgroundOverlay', 'showTooltip', 'showTooltipLevel', 'tankSelectionType', 'datasourceUnits',
'showTooltipDate', 'layout', 'volumeSource', 'widgetUnitsSource' 'showTooltipDate', 'layout', 'volumeSource', 'widgetUnitsSource', 'volumeUnitsSource'
]; ];
} }

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

@ -321,6 +321,12 @@ import {
import { import {
CommandButtonWidgetSettingsComponent CommandButtonWidgetSettingsComponent
} from '@home/components/widget/lib/settings/button/command-button-widget-settings.component'; } from '@home/components/widget/lib/settings/button/command-button-widget-settings.component';
import {
PowerButtonWidgetSettingsComponent
} from '@home/components/widget/lib/settings/button/power-button-widget-settings.component';
import {
SliderWidgetSettingsComponent
} from '@home/components/widget/lib/settings/control/slider-widget-settings.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -436,7 +442,9 @@ import {
BarChartWithLabelsWidgetSettingsComponent, BarChartWithLabelsWidgetSettingsComponent,
SingleSwitchWidgetSettingsComponent, SingleSwitchWidgetSettingsComponent,
ActionButtonWidgetSettingsComponent, ActionButtonWidgetSettingsComponent,
CommandButtonWidgetSettingsComponent CommandButtonWidgetSettingsComponent,
PowerButtonWidgetSettingsComponent,
SliderWidgetSettingsComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -557,7 +565,9 @@ import {
BarChartWithLabelsWidgetSettingsComponent, BarChartWithLabelsWidgetSettingsComponent,
SingleSwitchWidgetSettingsComponent, SingleSwitchWidgetSettingsComponent,
ActionButtonWidgetSettingsComponent, ActionButtonWidgetSettingsComponent,
CommandButtonWidgetSettingsComponent CommandButtonWidgetSettingsComponent,
PowerButtonWidgetSettingsComponent,
SliderWidgetSettingsComponent
] ]
}) })
export class WidgetSettingsModule { export class WidgetSettingsModule {
@ -645,5 +655,7 @@ export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsCo
'tb-bar-chart-with-labels-widget-settings': BarChartWithLabelsWidgetSettingsComponent, 'tb-bar-chart-with-labels-widget-settings': BarChartWithLabelsWidgetSettingsComponent,
'tb-single-switch-widget-settings': SingleSwitchWidgetSettingsComponent, 'tb-single-switch-widget-settings': SingleSwitchWidgetSettingsComponent,
'tb-action-button-widget-settings': ActionButtonWidgetSettingsComponent, 'tb-action-button-widget-settings': ActionButtonWidgetSettingsComponent,
'tb-command-button-widget-settings': CommandButtonWidgetSettingsComponent 'tb-command-button-widget-settings': CommandButtonWidgetSettingsComponent,
'tb-power-button-widget-settings': PowerButtonWidgetSettingsComponent,
'tb-slider-widget-settings': SliderWidgetSettingsComponent
}; };

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

Loading…
Cancel
Save