Browse Source

Merge branch 'master' of github.com:thingsboard/thingsboard into edqs

pull/12818/head
ViacheslavKlimov 2 years ago
parent
commit
92b83335c0
  1. 17
      application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md
  2. 108
      application/src/main/data/json/system/scada_symbols/bottom-flow-meter.svg
  3. 2
      application/src/main/data/json/system/scada_symbols/control-panel-hp.svg
  4. 2
      application/src/main/data/json/system/scada_symbols/dynamic-vertical-scale-hp.svg
  5. 2
      application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg
  6. 108
      application/src/main/data/json/system/scada_symbols/horizontal-inline-flow-meter.svg
  7. 2
      application/src/main/data/json/system/scada_symbols/leak-sensor.svg
  8. 2
      application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg
  9. 2
      application/src/main/data/json/system/scada_symbols/left-drain-pipe.svg
  10. 2
      application/src/main/data/json/system/scada_symbols/left-elbow-drain-pipe.svg
  11. 104
      application/src/main/data/json/system/scada_symbols/left-flow-meter.svg
  12. 2
      application/src/main/data/json/system/scada_symbols/long-bottom-filter.svg
  13. 2
      application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg
  14. 2
      application/src/main/data/json/system/scada_symbols/long-top-filter.svg
  15. 2
      application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg
  16. 2
      application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg
  17. 2
      application/src/main/data/json/system/scada_symbols/right-drain-pipe.svg
  18. 2
      application/src/main/data/json/system/scada_symbols/right-elbow-drain-pipe.svg
  19. 104
      application/src/main/data/json/system/scada_symbols/right-flow-meter.svg
  20. 2
      application/src/main/data/json/system/scada_symbols/short-bottom-filter.svg
  21. 2
      application/src/main/data/json/system/scada_symbols/short-left-drain-pipe.svg
  22. 2
      application/src/main/data/json/system/scada_symbols/short-right-drain-pipe.svg
  23. 2
      application/src/main/data/json/system/scada_symbols/short-top-filter.svg
  24. 2
      application/src/main/data/json/system/scada_symbols/simple-vertical-scale-hp.svg
  25. 108
      application/src/main/data/json/system/scada_symbols/top-flow-meter.svg
  26. 2
      application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg
  27. 108
      application/src/main/data/json/system/scada_symbols/vertical-inline-flow-meter.svg
  28. 1
      application/src/main/data/json/system/widget_bundles/buttons.json
  29. 1
      application/src/main/data/json/system/widget_bundles/control_widgets.json
  30. 1
      application/src/main/data/json/system/widget_bundles/general_high_performance_scada_symbols.json
  31. 2
      application/src/main/data/json/system/widget_bundles/high_performance_scada_fluid_system.json
  32. 51
      application/src/main/data/json/system/widget_types/value_stepper.json
  33. 4
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
  34. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  35. 11
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  36. 1
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java
  37. 18
      application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java
  38. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java
  39. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java
  40. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java
  41. 4
      application/src/main/resources/thingsboard.yml
  42. 34
      application/src/test/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessorTest.java
  43. 3
      application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java
  44. 10
      application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
  45. 25
      application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java
  46. 40
      application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java
  47. 3
      common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java
  48. 3
      common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java
  49. 2
      docker/docker-compose.prometheus-grafana.yml
  50. 2
      ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts
  51. 5
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  52. 32
      ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.html
  53. 38
      ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.ts
  54. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.html
  55. 297
      ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.html
  56. 225
      ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.ts
  57. 7
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.ts
  58. 373
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts
  59. 43
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.html
  60. 87
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.scss
  61. 391
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.ts
  62. 252
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.models.ts
  63. 266
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.html
  64. 191
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.ts
  65. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.html
  66. 5
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  67. 3
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  68. 8
      ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.html
  69. 2
      ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.ts
  70. 6
      ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html
  71. 2
      ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.ts
  72. 6
      ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-config.resolve.ts
  73. 41
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  74. 1
      ui-ngx/src/assets/widget/value-stepper/filled.svg
  75. 1
      ui-ngx/src/assets/widget/value-stepper/simplified.svg
  76. 1
      ui-ngx/src/assets/widget/value-stepper/volume.svg

17
application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md

@ -43,19 +43,6 @@ volumes:
Execute the following command to start upgrade process:
```bash
docker compose -f docker-compose-upgrade.yml up
docker compose -f docker-compose-upgrade.yml up --abort-on-container-exit
{:copy-code}
```
Once upgrade process successfully completed, exit from the docker-compose shell by this combination:
```text
Ctrl + C
```
Execute the following command to stop TB Edge upgrade container:
```bash
docker compose -f docker-compose-upgrade.yml stop
{:copy-code}
```
```

108
application/src/main/data/json/system/scada_symbols/bottom-flow-meter.svg

@ -57,7 +57,7 @@
},
{
"tag": "value",
"stateRenderFunction": "var value = ctx.values.value;\nctx.api.text(element, value.toFixed(0));\n",
"stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'displayClick');"
@ -463,16 +463,20 @@
"name": "{i18n:scada.symbol.units}",
"type": "units",
"default": "m³/hr",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "medium-width",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "valueDecimals",
"name": "{i18n:scada.symbol.decimals}",
"type": "number",
"default": 0,
"fieldClass": "medium-width",
"min": 0,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "defaultBorderColor",
@ -511,16 +515,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "activeBorderColor",
@ -559,16 +555,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "warningBorderColor",
@ -607,16 +595,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "criticalBorderColor",
@ -655,16 +635,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "backgroundColor",
@ -703,48 +675,24 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "fluidColor",
"name": "{i18n:scada.symbol.fluid-color}",
"type": "color",
"default": "#1EC1F480",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "pipeColor",
"name": "{i18n:scada.symbol.pipe-color}",
"type": "color",
"default": "#FFFFFF",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
}
]
}]]></tb:metadata>

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 73 KiB

2
application/src/main/data/json/system/scada_symbols/control-panel-hp.svg

@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="400" height="200" fill="none" version="1.1" viewBox="0 0 400 200"><tb:metadata xmlns=""><![CDATA[{
"title": "HP Control panel",
"description": "Control panel",
"description": "Sends the command to the device or updates attribute/time series when the user pushes the button. Widget settings will enable you to configure behavior, how to fetch the initial state, and what to trigger when power on/off states.",
"searchTags": [
"control"
],

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

2
application/src/main/data/json/system/scada_symbols/dynamic-vertical-scale-hp.svg

@ -776,7 +776,7 @@
}
]
}]]></tb:metadata>
<text x="206.16602" y="43.234375" fill="#000000" font-family="Roboto" font-size="56px" font-weight="400" text-anchor="middle" tb:tag="label" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">Outdoor</tspan></text><text x="195.21875" y="784.75" fill="#000000" font-family="Roboto" font-size="40px" font-weight="400" text-anchor="middle" tb:tag="units" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">°C</tspan></text><g stroke="#000" tb:tag="scale">
<text x="206.16602" y="43.234375" fill="#000000" font-family="Roboto" font-size="56px" font-weight="400" text-anchor="middle" tb:tag="label" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">Outdoor</tspan></text><text x="195.21875" y="770" fill="#000000" font-family="Roboto" font-size="40px" font-weight="400" text-anchor="middle" tb:tag="units" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">°C</tspan></text><g stroke="#000" tb:tag="scale">
<rect x="176.5" y="83" width="41" height="652" fill="#c8dff7" tb:tag="scaleBackground"/>
<rect transform="scale(1,-1)" x="176.5" y="-735" width="41" height="164" fill="#ebebeb" tb:tag="lowWarningScale"/>
<rect x="176.5" y="83.5" width="41" height="164" fill="#ebebeb" tb:tag="highWarningScale"/>

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

2
application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg

@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="200" fill="none" version="1.1" viewBox="0 0 200 200"><tb:metadata xmlns=""><![CDATA[{
"title": "HP Horizontal connector",
"description": "Horizontal connector",
"description": "Horizontal connector with an optional directional arrow to visually indicate flow.",
"widgetSizeX": 1,
"widgetSizeY": 1,
"tags": [

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

108
application/src/main/data/json/system/scada_symbols/horizontal-inline-flow-meter.svg

@ -57,7 +57,7 @@
},
{
"tag": "value",
"stateRenderFunction": "var value = ctx.values.value;\nctx.api.text(element, value.toFixed(0));\n",
"stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'displayClick');"
@ -463,16 +463,20 @@
"name": "{i18n:scada.symbol.units}",
"type": "units",
"default": "m³/hr",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "medium-width",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "valueDecimals",
"name": "{i18n:scada.symbol.decimals}",
"type": "number",
"default": 0,
"fieldClass": "medium-width",
"min": 0,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "defaultBorderColor",
@ -511,16 +515,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "activeBorderColor",
@ -559,16 +555,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "warningBorderColor",
@ -607,16 +595,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "criticalBorderColor",
@ -655,16 +635,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "backgroundColor",
@ -703,48 +675,24 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "fluidColor",
"name": "{i18n:scada.symbol.fluid-color}",
"type": "color",
"default": "#1EC1F480",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "pipeColor",
"name": "{i18n:scada.symbol.pipe-color}",
"type": "color",
"default": "#FFFFFF",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
}
]
}]]></tb:metadata>

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 76 KiB

2
application/src/main/data/json/system/scada_symbols/leak-sensor.svg

@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="100" height="100" fill="none" version="1.1" viewBox="0 0 100 100"><tb:metadata xmlns=""><![CDATA[{
"title": "Leak sensor",
"description": "Leak sensor",
"description": "Leak sensor for real-time detection and warning of fluid leakage.",
"searchTags": [
"leak"
],

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

2
application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg

@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="200" fill="none" version="1.1" viewBox="0 0 200 200"><tb:metadata xmlns=""><![CDATA[{
"title": "Left analog water level meter",
"description": "Left analog water level meter",
"description": "Left analog water level meter with real-time display of fluid levels.",
"searchTags": [
"water meter"
],

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

2
application/src/main/data/json/system/scada_symbols/left-drain-pipe.svg

@ -1,7 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="400" height="200" fill="none" version="1.1" viewBox="0 0 400 200">
<tb:metadata><![CDATA[{
"title": "Left drain pipe",
"description": "Left drain pipe",
"description": "Left drain pipe with configurable fluid and leak visualizations.",
"searchTags": [
"pipe",
"drain"

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

2
application/src/main/data/json/system/scada_symbols/left-elbow-drain-pipe.svg

@ -1,7 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="400" height="200" fill="none" version="1.1" viewBox="0 0 400 200">
<tb:metadata xmlns=""><![CDATA[{
"title": "Left elbow drain pipe",
"description": "Left elbow drain pipe",
"description": "Left elbow drain pipe with configurable fluid and leak visualizations.",
"searchTags": [
"pipe",
"drain"

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

104
application/src/main/data/json/system/scada_symbols/left-flow-meter.svg

@ -57,7 +57,7 @@
},
{
"tag": "value",
"stateRenderFunction": "var value = ctx.values.value;\nctx.api.text(element, value.toFixed(0));\n",
"stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'displayClick');"
@ -463,16 +463,16 @@
"name": "{i18n:scada.symbol.units}",
"type": "units",
"default": "m³/hr",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "medium-width"
},
{
"id": "valueDecimals",
"name": "{i18n:scada.symbol.decimals}",
"type": "number",
"default": 0,
"fieldClass": "medium-width",
"min": null,
"max": null,
"step": null
"min": 0,
"step": 1
},
{
"id": "defaultBorderColor",
@ -510,17 +510,7 @@
"rangeAdvanced": []
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
},
{
"id": "activeBorderColor",
@ -558,17 +548,7 @@
"rangeAdvanced": []
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
},
{
"id": "warningBorderColor",
@ -606,17 +586,7 @@
"rangeAdvanced": []
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
},
{
"id": "criticalBorderColor",
@ -654,17 +624,7 @@
"rangeAdvanced": []
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
},
{
"id": "backgroundColor",
@ -702,49 +662,19 @@
"rangeAdvanced": []
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
},
{
"id": "fluidColor",
"name": "{i18n:scada.symbol.fluid-color}",
"type": "color",
"default": "#1EC1F480",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"default": "#1EC1F480"
},
{
"id": "pipeColor",
"name": "{i18n:scada.symbol.pipe-color}",
"type": "color",
"default": "#FFFFFF",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"default": "#FFFFFF"
}
]
}]]></tb:metadata>

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 72 KiB

2
application/src/main/data/json/system/scada_symbols/long-bottom-filter.svg

@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="600" fill="none" version="1.1" viewBox="0 0 200 600">
<tb:metadata xmlns=""><![CDATA[{
"title": "Long bottom filter",
"title": "Long bottom filter with configurable click actions for custom operations, dashboard manipulation, etc.",
"description": "Long bottom filter",
"searchTags": [
"filter"

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

2
application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg

@ -1,7 +1,7 @@
<svg width="400" height="200" fill="none" version="1.1" viewBox="0 0 400 200" xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg">
<tb:metadata><![CDATA[{
"title": "HP Long horizontal connector",
"description": "Long horizontal connector",
"description": "Long horizontal connector with an optional directional arrow to visually indicate flow.",
"widgetSizeX": 2,
"widgetSizeY": 1,
"tags": [

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

2
application/src/main/data/json/system/scada_symbols/long-top-filter.svg

@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="600" fill="none" version="1.1" viewBox="0 0 200 600">
<tb:metadata xmlns=""><![CDATA[{
"title": "Long top filter",
"title": "Long top filter with configurable click actions for custom operations, dashboard manipulation, etc.",
"description": "Title\nLong top filter\n",
"searchTags": [
"filter"

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

2
application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg

@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="400" fill="none" version="1.1" viewBox="0 0 200 400"><tb:metadata xmlns=""><![CDATA[{
"title": "HP Long vertical connector",
"description": "Long vertical connector",
"description": "Long vertical connector with an optional directional arrow to visually indicate flow.",
"widgetSizeX": 1,
"widgetSizeY": 2,
"tags": [

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

2
application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg

@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="200" fill="none" version="1.1" viewBox="0 0 200 200"><tb:metadata xmlns=""><![CDATA[{
"title": "Right analog water level meter",
"description": "Right analog water level meter",
"description": "Right analog water level meter with real-time display of fluid levels.",
"searchTags": [
"water meter"
],

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

2
application/src/main/data/json/system/scada_symbols/right-drain-pipe.svg

@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="400" height="200" fill="none" version="1.1" viewBox="0 0 400 200"><tb:metadata xmlns=""><![CDATA[{
"title": "Right drain pipe",
"description": "Right drain pipe",
"description": "Right drain pipe with configurable fluid and leak visualizations.",
"searchTags": [
"pipe",
"drain"

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

2
application/src/main/data/json/system/scada_symbols/right-elbow-drain-pipe.svg

@ -1,7 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="400" height="200" fill="none" version="1.1" viewBox="0 0 400 200">
<tb:metadata xmlns=""><![CDATA[{
"title": "Right elbow drain pipe",
"description": "Right elbow drain pipe",
"description": "Right elbow drain pipe with configurable fluid and leak visualizations.",
"searchTags": [
"pipe",
"drain"

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

104
application/src/main/data/json/system/scada_symbols/right-flow-meter.svg

@ -57,7 +57,7 @@
},
{
"tag": "value",
"stateRenderFunction": "var value = ctx.values.value;\nctx.api.text(element, value.toFixed(0));\n",
"stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'displayClick');"
@ -463,16 +463,16 @@
"name": "{i18n:scada.symbol.units}",
"type": "units",
"default": "m³/hr",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "medium-width"
},
{
"id": "valueDecimals",
"name": "{i18n:scada.symbol.decimals}",
"type": "number",
"default": 0,
"fieldClass": "medium-width",
"min": null,
"max": null,
"step": null
"min": 0,
"step": 1
},
{
"id": "defaultBorderColor",
@ -510,17 +510,7 @@
"rangeAdvanced": []
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
},
{
"id": "activeBorderColor",
@ -558,17 +548,7 @@
"rangeAdvanced": []
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
},
{
"id": "warningBorderColor",
@ -606,17 +586,7 @@
"rangeAdvanced": []
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
},
{
"id": "criticalBorderColor",
@ -654,17 +624,7 @@
"rangeAdvanced": []
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
},
{
"id": "backgroundColor",
@ -702,49 +662,19 @@
"rangeAdvanced": []
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
},
{
"id": "fluidColor",
"name": "{i18n:scada.symbol.fluid-color}",
"type": "color",
"default": "#1EC1F480",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"default": "#1EC1F480"
},
{
"id": "pipeColor",
"name": "{i18n:scada.symbol.pipe-color}",
"type": "color",
"default": "#FFFFFF",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"default": "#FFFFFF"
}
]
}]]></tb:metadata>

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 72 KiB

2
application/src/main/data/json/system/scada_symbols/short-bottom-filter.svg

@ -1,7 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="400" fill="none" version="1.1" viewBox="0 0 200 400">
<tb:metadata xmlns=""><![CDATA[{
"title": "Short bottom filter",
"description": "Short bottom filter",
"description": "Short bottom filter with configurable click actions for custom operations, dashboard manipulation, etc.",
"searchTags": [
"filter"
],

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

2
application/src/main/data/json/system/scada_symbols/short-left-drain-pipe.svg

@ -1,7 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="200" fill="none" version="1.1" viewBox="0 0 200 200">
<tb:metadata><![CDATA[{
"title": "Short left drain pipe",
"description": "Short left drain pipe",
"description": "Short left drain pipe with configurable fluid and leak visualizations.",
"searchTags": [
"pipe",
"drain"

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

2
application/src/main/data/json/system/scada_symbols/short-right-drain-pipe.svg

@ -1,7 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="200" fill="none" version="1.1" viewBox="0 0 200 200">
<tb:metadata><![CDATA[{
"title": "Short right drain pipe",
"description": "Short right drain pipe",
"description": "Short right drain pipe with configurable fluid and leak visualizations.",
"searchTags": [
"pipe",
"drain"

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

2
application/src/main/data/json/system/scada_symbols/short-top-filter.svg

@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="400" fill="none" version="1.1" viewBox="0 0 200 400">
<tb:metadata xmlns=""><![CDATA[{
"title": "Short top filter",
"title": "Short top filter with configurable click actions for custom operations, dashboard manipulation, etc.",
"description": "Short top filter",
"searchTags": [
"filter"

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

2
application/src/main/data/json/system/scada_symbols/simple-vertical-scale-hp.svg

@ -704,7 +704,7 @@
}
]
}]]></tb:metadata>
<text x="206.16602" y="43.234375" fill="#000000" font-family="Roboto" font-size="56px" font-weight="400" text-anchor="middle" tb:tag="label" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">Outdoor</tspan></text><text x="195.21875" y="786.75" fill="#000000" font-family="Roboto" font-size="40px" font-weight="400" text-anchor="middle" tb:tag="units" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">°C</tspan></text><g stroke="#000" tb:tag="scale">
<text x="206.16602" y="43.234375" fill="#000000" font-family="Roboto" font-size="56px" font-weight="400" text-anchor="middle" tb:tag="label" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">Outdoor</tspan></text><text x="195.21875" y="770" fill="#000000" font-family="Roboto" font-size="40px" font-weight="400" text-anchor="middle" tb:tag="units" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">°C</tspan></text><g stroke="#000" tb:tag="scale">
<rect x="176.5" y="83" width="41" height="652" fill="#c8dff7" tb:tag="scaleBackground"/>
<rect transform="scale(1,-1)" x="176.5" y="-735" width="41" height="164" fill="#ebebeb" tb:tag="lowWarningScale"/>
<rect x="176.5" y="83.5" width="41" height="164" fill="#ebebeb" tb:tag="highWarningScale"/>

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

108
application/src/main/data/json/system/scada_symbols/top-flow-meter.svg

@ -57,7 +57,7 @@
},
{
"tag": "value",
"stateRenderFunction": "var value = ctx.values.value;\nctx.api.text(element, value.toFixed(0));\n",
"stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'displayClick');"
@ -463,16 +463,20 @@
"name": "{i18n:scada.symbol.units}",
"type": "units",
"default": "m³/hr",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "medium-width",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "valueDecimals",
"name": "{i18n:scada.symbol.decimals}",
"type": "number",
"default": 0,
"fieldClass": "medium-width",
"min": 0,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "defaultBorderColor",
@ -511,16 +515,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "activeBorderColor",
@ -559,16 +555,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "warningBorderColor",
@ -607,16 +595,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "criticalBorderColor",
@ -655,16 +635,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "backgroundColor",
@ -703,48 +675,24 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "fluidColor",
"name": "{i18n:scada.symbol.fluid-color}",
"type": "color",
"default": "#1EC1F480",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "pipeColor",
"name": "{i18n:scada.symbol.pipe-color}",
"type": "color",
"default": "#FFFFFF",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
}
]
}]]></tb:metadata>

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 72 KiB

2
application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg

@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="200" fill="none" version="1.1" viewBox="0 0 200 200"><tb:metadata xmlns=""><![CDATA[{
"title": "HP Vertical connector",
"description": "Vertical connector",
"description": "Vertical connector with an optional directional arrow to visually indicate flow.",
"widgetSizeX": 1,
"widgetSizeY": 1,
"tags": [

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

108
application/src/main/data/json/system/scada_symbols/vertical-inline-flow-meter.svg

@ -57,7 +57,7 @@
},
{
"tag": "value",
"stateRenderFunction": "var value = ctx.values.value;\nctx.api.text(element, value.toFixed(0));\n",
"stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n",
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'displayClick');"
@ -463,16 +463,20 @@
"name": "{i18n:scada.symbol.units}",
"type": "units",
"default": "m³/hr",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "medium-width",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "valueDecimals",
"name": "{i18n:scada.symbol.decimals}",
"type": "number",
"default": 0,
"fieldClass": "medium-width",
"min": 0,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "defaultBorderColor",
@ -511,16 +515,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "activeBorderColor",
@ -559,16 +555,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "warningBorderColor",
@ -607,16 +595,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "criticalBorderColor",
@ -655,16 +635,8 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "backgroundColor",
@ -703,48 +675,24 @@
},
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
},
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "fluidColor",
"name": "{i18n:scada.symbol.fluid-color}",
"type": "color",
"default": "#1EC1F480",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "pipeColor",
"name": "{i18n:scada.symbol.pipe-color}",
"type": "color",
"default": "#FFFFFF",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
}
]
}]]></tb:metadata>

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 76 KiB

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

@ -12,6 +12,7 @@
"command_button",
"toggle_button",
"two_segment_button",
"value_stepper",
"power_button"
]
}

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

@ -12,6 +12,7 @@
"command_button",
"toggle_button",
"two_segment_button",
"value_stepper",
"power_button",
"slider",
"control_widgets.switch_control",

1
application/src/main/data/json/system/widget_bundles/general_high_performance_scada_symbols.json

@ -29,6 +29,7 @@
"hp_top_tee_connector",
"hp_drawwork",
"hp_crane",
"hp_hook",
"hp_consumers",
"hp_house",
"hp_apartments",

2
application/src/main/data/json/system/widget_bundles/high_performance_scada_fluid_system.json

File diff suppressed because one or more lines are too long

51
application/src/main/data/json/system/widget_types/value_stepper.json

@ -0,0 +1,51 @@
{
"fqn": "value_stepper",
"name": "Value stepper",
"deprecated": false,
"image": "tb-image;/api/images/system/value-stepper-widget.svg",
"description": "Allows users to click the buttons to send commands to devices or update attributes/time series data. Configurable settings let users define how to retrieve the initial state and specify actions for each button.",
"descriptor": {
"type": "rpc",
"sizeX": 3.5,
"sizeY": 2,
"resources": [],
"templateHtml": "<tb-value-stepper-widget\n [ctx]='ctx'\n [widgetTitlePanel]=\"widgetTitlePanel\">\n</tb-value-stepper-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '230px',\n previewHeight: '110px',\n embedTitlePanel: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n",
"dataKeySettingsForm": [],
"settingsDirective": "tb-value-stepper-widget-settings",
"hasBasicMode": true,
"basicModeDirective": "tb-value-stepper-basic-config",
"defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"initialState\":{\"action\":\"EXECUTE_RPC\",\"defaultValue\":0,\"executeRpc\":{\"method\":\"getState\",\"requestTimeout\":5000,\"requestPersistent\":false,\"persistentPollingInterval\":1000},\"getAttribute\":{\"key\":\"state\",\"scope\":null},\"getTimeSeries\":{\"key\":\"state\"},\"getAlarmStatus\":{\"severityList\":null,\"typeList\":null},\"dataToValue\":{\"type\":\"NONE\",\"compareToValue\":true,\"dataToValueFunction\":\"/* Should return integer value */\\nreturn data;\"}},\"disabledState\":{\"action\":\"DO_NOTHING\",\"defaultValue\":false,\"getAttribute\":{\"key\":\"state\",\"scope\":null},\"getTimeSeries\":{\"key\":\"state\"},\"getAlarmStatus\":{\"severityList\":null,\"typeList\":null},\"dataToValue\":{\"type\":\"NONE\",\"compareToValue\":true,\"dataToValueFunction\":\"/* Should return boolean value */\\nreturn data;\"}},\"leftButtonClick\":{\"action\":\"EXECUTE_RPC\",\"executeRpc\":{\"method\":\"setState\",\"requestTimeout\":5000,\"requestPersistent\":false,\"persistentPollingInterval\":1000},\"setAttribute\":{\"key\":\"state\",\"scope\":\"SERVER_SCOPE\"},\"putTimeSeries\":{\"key\":\"state\"},\"valueToData\":{\"type\":\"VALUE\",\"constantValue\":0,\"valueToDataFunction\":\"/* Convert input integer value to RPC parameters or attribute/time-series value */\\nreturn value;\"}},\"rightButtonClick\":{\"action\":\"EXECUTE_RPC\",\"executeRpc\":{\"method\":\"setState\",\"requestTimeout\":5000,\"requestPersistent\":false,\"persistentPollingInterval\":1000},\"setAttribute\":{\"key\":\"state\",\"scope\":\"SERVER_SCOPE\"},\"putTimeSeries\":{\"key\":\"state\"},\"valueToData\":{\"type\":\"VALUE\",\"constantValue\":0,\"valueToDataFunction\":\"/* Convert input integer value to RPC parameters or attribute/time-series value */\\nreturn value;\"}},\"appearance\":{\"type\":\"simplified\",\"autoScale\":true,\"minValueRange\":-100,\"maxValueRange\":100,\"valueStep\":0.5,\"showValueBox\":true,\"valueUnits\":\"\",\"valueDecimals\":1,\"valueFont\":{\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"size\":16,\"sizeUnit\":\"px\",\"lineHeight\":\"24px\"},\"valueColor\":\"#000\",\"valueBoxBackground\":\"rgba(0, 0, 0, 0.04)\",\"showBorder\":true,\"borderWidth\":1,\"borderColor\":\"#305680\"},\"buttonAppearance\":{\"leftButton\":{\"showButton\":true,\"icon\":\"arrow_back_ios_new\",\"iconSize\":24,\"iconSizeUnit\":\"px\",\"mainColorOn\":\"#3F52DD\",\"backgroundColorOn\":\"#FFFFFF\",\"mainColorDisabled\":\"rgba(0,0,0,0.12)\",\"backgroundColorDisabled\":\"#FFFFFF\",\"customStyle\":{\"enabled\":null,\"hovered\":null,\"pressed\":null,\"activated\":null,\"disabled\":null}},\"rightButton\":{\"showButton\":true,\"icon\":\"arrow_forward_ios\",\"iconSize\":24,\"iconSizeUnit\":\"px\",\"mainColorOn\":\"#3F52DD\",\"backgroundColorOn\":\"#FFFFFF\",\"mainColorDisabled\":\"rgba(0,0,0,0.12)\",\"backgroundColorDisabled\":\"#FFFFFF\",\"customStyle\":{\"enabled\":null,\"hovered\":null,\"pressed\":null,\"activated\":null,\"disabled\":null}}},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"12px\"},\"title\":\"Value stepper\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"actions\":{},\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":null,\"weight\":\"500\",\"style\":null,\"lineHeight\":\"24px\"},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"configMode\":\"basic\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"datasources\":null,\"borderRadius\":null}"
},
"resources": [
{
"link": "/api/images/system/value-stepper-widget.svg",
"title": "\"Value stepper\" system widget image",
"type": "IMAGE",
"subType": "IMAGE",
"fileName": "value-stepper-widget.svg",
"publicResourceKey": "s0UKoqbiMCcKVn0pD55XZzPUR89XlXAO",
"mediaType": "image/svg+xml",
"data": "PHN2ZyB3aWR0aD0iMjE0IiBoZWlnaHQ9Ijc2IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxnIGZpbHRlcj0idXJsKCNhKSI+PHJlY3QgeD0iOC41IiB5PSI0LjUiIHdpZHRoPSIxOTciIGhlaWdodD0iNTkiIHJ4PSI0IiBmaWxsPSIjZmZmIiBzaGFwZS1yZW5kZXJpbmc9ImNyaXNwRWRnZXMiLz48cmVjdCB4PSI5IiB5PSI1IiB3aWR0aD0iMTk2IiBoZWlnaHQ9IjU4IiByeD0iMy41IiBzdHJva2U9IiMzMDU2ODAiIHNoYXBlLXJlbmRlcmluZz0iY3Jpc3BFZGdlcyIvPjxyZWN0IHg9IjIwLjUiIHk9IjE4IiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHJ4PSIxNiIgZmlsbD0iIzMwNTY4MCIvPjxwYXRoIGQ9Im0zOC41IDQwIDEuNDEtMS40MUwzNS4zMyAzNGw0LjU4LTQuNTlMMzguNSAyOGwtNiA2IDYgNloiIGZpbGw9IiNmZmYiLz48cmVjdCB4PSI2NSIgeT0iMTguNSIgd2lkdGg9Ijg0IiBoZWlnaHQ9IjMxIiByeD0iMy41IiBmaWxsPSIjMzA1NjgwIiBmaWxsLW9wYWNpdHk9Ii4wNCIvPjxyZWN0IHg9IjY1IiB5PSIxOC41IiB3aWR0aD0iODQiIGhlaWdodD0iMzEiIHJ4PSIzLjUiIHN0cm9rZT0iIzMwNTY4MCIvPjxwYXRoIGQ9Ik04OC41OSAzNy41VjM5aC03LjYzdi0xLjI5bDMuNy00LjA0Yy40MS0uNDYuNzMtLjg1Ljk3LTEuMTkuMjMtLjMzLjQtLjYzLjQ5LS45YTIuMyAyLjMgMCAwIDAtLjA2LTEuNzNjLS4xMy0uMjctLjMyLS41LS41OC0uNjVhMS43IDEuNyAwIDAgMC0uOTMtLjI0Yy0uNDIgMC0uNzcuMS0xLjA2LjI3LS4yOC4xOS0uNS40NC0uNjUuNzZhMi42IDIuNiAwIDAgMC0uMjIgMS4xaC0xLjg4YzAtLjY3LjE1LTEuMjcuNDYtMS44Mi4zLS41NS43My0uOTkgMS4zLTEuM2E0LjEgNC4xIDAgMCAxIDIuMDgtLjVjLjc2IDAgMS40LjEzIDEuOTQuMzguNTMuMjYuOTMuNjIgMS4yIDEuMDlhMy4zNiAzLjM2IDAgMCAxIC4yNSAyLjcyIDUgNSAwIDAgMS0uNDkgMS4wNCA5IDkgMCAwIDEtLjc0IDEuMDRjLS4yOC4zNS0uNi43LS45NCAxLjA1bC0yLjQ2IDIuNzFoNS4yNVptOS4yNy05Ljg4djEuMDRMOTMuMyAzOWgtMS45OGw0LjU0LTkuODhoLTUuOXYtMS41aDcuODlabTEuOTggMTAuNDRjMC0uMjkuMS0uNTMuMy0uNzMuMi0uMi40Ni0uMy44LS4zcy42LjEuOC4zYy4yLjIuMy40NC4zLjczYTEgMSAwIDAgMS0uMy43NGMtLjIuMi0uNDYuMy0uOC4zcy0uNi0uMS0uOC0uM2ExIDEgMCAwIDEtLjMtLjc0Wm02LjQ5LTQuMzUtMS41LS4zNy42MS01LjcyaDYuMTR2MS42SDEwN2wtLjMyIDIuNzlhMy42NyAzLjY3IDAgMCAxIDEuODEtLjQ2Yy41NCAwIDEuMDIuMDkgMS40NC4yNi40Mi4xNy43OS40MyAxLjA4Ljc2LjMuMzMuNTMuNzMuNjggMS4yYTUgNSAwIDAgMSAwIDMuMDcgMy4xNyAzLjE3IDAgMCAxLTEuODUgMi4wM2MtLjQ2LjE5LTEuMDEuMjktMS42NS4yOWE0LjYgNC42IDAgMCAxLTEuMzYtLjIgMy43MyAzLjczIDAgMCAxLTEuMTctLjYyIDMuMTQgMy4xNCAwIDAgMS0xLjE5LTIuNDJoMS44NWMuMDUuMzcuMTUuNjkuMy45NS4xNi4yNS4zOC40NS42NC41OGEyIDIgMCAwIDAgLjkyLjJjLjMyIDAgLjYtLjA1LjgzLS4xNi4yMy0uMTEuNDItLjI3LjU3LS40OC4xNS0uMjIuMjctLjQ3LjM0LS43NWEzLjYyIDMuNjIgMCAwIDAtLjAyLTEuODcgMS45OCAxLjk4IDAgMCAwLS4zOC0uNzJjLS4xNy0uMi0uMzgtLjM2LS42My0uNDdhMi4xMyAyLjEzIDAgMCAwLS44OC0uMTdjLS40NSAwLS44LjA3LTEuMDQuMi0uMjMuMTMtLjQ1LjI5LS42NS40OFptMTEuNzUtNC4xNmMwLS4zOC4xLS43Mi4yOC0xLjA0LjE5LS4zMi40NC0uNTcuNzUtLjc2YTEuOTYgMS45NiAwIDAgMSAyLjA1IDBjLjMxLjE5LjU2LjQ0Ljc0Ljc2LjE5LjMyLjI4LjY2LjI4IDEuMDRzLS4xLjczLS4yOCAxLjA1Yy0uMTguMzEtLjQzLjU2LS43NC43NGEyLjA0IDIuMDQgMCAwIDEtMi44LS43NCAyLjAzIDIuMDMgMCAwIDEtLjI4LTEuMDVabTEuMDUgMGExIDEgMCAwIDAgMSAxIC45Ny45NyAwIDAgMCAuOTgtMSAxIDEgMCAwIDAtLjI3LS43Mi45My45MyAwIDAgMC0uNy0uM2MtLjI4IDAtLjUxLjEtLjcxLjMtLjIuMi0uMy40My0uMy43MlptMTIuMTkgNS43NWgxLjk1YTQuNSA0LjUgMCAwIDEtLjYyIDEuOTkgMy43MiAzLjcyIDAgMCAxLTEuNSAxLjM3IDUgNSAwIDAgMS0yLjMzLjUgNC4xNSA0LjE1IDAgMCAxLTMuMzQtMS40NWMtLjQtLjQ4LS43MS0xLjA0LS45Mi0xLjctLjIxLS42Ni0uMzItMS40LS4zMi0yLjIydi0uOTVjMC0uODEuMS0xLjU1LjMyLTIuMjIuMjItLjY2LjUzLTEuMjIuOTQtMS42OS40LS40Ny45LS44NCAxLjQ2LTEuMDlhNC43OCA0Ljc4IDAgMCAxIDEuOTMtLjM3Yy45IDAgMS42Ny4xNyAyLjMuNS42Mi4zMyAxLjEuOCAxLjQ1IDEuMzguMzUuNTkuNTYgMS4yNi42NCAyLjAyaC0xLjk1Yy0uMDUtLjQ4LS4xNy0uOS0uMzUtMS4yNWExLjc3IDEuNzcgMCAwIDAtLjc2LS44IDIuNzMgMi43MyAwIDAgMC0xLjMzLS4yOGMtLjQ1IDAtLjg0LjA4LTEuMTcuMjVhMi4yIDIuMiAwIDAgMC0uODQuNzNjLS4yMi4zMy0uMzkuNzItLjUgMS4yLS4xMS40Ny0uMTcgMS0uMTcgMS42di45N2MwIC41Ny4wNSAxLjEuMTUgMS41Ni4xLjQ3LjI2Ljg2LjQ3IDEuMi4yMS4zMy40OC41OS44MS43Ny4zMy4xOC43Mi4yNyAxLjE4LjI3LjU2IDAgMS0uMDggMS4zNS0uMjYuMzUtLjE4LjYxLS40NC44LS43OC4xNy0uMzQuMy0uNzYuMzUtMS4yNVoiIGZpbGw9IiMwMDAiIGZpbGwtb3BhY2l0eT0iLjg3Ii8+PHJlY3QgeD0iMTYxLjUiIHk9IjE4IiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHJ4PSIxNiIgZmlsbD0iIzMwNTY4MCIvPjxwYXRoIGQ9Im0xNzUuNSAyOC0xLjQxIDEuNDEgNC41OCA0LjU5LTQuNTggNC41OUwxNzUuNSA0MGw2LTYtNi02WiIgZmlsbD0iI2ZmZiIvPjwvZz48ZGVmcz48ZmlsdGVyIGlkPSJhIiB4PSIuNSIgeT0iLjUiIHdpZHRoPSIyMTMiIGhlaWdodD0iNzUiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj48ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VBbHBoYSIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAxMjcgMCIgcmVzdWx0PSJoYXJkQWxwaGEiLz48ZmVPZmZzZXQgZHk9IjQiLz48ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSI0Ii8+PGZlQ29tcG9zaXRlIGluMj0iaGFyZEFscGhhIiBvcGVyYXRvcj0ib3V0Ii8+PGZlQ29sb3JNYXRyaXggdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwLjA0IDAiLz48ZmVCbGVuZCBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJlZmZlY3QxX2Ryb3BTaGFkb3dfNTY2OV8xNjA3MDUiLz48ZmVCbGVuZCBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJlZmZlY3QxX2Ryb3BTaGFkb3dfNTY2OV8xNjA3MDUiIHJlc3VsdD0ic2hhcGUiLz48L2ZpbHRlcj48L2RlZnM+PC9zdmc+",
"public": true
}
],
"scada": false,
"tags": [
"command",
"downlink",
"device configuration",
"device control",
"invocation",
"remote method",
"remote function",
"interface",
"subroutine call",
"inter-process communication",
"server request",
"update attribute",
"set attribute",
"add time-series"
]
}

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

@ -126,8 +126,8 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
final TenantId tenantId;
final DeviceId deviceId;
final LinkedHashMapRemoveEldest<UUID, SessionInfoMetaData> sessions;
private final Map<UUID, SessionInfo> attributeSubscriptions;
private final Map<UUID, SessionInfo> rpcSubscriptions;
final Map<UUID, SessionInfo> attributeSubscriptions;
final Map<UUID, SessionInfo> rpcSubscriptions;
private final Map<Integer, ToDeviceRpcRequestMetadata> toDeviceRpcPendingMap;
private final boolean rpcSequential;
private final RpcSubmitStrategy rpcSubmitStrategy;

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

@ -404,6 +404,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private void scheduleEdgeEventsCheck(EdgeGrpcSession session) {
EdgeId edgeId = session.getEdge().getId();
TenantId tenantId = session.getEdge().getTenantId();
cancelScheduleEdgeEventsCheck(edgeId);
if (sessions.containsKey(edgeId)) {
ScheduledFuture<?> edgeEventCheckTask = edgeEventProcessingExecutorService.schedule(() -> {
try {

11
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java

@ -448,7 +448,11 @@ public abstract class EdgeGrpcSession implements Closeable {
private void scheduleDownlinkMsgsPackSend(int attempt) {
Runnable sendDownlinkMsgsTask = () -> {
try {
if (isConnected() && !sessionState.getPendingMsgsMap().values().isEmpty()) {
if (!isConnected()) {
stopCurrentSendDownlinkMsgsTask(true);
return;
}
if (!sessionState.getPendingMsgsMap().values().isEmpty()) {
List<DownlinkMsg> copy = new ArrayList<>(sessionState.getPendingMsgsMap().values());
if (attempt > 1) {
String error = "Failed to deliver the batch";
@ -529,6 +533,11 @@ public abstract class EdgeGrpcSession implements Closeable {
log.debug("[{}][{}][{}] Msg has been processed successfully! Msg Id: [{}], Msg: {}", tenantId, edge.getId(), sessionId, msg.getDownlinkMsgId(), msg);
} else {
log.error("[{}][{}][{}] Msg processing failed! Msg Id: [{}], Error msg: {}", tenantId, edge.getId(), sessionId, msg.getDownlinkMsgId(), msg.getErrorMsg());
DownlinkMsg downlinkMsg = sessionState.getPendingMsgsMap().get(msg.getDownlinkMsgId());
// if NOT timeseries or attributes failures - ack failed downlink
if (downlinkMsg.getEntityDataCount() == 0) {
sessionState.getPendingMsgsMap().remove(msg.getDownlinkMsgId());
}
}
if (sessionState.getPendingMsgsMap().isEmpty()) {
log.debug("[{}][{}][{}] Pending msgs map is empty. Stopping current iteration", tenantId, edge.getId(), sessionId);

1
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java

@ -87,6 +87,7 @@ public class EdgeSyncCursor {
fetchers.add(new SystemWidgetsBundlesEdgeEventFetcher(ctx.getWidgetsBundleService()));
fetchers.add(new TenantWidgetsBundlesEdgeEventFetcher(ctx.getWidgetsBundleService()));
fetchers.add(new OtaPackagesEdgeEventFetcher(ctx.getOtaPackageService()));
fetchers.add(new DeviceProfilesEdgeEventFetcher(ctx.getDeviceProfileService()));
fetchers.add(new TenantResourcesEdgeEventFetcher(ctx.getResourceService()));
fetchers.add(new OAuth2EdgeEventFetcher(ctx.getDomainService()));
}

18
application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java

@ -54,23 +54,25 @@ public class GeneralEdgeEventFetcher implements EdgeEventFetcher {
log.trace("[{}] Finding general edge events [{}], seqIdStart = {}, pageLink = {}",
tenantId, edge.getId(), seqIdStart, pageLink);
PageData<EdgeEvent> edgeEvents = edgeEventService.findEdgeEvents(tenantId, edge.getId(), seqIdStart, null, (TimePageLink) pageLink);
if (edgeEvents.getData().isEmpty()) {
if (!edgeEvents.getData().isEmpty()) {
return edgeEvents;
}
if (seqIdStart > this.maxReadRecordsCount) {
edgeEvents = edgeEventService.findEdgeEvents(tenantId, edge.getId(), 0L, Math.max(this.maxReadRecordsCount, seqIdStart - this.maxReadRecordsCount), (TimePageLink) pageLink);
if (edgeEvents.getData().stream().anyMatch(ee -> ee.getSeqId() < seqIdStart)) {
log.info("[{}] seqId column of edge_event table started new cycle [{}]", tenantId, edge.getId());
this.seqIdNewCycleStarted = true;
this.seqIdStart = 0L;
} else {
edgeEvents = new PageData<>();
log.warn("[{}] unexpected edge notification message received. " +
"no new events found and seqId column of edge_event table doesn't started new cycle [{}]", tenantId, edge.getId());
return edgeEvents;
}
}
return edgeEvents;
log.info("[{}] Unexpected edge notification message received. " +
"No new events found, and the seqId column of the edge_event table has not started a new cycle [{}].", tenantId, edge.getId());
return new PageData<>();
} catch (Exception e) {
log.error("[{}] failed to find edge events [{}]", tenantId, edge.getId());
log.error("[{}] Failed to find edge events [{}]", tenantId, edge.getId(), e);
return new PageData<>();
}
return new PageData<>();
}
}

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

@ -120,7 +120,7 @@ public abstract class BaseEdgeProcessor {
private boolean doSaveIfEdgeIsOffline(EdgeEventType type, EdgeEventActionType action) {
return switch (action) {
case TIMESERIES_UPDATED, ALARM_ACK, ALARM_CLEAR, ALARM_ASSIGNED, ALARM_UNASSIGNED, ADDED_COMMENT,
UPDATED_COMMENT -> true;
UPDATED_COMMENT, DELETED -> true;
default -> switch (type) {
case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE,
WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, NOTIFICATION_TEMPLATE, NOTIFICATION_TARGET,

5
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java

@ -41,7 +41,10 @@ public abstract class BaseDashboardProcessor extends BaseEdgeProcessor {
if (dashboard == null) {
throw new RuntimeException("[{" + tenantId + "}] dashboardUpdateMsg {" + dashboardUpdateMsg + "} cannot be converted to dashboard");
}
Set<ShortCustomerInfo> newAssignedCustomers = new HashSet<>(dashboard.getAssignedCustomers());
Set<ShortCustomerInfo> newAssignedCustomers = new HashSet<>();
if (dashboard.getAssignedCustomers() != null && !dashboard.getAssignedCustomers().isEmpty()) {
newAssignedCustomers.addAll(dashboard.getAssignedCustomers());
}
Dashboard dashboardById = edgeCtx.getDashboardService().findDashboardById(tenantId, dashboardId);
if (dashboardById == null) {
created = true;

5
application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java

@ -28,17 +28,22 @@ import org.thingsboard.server.gen.edge.v1.WidgetBundleTypesRequestMsg;
public interface EdgeRequestsService {
@Deprecated(since = "3.9.1", forRemoval = true)
ListenableFuture<Void> processRuleChainMetadataRequestMsg(TenantId tenantId, Edge edge, RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg);
ListenableFuture<Void> processAttributesRequestMsg(TenantId tenantId, Edge edge, AttributesRequestMsg attributesRequestMsg);
ListenableFuture<Void> processRelationRequestMsg(TenantId tenantId, Edge edge, RelationRequestMsg relationRequestMsg);
@Deprecated(since = "3.9.1", forRemoval = true)
ListenableFuture<Void> processDeviceCredentialsRequestMsg(TenantId tenantId, Edge edge, DeviceCredentialsRequestMsg deviceCredentialsRequestMsg);
@Deprecated(since = "3.9.1", forRemoval = true)
ListenableFuture<Void> processUserCredentialsRequestMsg(TenantId tenantId, Edge edge, UserCredentialsRequestMsg userCredentialsRequestMsg);
@Deprecated(since = "3.9.1", forRemoval = true)
ListenableFuture<Void> processWidgetBundleTypesRequestMsg(TenantId tenantId, Edge edge, WidgetBundleTypesRequestMsg widgetBundleTypesRequestMsg);
@Deprecated(since = "3.9.1", forRemoval = true)
ListenableFuture<Void> processEntityViewsRequestMsg(TenantId tenantId, Edge edge, EntityViewsRequestMsg entityViewsRequestMsg);
}

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

@ -212,9 +212,9 @@ ui:
database:
ts_max_intervals: "${DATABASE_TS_MAX_INTERVALS:700}" # Max number of DB queries generated by a single API call to fetch telemetry records
ts:
type: "${DATABASE_TS_TYPE:sql}" # cassandra or sql. timescale option is deprecated and will no longer be supported in ThingsBoard 4.0
type: "${DATABASE_TS_TYPE:sql}" # cassandra, sql, or timescale (for hybrid mode, DATABASE_TS_TYPE value should be cassandra, or timescale)
ts_latest:
type: "${DATABASE_TS_LATEST_TYPE:sql}" # cassandra or sql. timescale option is deprecated and will no longer be supported in ThingsBoard 4.0
type: "${DATABASE_TS_LATEST_TYPE:sql}" # cassandra, sql, or timescale (for hybrid mode, DATABASE_TS_TYPE value should be cassandra, or timescale)
# Cassandra driver configuration parameters
cassandra:

34
application/src/test/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessorTest.java

@ -17,22 +17,27 @@ package org.thingsboard.server.actors.device;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.thingsboard.common.util.LinkedHashMapRemoveEldest;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.service.transport.TbCoreToTransportService;
import java.util.UUID;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
public class DeviceActorMessageProcessorTest {
public static final long MAX_CONCURRENT_SESSIONS_PER_DEVICE = 10L;
public static final int MAX_CONCURRENT_SESSIONS_PER_DEVICE = 10;
ActorSystemContext systemContext;
DeviceService deviceService;
TenantId tenantId = TenantId.SYS_TENANT_ID;
@ -44,15 +49,38 @@ public class DeviceActorMessageProcessorTest {
public void setUp() {
systemContext = mock(ActorSystemContext.class);
deviceService = mock(DeviceService.class);
willReturn(MAX_CONCURRENT_SESSIONS_PER_DEVICE).given(systemContext).getMaxConcurrentSessionsPerDevice();
willReturn((long)MAX_CONCURRENT_SESSIONS_PER_DEVICE).given(systemContext).getMaxConcurrentSessionsPerDevice();
willReturn(deviceService).given(systemContext).getDeviceService();
processor = new DeviceActorMessageProcessor(systemContext, tenantId, deviceId);
willReturn(mock(TbCoreToTransportService.class)).given(systemContext).getTbCoreToTransportService();
}
@Test
public void givenSystemContext_whenNewInstance_thenVerifySessionMapMaxSize() {
assertThat(processor.sessions, instanceOf(LinkedHashMapRemoveEldest.class));
assertThat(processor.sessions.getMaxEntries(), is(MAX_CONCURRENT_SESSIONS_PER_DEVICE));
assertThat(processor.sessions.getMaxEntries(), is((long)MAX_CONCURRENT_SESSIONS_PER_DEVICE));
assertThat(processor.sessions.getRemovalConsumer(), notNullValue());
}
@Test
public void givenFullSessionMap_whenSessionOverflow_thenShouldDeleteAttributeAndRPCSubscriptions() {
//givenFullSessionMap
for (int i = 0; i < MAX_CONCURRENT_SESSIONS_PER_DEVICE; i++) {
UUID sessionID = UUID.randomUUID();
processor.sessions.put(sessionID, Mockito.mock(SessionInfoMetaData.class, RETURNS_DEEP_STUBS));
processor.attributeSubscriptions.put(sessionID, Mockito.mock(SessionInfo.class));
processor.rpcSubscriptions.put(sessionID, Mockito.mock(SessionInfo.class));
}
assertThat(processor.sessions.size(), is(MAX_CONCURRENT_SESSIONS_PER_DEVICE));
assertThat(processor.attributeSubscriptions.size(), is(MAX_CONCURRENT_SESSIONS_PER_DEVICE));
assertThat(processor.rpcSubscriptions.size(), is(MAX_CONCURRENT_SESSIONS_PER_DEVICE));
//add one more
processor.sessions.put(UUID.randomUUID(), Mockito.mock(SessionInfoMetaData.class));
assertThat(processor.sessions.size(), is(MAX_CONCURRENT_SESSIONS_PER_DEVICE));
assertThat(processor.attributeSubscriptions.size(), is(MAX_CONCURRENT_SESSIONS_PER_DEVICE-1));
assertThat(processor.rpcSubscriptions.size(), is(MAX_CONCURRENT_SESSIONS_PER_DEVICE-1));
}
}

3
application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java

@ -897,7 +897,7 @@ public class EdgeControllerTest extends AbstractControllerTest {
edgeImitator.ignoreType(OAuth2ClientUpdateMsg.class);
edgeImitator.ignoreType(OAuth2DomainUpdateMsg.class);
edgeImitator.expectMessageAmount(26);
edgeImitator.expectMessageAmount(27);
edgeImitator.connect();
waitForMessages(edgeImitator);
@ -1003,6 +1003,7 @@ public class EdgeControllerTest extends AbstractControllerTest {
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "connectivity"));
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "jwt"));
Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));

10
application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java

@ -144,7 +144,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret());
edgeImitator.ignoreType(OAuth2ClientUpdateMsg.class);
edgeImitator.ignoreType(OAuth2DomainUpdateMsg.class);
edgeImitator.expectMessageAmount(24);
edgeImitator.expectMessageAmount(26);
edgeImitator.connect();
requestEdgeRuleChainMetadata();
@ -265,10 +265,10 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
// 4 messages
// - 1 from default profile fetcher
// - 2 from device profile fetcher (default and thermostat)
// - 4 from device profile fetcher (2 * (default and thermostat) before and after ota packages fetcher
// - 1 from device fetcher
validateMsgsCnt(DeviceProfileUpdateMsg.class, 4);
validateDeviceProfiles(4);
validateMsgsCnt(DeviceProfileUpdateMsg.class, 6);
validateDeviceProfiles(6);
// 3 messages
// - 1 from default profile fetcher
@ -656,7 +656,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
}
protected RuleChainId createEdgeRuleChainAndAssignToEdge(String ruleChainName) throws Exception {
edgeImitator.expectMessageAmount(1);
edgeImitator.expectMessageAmount(2);
RuleChain ruleChain = new RuleChain();
ruleChain.setName(ruleChainName);
ruleChain.setType(RuleChainType.EDGE);

25
application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java

@ -48,12 +48,14 @@ public class DashboardEdgeTest extends AbstractEdgeTest {
private static final int MOBILE_ORDER = 5;
private static final String IMAGE = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+";
private static final String DASHBOARD_TITLE = "Edge Test Dashboard";
@Test
public void testDashboards() throws Exception {
// create dashboard and assign to edge
edgeImitator.expectMessageAmount(2);
Dashboard dashboard = new Dashboard();
dashboard.setTitle("Edge Test Dashboard");
dashboard.setTitle(DASHBOARD_TITLE);
dashboard.setMobileHide(true);
dashboard.setImage(IMAGE);
dashboard.setMobileOrder(MOBILE_ORDER);
@ -200,12 +202,27 @@ public class DashboardEdgeTest extends AbstractEdgeTest {
Dashboard foundDashboard = doGet("/api/dashboard/" + dashboard.getUuidId(), Dashboard.class);
Assert.assertNotNull(foundDashboard);
Assert.assertEquals("Edge Test Dashboard", foundDashboard.getName());
Assert.assertEquals(DASHBOARD_TITLE, foundDashboard.getName());
PageData<DashboardInfo> pageData = doGetTypedWithPageLink("/api/customer/" + savedCustomer.getId().toString() + "/dashboards?",
new TypeReference<>() {}, new PageLink(100));
Assert.assertEquals(1, pageData.getData().size());
Assert.assertEquals("Edge Test Dashboard", pageData.getData().get(0).getTitle());
Assert.assertEquals(DASHBOARD_TITLE, pageData.getData().get(0).getTitle());
dashboard.setTitle(DASHBOARD_TITLE + " Updated");
dashboard.setAssignedCustomers(null);
dashboardUpdateMsgBuilder.setEntity(JacksonUtil.toString(dashboard));
dashboardUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE);
uplinkMsgBuilder = UplinkMsg.newBuilder();
uplinkMsgBuilder.addDashboardUpdateMsg(dashboardUpdateMsgBuilder.build());
edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses());
foundDashboard = doGet("/api/dashboard/" + dashboard.getUuidId(), Dashboard.class);
Assert.assertEquals(DASHBOARD_TITLE + " Updated", foundDashboard.getName());
}
@Test
@ -256,7 +273,7 @@ public class DashboardEdgeTest extends AbstractEdgeTest {
Dashboard dashboard = new Dashboard();
dashboard.setId(new DashboardId(UUID.randomUUID()));
dashboard.setTenantId(tenantId);
dashboard.setTitle("Edge Test Dashboard");
dashboard.setTitle(DASHBOARD_TITLE);
dashboard.setAssignedCustomers(Sets.newHashSet(new ShortCustomerInfo(savedCustomer.getId(), savedCustomer.getTitle(), savedCustomer.isPublic())));
return dashboard;
}

40
application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java

@ -126,6 +126,45 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest {
unAssignFromEdgeAndDeleteDashboard(thermostatsDashboardId);
}
@Test
public void testDeleteDeviceProfilesWhenEdgeIsOffline() throws Exception {
RuleChainId thermostatsRuleChainId = createEdgeRuleChainAndAssignToEdge("Thermostats Rule Chain");
// create device profile
DeviceProfile deviceProfile = this.createDeviceProfile("ONE_MORE_DEVICE_PROFILE", null);
deviceProfile.setDefaultEdgeRuleChainId(thermostatsRuleChainId);
extendDeviceProfileData(deviceProfile);
edgeImitator.expectMessageAmount(1);
deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof DeviceProfileUpdateMsg);
DeviceProfileUpdateMsg deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage;
DeviceProfile deviceProfileMsg = JacksonUtil.fromString(deviceProfileUpdateMsg.getEntity(), DeviceProfile.class, true);
Assert.assertNotNull(deviceProfileMsg);
Assert.assertEquals(deviceProfile, deviceProfileMsg);
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType());
// delete profile when edge is offline
edgeImitator.disconnect();
doDelete("/api/deviceProfile/" + deviceProfile.getUuidId())
.andExpect(status().isOk());
edgeImitator.connect();
// 27 sync message
// + 1 delete message
edgeImitator.expectMessageAmount(28);
Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof DeviceProfileUpdateMsg);
deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage;
Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType());
Assert.assertEquals(deviceProfile.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getIdMSB());
Assert.assertEquals(deviceProfile.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getIdLSB());
unAssignFromEdgeAndDeleteRuleChain(thermostatsRuleChainId);
}
@Test
public void testDeviceProfiles_snmp() throws Exception {
DeviceProfile deviceProfile = createDeviceProfileAndDoBasicAssert("SNMP", createSnmpDeviceProfileTransportConfiguration());
@ -449,4 +488,5 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest {
deviceProfile.setProfileData(createProfileData());
return deviceProfile;
}
}

3
common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java

@ -103,6 +103,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi
private final TbKafkaAdmin edqsRequestsAdmin;
private final AtomicLong consumerCount = new AtomicLong();
private final AtomicLong edgeConsumerCount = new AtomicLong();
public KafkaMonolithQueueFactory(TopicService topicService, TbKafkaSettings kafkaSettings,
TbServiceInfoProvider serviceInfoProvider,
@ -483,7 +484,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi
TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> consumerBuilder = TbKafkaConsumerTemplate.builder();
consumerBuilder.settings(kafkaSettings);
consumerBuilder.topic(topicService.buildTopicName("tb_edge_event.notifications." + tenantId + "." + edgeId));
consumerBuilder.clientId("monolith-to-edge-event-consumer" + serviceInfoProvider.getServiceId());
consumerBuilder.clientId("monolith-to-edge-event-consumer-" + serviceInfoProvider.getServiceId() + "-" + edgeConsumerCount.incrementAndGet());
consumerBuilder.groupId(topicService.buildTopicName("monolith-edge-event-consumer"));
consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeEventNotificationMsg.parseFrom(msg.getData()), msg.getHeaders()));
consumerBuilder.admin(edgeEventAdmin);

3
common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java

@ -102,6 +102,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory {
private final TbKafkaAdmin edqsRequestsAdmin;
private final AtomicLong consumerCount = new AtomicLong();
private final AtomicLong edgeConsumerCount = new AtomicLong();
public KafkaTbCoreQueueFactory(TopicService topicService,
TbKafkaSettings kafkaSettings,
@ -432,7 +433,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory {
TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> consumerBuilder = TbKafkaConsumerTemplate.builder();
consumerBuilder.settings(kafkaSettings);
consumerBuilder.topic(topicService.buildTopicName("tb_edge_event.notifications." + tenantId + "." + edgeId));
consumerBuilder.clientId("tb-core-edge-event-consumer" + serviceInfoProvider.getServiceId());
consumerBuilder.clientId("tb-core-edge-event-consumer-" + serviceInfoProvider.getServiceId() + "-" + edgeConsumerCount.incrementAndGet());
consumerBuilder.groupId(topicService.buildTopicName("tb-core-edge-event-consumer"));
consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeEventNotificationMsg.parseFrom(msg.getData()), msg.getHeaders()));
consumerBuilder.admin(edgeEventAdmin);

2
docker/docker-compose.prometheus-grafana.yml

@ -23,7 +23,7 @@ volumes:
services:
prometheus:
image: prom/prometheus:v2.1.0
image: prom/prometheus:v3.1.0
volumes:
- ./monitoring/prometheus/:/etc/prometheus/
- prometheus_data:/prometheus

2
ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts

@ -103,7 +103,7 @@ export class DeviceProfileTransportConfigurationComponent implements ControlValu
delete configuration.type;
}
setTimeout(() => {
this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration});
this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false});
}, 0);
}

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

@ -146,6 +146,9 @@ import { ScadaSymbolBasicConfigComponent } from '@home/components/widget/config/
import {
SegmentedButtonBasicConfigComponent
} from '@home/components/widget/config/basic/button/segmented-button-basic-config.component';
import {
ValueStepperBasicConfigComponent
} from '@home/components/widget/config/basic/rpc/value-stepper-basic-config.component';
@NgModule({
declarations: [
@ -182,6 +185,7 @@ import {
PowerButtonBasicConfigComponent,
SliderBasicConfigComponent,
ToggleButtonBasicConfigComponent,
ValueStepperBasicConfigComponent,
TimeSeriesChartBasicConfigComponent,
ComparisonKeyRowComponent,
ComparisonKeysTableComponent,
@ -236,6 +240,7 @@ import {
PowerButtonBasicConfigComponent,
SliderBasicConfigComponent,
ToggleButtonBasicConfigComponent,
ValueStepperBasicConfigComponent,
TimeSeriesChartBasicConfigComponent,
StatusWidgetBasicConfigComponent,
PieChartBasicConfigComponent,

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

@ -116,6 +116,38 @@
</tb-color-input>
</div>
</div>
<div class="tb-form-row column-xs" formGroupName="onButtonIcon">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon">
{{ 'widgets.power-button.button-icon-on' | translate }}
</mat-slide-toggle>
<div class="flex flex-1 flex-row items-center justify-start gap-2">
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select class="flex-1" formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
[color]="powerButtonWidgetConfigForm.get('mainColorOn').value"
formControlName="icon">
</tb-material-icon-select>
</div>
</div>
<div class="tb-form-row column-xs" formGroupName="offButtonIcon">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon">
{{ 'widgets.power-button.button-icon-off' | translate }}
</mat-slide-toggle>
<div class="flex flex-1 flex-row items-center justify-start gap-2">
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select class="flex-1" formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
[color]="powerButtonWidgetConfigForm.get('mainColorOff').value"
formControlName="icon">
</tb-material-icon-select>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.power-button.power-on-colors' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-3">

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

@ -87,6 +87,19 @@ export class PowerButtonBasicConfigComponent extends BasicWidgetConfigComponent
icon: [configData.config.titleIcon, []],
iconColor: [configData.config.iconColor, []],
onButtonIcon: this.fb.group({
showIcon: [settings.onButtonIcon.showIcon, []],
iconSize: [settings.onButtonIcon.iconSize, [Validators.min(0)]],
iconSizeUnit: [settings.onButtonIcon.iconSizeUnit, []],
icon: [settings.onButtonIcon.icon, []],
}),
offButtonIcon: this.fb.group({
showIcon: [settings.offButtonIcon.showIcon, []],
iconSize: [settings.offButtonIcon.iconSize, [Validators.min(0)]],
iconSizeUnit: [settings.offButtonIcon.iconSizeUnit, []],
icon: [settings.offButtonIcon.icon, []],
}),
mainColorOn: [settings.mainColorOn, []],
backgroundColorOn: [settings.backgroundColorOn, []],
@ -128,6 +141,9 @@ export class PowerButtonBasicConfigComponent extends BasicWidgetConfigComponent
this.widgetConfig.config.settings.layout = config.layout;
this.widgetConfig.config.settings.onButtonIcon = config.onButtonIcon;
this.widgetConfig.config.settings.offButtonIcon = config.offButtonIcon;
this.widgetConfig.config.settings.mainColorOn = config.mainColorOn;
this.widgetConfig.config.settings.backgroundColorOn = config.backgroundColorOn;
@ -148,12 +164,14 @@ export class PowerButtonBasicConfigComponent extends BasicWidgetConfigComponent
}
protected validatorTriggers(): string[] {
return ['showTitle', 'showIcon'];
return ['showTitle', 'showIcon', 'onButtonIcon.showIcon', 'offButtonIcon.showIcon'];
}
protected updateValidators(emitEvent: boolean, trigger?: string) {
const showTitle: boolean = this.powerButtonWidgetConfigForm.get('showTitle').value;
const showIcon: boolean = this.powerButtonWidgetConfigForm.get('showIcon').value;
const onButtonIcon: boolean = this.powerButtonWidgetConfigForm.get('onButtonIcon').get('showIcon').value;
const offButtonIcon: boolean = this.powerButtonWidgetConfigForm.get('offButtonIcon').get('showIcon').value;
if (showTitle) {
this.powerButtonWidgetConfigForm.get('title').enable();
this.powerButtonWidgetConfigForm.get('titleFont').enable();
@ -180,6 +198,24 @@ export class PowerButtonBasicConfigComponent extends BasicWidgetConfigComponent
this.powerButtonWidgetConfigForm.get('icon').disable();
this.powerButtonWidgetConfigForm.get('iconColor').disable();
}
if (onButtonIcon) {
this.powerButtonWidgetConfigForm.get('onButtonIcon').get('iconSize').enable();
this.powerButtonWidgetConfigForm.get('onButtonIcon').get('iconSizeUnit').enable();
this.powerButtonWidgetConfigForm.get('onButtonIcon').get('icon').enable();
} else {
this.powerButtonWidgetConfigForm.get('onButtonIcon').get('iconSize').disable();
this.powerButtonWidgetConfigForm.get('onButtonIcon').get('iconSizeUnit').disable();
this.powerButtonWidgetConfigForm.get('onButtonIcon').get('icon').disable();
}
if (offButtonIcon) {
this.powerButtonWidgetConfigForm.get('offButtonIcon').get('iconSize').enable();
this.powerButtonWidgetConfigForm.get('offButtonIcon').get('iconSizeUnit').enable();
this.powerButtonWidgetConfigForm.get('offButtonIcon').get('icon').enable();
} else {
this.powerButtonWidgetConfigForm.get('offButtonIcon').get('iconSize').disable();
this.powerButtonWidgetConfigForm.get('offButtonIcon').get('iconSizeUnit').disable();
this.powerButtonWidgetConfigForm.get('offButtonIcon').get('icon').disable();
}
}
private getCardButtons(config: WidgetConfig): string[] {

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

@ -63,7 +63,7 @@
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between flex-1 flex-col">
<div class="tb-form-row space-between flex-1 !flex-col">
<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>
<tb-toggle-select formControlName="tankSelectionType">

297
ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.html

@ -0,0 +1,297 @@
<!--
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]="valueStepperWidgetConfigForm">
<tb-target-device formControlName="targetDevice"></tb-target-device>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.value-stepper.behavior</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-stepper.initial-state-hint' | translate}}" translate>widgets.value-stepper.initial-state</div>
<tb-get-value-action-settings class="flex-1"
panelTitle="{{ 'widgets.value-stepper.initial-state' | translate }}"
[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.value-stepper.left-button-click-hint' | translate}}" translate>widgets.value-stepper.left-button-click</div>
<tb-set-value-action-settings class="flex-1"
panelTitle="{{ 'widgets.value-stepper.left-button-click' | translate }}"
[valueType]="valueType.DOUBLE"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="leftButtonClick"></tb-set-value-action-settings>
</div>
<div class="tb-form-row space-between">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-stepper.right-button-click-hint' | translate}}" translate>widgets.value-stepper.right-button-click</div>
<tb-set-value-action-settings class="flex-1"
panelTitle="{{ 'widgets.value-stepper.right-button-click' | translate }}"
[valueType]="valueType.DOUBLE"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="rightButtonClick"></tb-set-value-action-settings>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.button-state.disabled-state-hint' | translate}}" translate>widgets.button-state.disabled-state</div>
<tb-get-value-action-settings class="flex-1"
panelTitle="{{ 'widgets.button-state.disabled-state' | translate }}"
[valueType]="valueType.BOOLEAN"
stateLabel="{{ 'widgets.button-state.disabled' | translate }}"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="disabledState"></tb-get-value-action-settings>
</div>
</div>
<div class="tb-form-panel" formGroupName="appearance">
<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.button.layout' | translate }}" formControlName="type">
<tb-image-cards-select-option *ngFor="let type of valueStepperTypes"
[value]="type"
[image]="valueStepperTypeImageMap.get(type)">
{{ valueStepperTypeTranslationMap.get(type) | translate }}
</tb-image-cards-select-option>
</tb-image-cards-select>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="autoScale">
{{ 'widgets.value-stepper.auto-scale' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.value-stepper.value-range' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-2">
<div class="tb-small-label" translate>widgets.value-stepper.min-range</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="minValueRange" type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<div class="tb-small-label" translate>widgets.value-stepper.max-range</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="maxValueRange" type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.value-stepper.value-increment-decrement-step' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="valueStep" type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showValueBox">
{{ 'widgets.value-stepper.value' | translate }}
</mat-slide-toggle>
<div class="flex flex-1 flex-row items-center justify-start gap-2">
<tb-unit-input class="flex" formControlName="valueUnits"></tb-unit-input>
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput formControlName="valueDecimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix class="lt-md:!hidden" 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">
<div>{{ 'widgets.value-stepper.value-box-background' | translate }}</div>
<tb-color-input asBoxInput
colorClearButton
formControlName="valueBoxBackground">
</tb-color-input>
</div>
<div class="tb-form-row space-between column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showBorder">
{{ 'widgets.value-stepper.border' | translate }}
</mat-slide-toggle>
<div class="flex flex-1 flex-row items-center justify-end gap-2">
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="borderWidth" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix class="lt-md:!hidden">px</div>
</mat-form-field>
<tb-color-input asBoxInput
colorClearButton
formControlName="borderColor">
</tb-color-input>
</div>
</div>
</div>
<div class="tb-form-panel" formGroupName="buttonAppearance">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title" translate>widgets.value-stepper.button-appearance</div>
<tb-toggle-select [(ngModel)]="buttonAppearanceType" [ngModelOptions]="{standalone: true}">
<tb-toggle-option value="left">{{ 'widgets.value-stepper.left' | translate }}</tb-toggle-option>
<tb-toggle-option value="right">{{ 'widgets.value-stepper.right' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="tb-form-panel no-border no-padding" formGroupName="leftButton" [class.!hidden]="buttonAppearanceType !== 'left'">
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showButton">
{{ 'widgets.value-stepper.left-button' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<div>{{ 'widgets.value-stepper.icon' | translate }}</div>
<div class="flex flex-1 flex-row items-center justify-start gap-2">
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select class="flex-1" formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
formControlName="icon">
</tb-material-icon-select>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.value-stepper.button-on-colors' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-3">
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.value-stepper.main</div>
<tb-color-input asBoxInput
formControlName="mainColorOn">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.value-stepper.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.value-stepper.disabled-colors' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-3">
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.value-stepper.main</div>
<tb-color-input asBoxInput
formControlName="mainColorDisabled">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.value-stepper.background</div>
<tb-color-input asBoxInput
formControlName="backgroundColorDisabled">
</tb-color-input>
</div>
</div>
</div>
</div>
<div class="tb-form-panel no-border no-padding" formGroupName="rightButton" [class.!hidden]="buttonAppearanceType !== 'right'">
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showButton">
{{ 'widgets.value-stepper.right-button' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<div>{{ 'widgets.value-stepper.icon' | translate }}</div>
<div class="flex flex-1 flex-row items-center justify-start gap-2">
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select class="flex-1" formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
formControlName="icon">
</tb-material-icon-select>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.value-stepper.button-on-colors' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-3">
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.value-stepper.main</div>
<tb-color-input asBoxInput
formControlName="mainColorOn">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.value-stepper.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.value-stepper.disabled-colors' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-3">
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.value-stepper.main</div>
<tb-color-input asBoxInput
formControlName="mainColorDisabled">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.value-stepper.background</div>
<tb-color-input asBoxInput
formControlName="backgroundColorDisabled">
</tb-color-input>
</div>
</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 class="tb-form-row space-between">
<div>{{ 'widget-config.card-padding' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="padding" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<tb-widget-actions-panel
formControlName="actions">
</tb-widget-actions-panel>
</ng-container>

225
ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.ts

@ -0,0 +1,225 @@
///
/// 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 {
valueStepperDefaultSettings,
valueStepperTypeImages,
valueStepperTypes,
valueStepperTypeTranslations,
ValueStepperWidgetSettings
} from '@home/components/widget/lib/rpc/value-stepper-widget.models';
type ButtonAppearanceType = 'left' | 'right';
@Component({
selector: 'tb-value-stepper-basic-config',
templateUrl: './value-stepper-basic-config.component.html',
styleUrls: ['../basic-config.scss']
})
export class ValueStepperBasicConfigComponent extends BasicWidgetConfigComponent {
get targetDevice(): TargetDevice {
return this.valueStepperWidgetConfigForm.get('targetDevice').value;
}
valueStepperTypeTranslationMap = valueStepperTypeTranslations;
valueStepperTypes = valueStepperTypes;
valueStepperTypeImageMap = valueStepperTypeImages;
buttonAppearanceType: ButtonAppearanceType = 'left';
valueType = ValueType;
valueStepperWidgetConfigForm: 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.valueStepperWidgetConfigForm;
}
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: ValueStepperWidgetSettings = {...valueStepperDefaultSettings, ...(configData.config.settings || {})};
this.valueStepperWidgetConfigForm = this.fb.group({
targetDevice: [configData.config.targetDevice, []],
initialState: [settings.initialState, []],
leftButtonClick: [settings.leftButtonClick, []],
rightButtonClick: [settings.rightButtonClick, []],
disabledState: [settings.disabledState, []],
appearance: this.fb.group({
type: [settings.appearance.type, []],
autoScale: [settings.appearance.autoScale, []],
minValueRange: [settings.appearance.minValueRange, []],
maxValueRange: [settings.appearance.maxValueRange, []],
valueStep: [settings.appearance.valueStep, [Validators.min(0)]],
showValueBox: [settings.appearance.showValueBox, []],
valueUnits: [settings.appearance.valueUnits, []],
valueDecimals: [settings.appearance.valueDecimals, []],
valueFont: [settings.appearance.valueFont, []],
valueColor: [settings.appearance.valueColor],
valueBoxBackground: [settings.appearance.valueBoxBackground, []],
showBorder: [settings.appearance.showBorder, []],
borderWidth: [settings.appearance.borderWidth, []],
borderColor: [settings.appearance.borderColor, []]
}),
buttonAppearance: this.fb.group({
leftButton: this.fb.group({
showButton: [settings.buttonAppearance.leftButton.showButton],
icon: [settings.buttonAppearance.leftButton.icon],
iconSize: [settings.buttonAppearance.leftButton.iconSize],
iconSizeUnit: [settings.buttonAppearance.leftButton.iconSizeUnit],
mainColorOn: [settings.buttonAppearance.leftButton.mainColorOn, []],
backgroundColorOn: [settings.buttonAppearance.leftButton.backgroundColorOn, []],
mainColorDisabled: [settings.buttonAppearance.leftButton.mainColorDisabled, []],
backgroundColorDisabled: [settings.buttonAppearance.leftButton.backgroundColorDisabled, []]
}),
rightButton: this.fb.group({
showButton: [settings.buttonAppearance.rightButton.showButton],
icon: [settings.buttonAppearance.rightButton.icon],
iconSize: [settings.buttonAppearance.rightButton.iconSize],
iconSizeUnit: [settings.buttonAppearance.rightButton.iconSizeUnit],
mainColorOn: [settings.buttonAppearance.rightButton.mainColorOn, []],
backgroundColorOn: [settings.buttonAppearance.rightButton.backgroundColorOn, []],
mainColorDisabled: [settings.buttonAppearance.rightButton.mainColorDisabled, []],
backgroundColorDisabled: [settings.buttonAppearance.rightButton.backgroundColorDisabled, []]
})
}),
background: [settings.background, []],
cardButtons: [this.getCardButtons(configData.config), []],
borderRadius: [configData.config.borderRadius, []],
padding: [settings.padding, []],
actions: [configData.config.actions || {}, []]
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.targetDevice = config.targetDevice;
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.initialState = config.initialState;
this.widgetConfig.config.settings.disabledState = config.disabledState;
this.widgetConfig.config.settings.leftButtonClick = config.leftButtonClick;
this.widgetConfig.config.settings.rightButtonClick = config.rightButtonClick;
this.widgetConfig.config.settings.appearance = config.appearance;
this.widgetConfig.config.settings.buttonAppearance = config.buttonAppearance;
this.widgetConfig.config.settings.background = config.background;
this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.borderRadius = config.borderRadius;
this.widgetConfig.config.settings.padding = config.padding;
this.widgetConfig.config.actions = config.actions;
return this.widgetConfig;
}
protected validatorTriggers(): string[] {
return ['appearance.showValueBox', 'appearance.showBorder',
'buttonAppearance.leftButton.showButton', 'buttonAppearance.rightButton.showButton'];
}
protected updateValidators(emitEvent: boolean, trigger?: string) {
const showValueBox: boolean = this.valueStepperWidgetConfigForm.get('appearance').get('showValueBox').value;
const showBorder: boolean = this.valueStepperWidgetConfigForm.get('appearance').get('showBorder').value;
const showLeftButton: boolean = this.valueStepperWidgetConfigForm.get('buttonAppearance').get('leftButton').get('showButton').value;
const showRightButton: boolean = this.valueStepperWidgetConfigForm.get('buttonAppearance').get('rightButton').get('showButton').value;
if (showValueBox) {
this.valueStepperWidgetConfigForm.get('appearance').get('valueUnits').enable();
this.valueStepperWidgetConfigForm.get('appearance').get('valueDecimals').enable();
this.valueStepperWidgetConfigForm.get('appearance').get('valueFont').enable();
this.valueStepperWidgetConfigForm.get('appearance').get('valueColor').enable();
this.valueStepperWidgetConfigForm.get('appearance').get('valueBoxBackground').enable();
this.valueStepperWidgetConfigForm.get('appearance').get('showBorder').enable({emitEvent: false});
if (showBorder) {
this.valueStepperWidgetConfigForm.get('appearance').get('borderWidth').enable();
this.valueStepperWidgetConfigForm.get('appearance').get('borderColor').enable();
} else {
this.valueStepperWidgetConfigForm.get('appearance').get('borderWidth').disable();
this.valueStepperWidgetConfigForm.get('appearance').get('borderColor').disable();
}
} else {
this.valueStepperWidgetConfigForm.get('appearance').get('valueUnits').disable();
this.valueStepperWidgetConfigForm.get('appearance').get('valueDecimals').disable();
this.valueStepperWidgetConfigForm.get('appearance').get('valueFont').disable();
this.valueStepperWidgetConfigForm.get('appearance').get('valueColor').disable();
this.valueStepperWidgetConfigForm.get('appearance').get('valueBoxBackground').disable();
this.valueStepperWidgetConfigForm.get('appearance').get('showBorder').disable({emitEvent: false});
this.valueStepperWidgetConfigForm.get('appearance').get('borderWidth').disable();
this.valueStepperWidgetConfigForm.get('appearance').get('borderColor').disable();
}
this.buttonValidators(showLeftButton, 'leftButton');
this.buttonValidators(showRightButton, 'rightButton');
}
private buttonValidators(showButtonValue: boolean, button: string) {
if (showButtonValue) {
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('icon').enable()
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('iconSize').enable()
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('iconSizeUnit').enable()
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('mainColorOn').enable()
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('backgroundColorOn').enable()
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('mainColorDisabled').enable()
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('backgroundColorDisabled').enable()
} else {
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('icon').disable()
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('iconSize').disable()
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('iconSizeUnit').disable()
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('mainColorOn').disable()
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('backgroundColorOn').disable()
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('mainColorDisabled').disable()
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('backgroundColorDisabled').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.valueStepperWidgetConfigForm.get('appearance').get('valueUnits').value;
const decimals: number = this.valueStepperWidgetConfigForm.get('appearance').get('valueDecimals').value;
return formatValue(48, decimals, units, false);
}
}

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

@ -18,7 +18,8 @@ import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef, NgZone,
ElementRef,
NgZone,
OnDestroy,
OnInit,
Renderer2,
@ -38,6 +39,7 @@ import {
PowerButtonWidgetSettings
} from '@home/components/widget/lib/rpc/power-button-widget.models';
import { SVG, Svg } from '@svgdotjs/svg.js';
import { MatIconRegistry } from '@angular/material/icon';
@Component({
selector: 'tb-power-button-widget',
@ -72,6 +74,7 @@ export class PowerButtonWidgetComponent extends
constructor(protected imagePipe: ImagePipe,
protected sanitizer: DomSanitizer,
private renderer: Renderer2,
private iconRegistry: MatIconRegistry,
protected cd: ChangeDetectorRef,
protected zone: NgZone) {
super(cd);
@ -180,7 +183,7 @@ export class PowerButtonWidgetComponent extends
this.renderer.setStyle(this.svgShape.node, 'user-select', 'none');
this.zone.run(() => {
this.powerButtonSvgShape = PowerButtonShape.fromSettings(this.ctx, this.svgShape,
this.powerButtonSvgShape = PowerButtonShape.fromSettings(this.ctx, this.svgShape, this.iconRegistry,
this.settings, this.value, this.disabledState, () => this.onClick());
});

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

@ -28,6 +28,11 @@ import { Circle, Effect, Element, G, Gradient, Path, Runner, Svg, Text, Timeline
import '@svgdotjs/svg.filter.js';
import tinycolor from 'tinycolor2';
import { WidgetContext } from '@home/models/widget-component.models';
import { Observable, of } from 'rxjs';
import { isSvgIcon, splitIconName } from '@shared/models/icon.models';
import { catchError, map, take } from 'rxjs/operators';
import { MatIconRegistry } from '@angular/material/icon';
import { isDefinedAndNotNull } from '@core/utils';
export enum PowerButtonLayout {
default = 'default',
@ -71,20 +76,29 @@ export const powerButtonLayoutImages = new Map<PowerButtonLayout, string>(
]
);
export interface ButtonIconSettings {
showIcon: boolean;
iconSize: number;
iconSizeUnit: string;
icon: string;
}
export interface PowerButtonWidgetSettings {
initialState: GetValueSettings<boolean>;
disabledState: GetValueSettings<boolean>;
onUpdateState: SetValueSettings;
offUpdateState: SetValueSettings;
initialState?: GetValueSettings<boolean>;
disabledState?: GetValueSettings<boolean>;
onUpdateState?: SetValueSettings;
offUpdateState?: SetValueSettings;
layout: PowerButtonLayout;
onButtonIcon: ButtonIconSettings,
offButtonIcon: ButtonIconSettings,
mainColorOn: string;
backgroundColorOn: string;
mainColorOff: string;
backgroundColorOff: string;
mainColorDisabled: string;
backgroundColorDisabled: string;
background: BackgroundSettings;
padding: string;
background?: BackgroundSettings;
padding?: string;
}
export const powerButtonDefaultSettings: PowerButtonWidgetSettings = {
@ -177,6 +191,18 @@ export const powerButtonDefaultSettings: PowerButtonWidgetSettings = {
}
},
layout: PowerButtonLayout.default,
onButtonIcon: {
showIcon: false,
iconSize: 32,
iconSizeUnit: 'px',
icon: 'power_settings_new'
},
offButtonIcon: {
showIcon: false,
iconSize: 32,
iconSizeUnit: 'px',
icon: 'power_settings_new'
},
mainColorOn: '#3F52DD',
backgroundColorOn: '#FFFFFF',
mainColorOff: '#A2A2A2',
@ -207,6 +233,11 @@ interface PowerButtonColorState {
backgroundColor: PowerButtonColor;
}
interface ButtonsIconSettings {
onButtonIcon: ButtonIconSettings;
offButtonIcon: ButtonIconSettings;
}
type PowerButtonShapeColors = Record<PowerButtonState, PowerButtonColorState>;
const createPowerButtonShapeColors = (settings: PowerButtonWidgetSettings): PowerButtonShapeColors => {
@ -257,33 +288,35 @@ export abstract class PowerButtonShape {
static fromSettings(ctx: WidgetContext,
svgShape: Svg,
iconRegistry: MatIconRegistry,
settings: PowerButtonWidgetSettings,
value: boolean,
disabled: boolean,
onClick: () => void): PowerButtonShape {
switch (settings.layout) {
case PowerButtonLayout.default:
return new DefaultPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
return new DefaultPowerButtonShape(ctx, svgShape, iconRegistry, settings, value, disabled, onClick);
case PowerButtonLayout.simplified:
return new SimplifiedPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
return new SimplifiedPowerButtonShape(ctx, svgShape, iconRegistry, settings, value, disabled, onClick);
case PowerButtonLayout.outlined:
return new OutlinedPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
return new OutlinedPowerButtonShape(ctx, svgShape, iconRegistry, settings, value, disabled, onClick);
case PowerButtonLayout.default_volume:
return new DefaultVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
return new DefaultVolumePowerButtonShape(ctx, svgShape, iconRegistry, settings, value, disabled, onClick);
case PowerButtonLayout.simplified_volume:
return new SimplifiedVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
return new SimplifiedVolumePowerButtonShape(ctx, svgShape, iconRegistry, settings, value, disabled, onClick);
case PowerButtonLayout.outlined_volume:
return new OutlinedVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
return new OutlinedVolumePowerButtonShape(ctx, svgShape, iconRegistry, settings, value, disabled, onClick);
case PowerButtonLayout.default_icon:
return new DefaultIconPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
return new DefaultIconPowerButtonShape(ctx, svgShape, iconRegistry, settings, value, disabled, onClick);
case PowerButtonLayout.simplified_icon:
return new SimplifiedIconPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
return new SimplifiedIconPowerButtonShape(ctx, svgShape, iconRegistry, settings, value, disabled, onClick);
case PowerButtonLayout.outlined_icon:
return new OutlinedIconPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
return new OutlinedIconPowerButtonShape(ctx, svgShape, iconRegistry, settings, value, disabled, onClick);
}
}
protected readonly colors: PowerButtonShapeColors;
protected readonly icons: ButtonsIconSettings;
protected readonly onLabel: string;
protected readonly offLabel: string;
@ -293,18 +326,67 @@ export abstract class PowerButtonShape {
protected pressed = false;
protected forcePressed = false;
protected offPowerSymbolIcon: Element;
protected onPowerSymbolIcon: Element;
protected offLabelShape: Text;
protected onLabelShape: Text;
protected offPowerSymbolCircle: Path;
protected offPowerSymbolLine: Path;
protected onPowerSymbolCircle: Path;
protected onPowerSymbolLine: Path;
protected constructor(protected widgetContext: WidgetContext,
protected svgShape: Svg,
protected iconRegistry: MatIconRegistry,
protected settings: PowerButtonWidgetSettings,
protected value: boolean,
protected disabled: boolean,
protected onClick: () => void) {
this.colors = createPowerButtonShapeColors(this.settings);
this.icons = {onButtonIcon: this.settings.onButtonIcon, offButtonIcon: this.settings.offButtonIcon};
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 createIconElement(icon: string, size: number): Observable<Element> {
const isSvg = isSvgIcon(icon);
if (isSvg) {
const [namespace, iconName] = splitIconName(icon);
return this.iconRegistry
.getNamedSvgIcon(iconName, namespace)
.pipe(
take(1),
map((svgElement) => {
const element = new Element(svgElement.firstChild);
const box = element.bbox();
const scale = size / box.height;
element.scale(scale);
return element;
}),
catchError(() => of(null)
));
} else {
const iconName = splitIconName(icon)[1];
const textElement = this.svgShape.text(iconName);
const fontSetClasses = (
this.iconRegistry.getDefaultFontSetClass()
).filter(className => className.length > 0);
fontSetClasses.forEach(className => textElement.addClass(className));
textElement.font({size: `${size}px`});
textElement.attr({
style: `font-size: ${size}px`,
'text-anchor': 'start'
});
const tspan = textElement.first();
tspan.attr({
'dominant-baseline': 'hanging'
});
return of(textElement);
}
}
public setValue(value: boolean) {
if (this.value !== value) {
this.value = value;
@ -330,6 +412,106 @@ export abstract class PowerButtonShape {
}
}
public drawOffShape(centerGroup: G, label: boolean, labelWeight?: string, circleStroke?: boolean) {
if (this.icons.offButtonIcon.showIcon) {
this.createIconElement(this.icons.offButtonIcon.icon, this.icons.offButtonIcon.iconSize).subscribe(icon =>
this.offPowerSymbolIcon = icon.center(cx, cy).addTo(centerGroup));
} else {
if (label) {
this.offLabelShape = this.createOffLabel(labelWeight).addTo(centerGroup);
} else {
this.offPowerSymbolCircle = this.svgShape.path(circleStroke ? powerCircle : powerCircleStroke).center(cx, cy).addTo(centerGroup);
this.offPowerSymbolLine = this.svgShape.path(circleStroke ? powerLine : powerLineStroke).center(cx, cy-12).addTo(centerGroup);
}
}
}
public drawOnShape(onCenterGroup?: G, label?: boolean, labelWeight?: string, circleStroke?: boolean, mask?: Circle) {
if (this.icons.onButtonIcon.showIcon) {
this.createIconElement(this.icons.onButtonIcon.icon, this.icons.onButtonIcon.iconSize).subscribe(icon => {
this.onPowerSymbolIcon = icon.center(cx, cy);
if (isDefinedAndNotNull(onCenterGroup)) {
this.onPowerSymbolIcon.addTo(onCenterGroup);
}
if (isDefinedAndNotNull(mask)) {
this.createMask(mask, [this.onPowerSymbolIcon]);
}
});
} else {
if (label) {
this.onLabelShape = this.createOnLabel(labelWeight);
if (isDefinedAndNotNull(onCenterGroup)) {
this.onLabelShape.addTo(onCenterGroup);
}
if (isDefinedAndNotNull(mask)) {
this.createMask(mask, [this.onLabelShape]);
}
} else {
this.onPowerSymbolCircle = this.svgShape.path(circleStroke ? powerCircle : powerCircleStroke).center(cx, cy);
this.onPowerSymbolLine = this.svgShape.path(circleStroke ? powerLine : powerLineStroke).center(cx, cy-12);
if (isDefinedAndNotNull(onCenterGroup)) {
this.onPowerSymbolCircle.addTo(onCenterGroup);
this.onPowerSymbolLine.addTo(onCenterGroup);
}
if (isDefinedAndNotNull(mask)) {
this.createMask(mask, [this.onPowerSymbolCircle, this.onPowerSymbolLine]);
}
}
}
}
public onCenterTimeLine(timeline: Timeline, label: boolean) {
if (this.icons.onButtonIcon.showIcon) {
this.onPowerSymbolIcon.timeline(timeline);
} else {
if (label) {
this.onLabelShape.timeline(timeline);
} else {
this.onPowerSymbolCircle.timeline(timeline);
this.onPowerSymbolLine.timeline(timeline);
}
}
}
public offCenterColor(mainColor: PowerButtonColor, label: boolean) {
if (this.icons.offButtonIcon.showIcon) {
this.offPowerSymbolIcon.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
} else {
if (label) {
this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
} else {
this.offPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
}
}
}
public onCenterColor(mainColor: PowerButtonColor, label: boolean) {
if (this.icons.onButtonIcon.showIcon) {
this.onPowerSymbolIcon.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
} else {
if (label) {
this.onLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
} else {
this.onPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.onPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
}
}
}
public buttonAnimation(scale: number, label: boolean) {
if (this.icons.onButtonIcon.showIcon) {
powerButtonAnimation(this.onPowerSymbolIcon).transform({scale});
} else {
if (label) {
powerButtonAnimation(this.onLabelShape).transform({scale, origin: {x: cx, y: cy}});
} else {
powerButtonAnimation(this.onPowerSymbolCircle).transform({scale, origin: {x: cx, y: cy}});
powerButtonAnimation(this.onPowerSymbolLine).transform({scale, origin: {x: cx, y: cy}});
}
}
}
private _drawShape() {
this.backgroundShape = this.svgShape.circle(powerButtonShapeSize).center(cx, cy)
@ -530,9 +712,7 @@ 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;
@ -543,22 +723,20 @@ class DefaultPowerButtonShape extends PowerButtonShape {
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.drawOffShape(this.centerGroup, true);
this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 20).center(cx, cy);
this.drawOnShape(null, true, '', false, this.onCircleShape);
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.onCenterTimeLine(this.pressedTimeline, true);
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.offCenterColor(mainColor, true);
this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
}
@ -578,14 +756,14 @@ class DefaultPowerButtonShape extends PowerButtonShape {
this.pressedTimeline.finish();
const pressedScale = 0.75;
powerButtonAnimation(this.centerGroup).transform({scale: pressedScale});
powerButtonAnimation(this.onLabelShape).transform({scale: pressedScale, origin: {x: cx, y: cy}});
this.buttonAnimation(pressedScale, true);
this.pressedShadow.animate(6, 0.6);
}
protected onPressEnd() {
this.pressedTimeline.finish();
powerButtonAnimation(this.centerGroup).transform({scale: 1});
powerButtonAnimation(this.onLabelShape).transform({scale: 1, origin: {x: cx, y: cy}});
this.buttonAnimation(1, true);
this.pressedShadow.animateRestore();
}
@ -596,8 +774,6 @@ 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;
@ -608,22 +784,21 @@ class SimplifiedPowerButtonShape extends PowerButtonShape {
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.drawOffShape(this.centerGroup, true);
this.onCircleShape = this.svgShape.circle(powerButtonShapeSize).center(cx, cy);
this.onLabelShape = this.createOnLabel();
this.createMask(this.onCircleShape, [this.onLabelShape]);
this.drawOnShape(null, true, '', false, this.onCircleShape)
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.onCenterTimeLine(this.pressedTimeline, true);
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});
this.offCenterColor(mainColor, true);
}
protected drawOff() {
@ -642,14 +817,14 @@ class SimplifiedPowerButtonShape extends PowerButtonShape {
this.pressedTimeline.finish();
const pressedScale = 0.75;
powerButtonAnimation(this.centerGroup).transform({scale: pressedScale});
powerButtonAnimation(this.onLabelShape).transform({scale: pressedScale, origin: {x: cx, y: cy}});
this.buttonAnimation(pressedScale, true);
this.pressedShadow.animate(6, 0.6);
}
protected onPressEnd() {
this.pressedTimeline.finish();
powerButtonAnimation(this.centerGroup).transform({scale: 1});
powerButtonAnimation(this.onLabelShape).transform({scale: 1, origin: {x: cx, y: cy}});
this.buttonAnimation(1, true);
this.pressedShadow.animateRestore();
}
}
@ -659,9 +834,7 @@ class OutlinedPowerButtonShape extends PowerButtonShape {
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;
@ -677,25 +850,23 @@ class OutlinedPowerButtonShape extends PowerButtonShape {
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.drawOffShape(this.centerGroup, true);
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.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 28).center(cx, cy).addTo(this.onCenterGroup);
this.drawOnShape(null, true, '', false, this.onCircleShape)
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.onCenterTimeLine(this.pressedTimeline, true);
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.offCenterColor(mainColor, true);
this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
}
@ -714,7 +885,7 @@ class OutlinedPowerButtonShape extends PowerButtonShape {
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, origin: {x: cx, y: cy}});
this.buttonAnimation(pressedScale / 0.98, true);
this.pressedShadow.animate(6, 0.6);
}
@ -722,7 +893,7 @@ class OutlinedPowerButtonShape extends PowerButtonShape {
this.pressedTimeline.finish();
powerButtonAnimation(this.centerGroup).transform({scale: 1});
powerButtonAnimation(this.onCenterGroup).transform({scale: 1});
powerButtonAnimation(this.onLabelShape).transform({scale: 1, origin: {x: cx, y: cy}});
this.buttonAnimation(1, true);
this.pressedShadow.animateRestore();
}
}
@ -737,35 +908,29 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape {
private innerShadow: InnerShadowCircle;
//private innerShadowGradient: Gradient;
//private innerShadowGradientStop: Stop;
private offLabelShape: Text;
private onCircleShape: Circle;
private onLabelShape: Text;
protected onCircleShape: Circle;
private pressedTimeline: Timeline;
private centerGroup: G;
protected drawOffCenter(centerGroup: G) {
this.offLabelShape = this.createOffLabel('400').addTo(centerGroup);
this.drawOffShape(centerGroup, true, '400');
}
protected drawOnCenter() {
this.onLabelShape = this.createOnLabel('400');
}
protected addOnCenterToMask(onCircleShape: Circle) {
this.createMask(onCircleShape,[this.onLabelShape]);
this.drawOnShape(null, true, '400', false, this.onCircleShape);
}
protected addOnCenterTimeLine(pressedTimeline: Timeline) {
this.onLabelShape.timeline(pressedTimeline);
this.onCenterTimeLine(pressedTimeline, true);
}
protected drawOffCenterColor(mainColor: PowerButtonColor) {
this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offCenterColor(mainColor, true);
}
protected onCenterAnimation(scale: number) {
powerButtonAnimation(this.onLabelShape).transform({scale, origin: {x: cx, y: cy}});
this.buttonAnimation(scale, true);
}
protected drawShape() {
@ -789,7 +954,6 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape {
this.drawOffCenter(this.centerGroup);
this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 24).center(cx, cy);
this.drawOnCenter();
this.addOnCenterToMask(this.onCircleShape);
this.innerShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 24, cx, cy, 3, 0.3);
this.pressedTimeline = new Timeline();
@ -857,38 +1021,25 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape {
}
class DefaultIconPowerButtonShape extends DefaultVolumePowerButtonShape {
private offPowerSymbolCircle: Path;
private offPowerSymbolLine: Path;
private onPowerSymbolCircle: Path;
private onPowerSymbolLine: Path;
protected drawOffCenter(centerGroup: G) {
this.offPowerSymbolCircle = this.svgShape.path(powerCircle).center(cx, cy).addTo(centerGroup);
this.offPowerSymbolLine = this.svgShape.path(powerLine).center(cx, cy-12).addTo(centerGroup);
this.drawOffShape(centerGroup, false);
}
protected drawOnCenter() {
this.onPowerSymbolCircle = this.svgShape.path(powerCircle).center(cx, cy);
this.onPowerSymbolLine = this.svgShape.path(powerLine).center(cx, cy-12);
}
protected addOnCenterToMask(onCircleShape: Circle) {
this.createMask(onCircleShape, [this.onPowerSymbolCircle, this.onPowerSymbolLine]);
this.drawOnShape(null, false, '', false, this.onCircleShape);
}
protected addOnCenterTimeLine(pressedTimeline: Timeline) {
this.onPowerSymbolCircle.timeline(pressedTimeline);
this.onPowerSymbolLine.timeline(pressedTimeline);
this.onCenterTimeLine(pressedTimeline, false);
}
protected drawOffCenterColor(mainColor: PowerButtonColor) {
this.offPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offCenterColor(mainColor, false);
}
protected onCenterAnimation(scale: number) {
powerButtonAnimation(this.onPowerSymbolCircle).transform({scale, origin: {x: cx, y: cy}});
powerButtonAnimation(this.onPowerSymbolLine).transform({scale, origin: {x: cx, y: cy}});
this.buttonAnimation(scale, false);
}
}
@ -896,8 +1047,6 @@ 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;
@ -905,8 +1054,8 @@ class SimplifiedVolumePowerButtonShape extends PowerButtonShape {
private onCenterGroup: G;
protected drawCenterGroup(centerGroup: G, onCenterGroup: G) {
this.offLabelShape = this.createOffLabel().addTo(centerGroup);
this.onLabelShape = this.createOnLabel().addTo(onCenterGroup);
this.drawOffShape(centerGroup, true);
this.drawOnShape(onCenterGroup, true);
}
protected drawShape() {
@ -926,8 +1075,8 @@ class SimplifiedVolumePowerButtonShape extends PowerButtonShape {
}
protected drawColorState(mainColor: PowerButtonColor){
this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.onLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offCenterColor(mainColor, true);
this.onCenterColor(mainColor, true);
}
protected drawOff() {
@ -970,23 +1119,15 @@ class SimplifiedVolumePowerButtonShape extends PowerButtonShape {
}
class SimplifiedIconPowerButtonShape extends SimplifiedVolumePowerButtonShape {
private offPowerSymbolCircle: Path;
private offPowerSymbolLine: Path;
private onPowerSymbolCircle: Path;
private onPowerSymbolLine: Path;
protected drawCenterGroup(centerGroup: G, onCenterGroup: G) {
this.offPowerSymbolCircle = this.svgShape.path(powerCircle).center(cx, cy).addTo(centerGroup);
this.offPowerSymbolLine = this.svgShape.path(powerLine).center(cx, cy-12).addTo(centerGroup);
this.onPowerSymbolCircle = this.svgShape.path(powerCircle).center(cx, cy).addTo(onCenterGroup);
this.onPowerSymbolLine = this.svgShape.path(powerLine).center(cx, cy-12).addTo(onCenterGroup);
this.drawOffShape(centerGroup, false);
this.drawOnShape(onCenterGroup, false, '', false);
}
protected drawColorState(mainColor: PowerButtonColor) {
this.offPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.onPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.onPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offCenterColor(mainColor, false);
this.onCenterColor(mainColor, false);
}
}
@ -996,36 +1137,30 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape {
private outerBorderGradient: Gradient;
private innerBorder: Circle;
private innerBorderMask: Circle;
private offLabelShape: Text;
private onCircleShape: Circle;
private onLabelShape: Text;
protected onCircleShape: Circle;
private pressedShadow: InnerShadowCircle;
private pressedTimeline: Timeline;
private centerGroup: G;
private onCenterGroup: G;
protected drawOffCenter(centerGroup: G) {
this.offLabelShape = this.createOffLabel('800').addTo(centerGroup);
this.drawOffShape(centerGroup, true, '800');
}
protected drawOnCenter() {
this.onLabelShape = this.createOnLabel('800');
}
protected addOnCenterToMask(onCircleShape: Circle) {
this.createMask(onCircleShape,[this.onLabelShape]);
this.drawOnShape(null, true, '800', false, this.onCircleShape);
}
protected addOnCenterTimeLine(pressedTimeline: Timeline) {
this.onLabelShape.timeline(pressedTimeline);
this.onCenterTimeLine(pressedTimeline, true);
}
protected drawOffCenterColor(mainColor: PowerButtonColor) {
this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offCenterColor(mainColor, true);
}
protected onCenterAnimation(scale: number) {
powerButtonAnimation(this.onLabelShape).transform({scale, origin: {x: cx, y: cy}});
this.buttonAnimation(scale, true);
}
protected drawShape() {
@ -1047,7 +1182,6 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape {
this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 30).center(cx, cy)
.addTo(this.onCenterGroup);
this.drawOnCenter();
this.addOnCenterToMask(this.onCircleShape);
this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 30, cx, cy, 0, 0);
this.backgroundShape.addClass('tb-small-shadow');
@ -1102,37 +1236,24 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape {
}
class OutlinedIconPowerButtonShape extends OutlinedVolumePowerButtonShape {
private offPowerSymbolCircle: Path;
private offPowerSymbolLine: Path;
private onPowerSymbolCircle: Path;
private onPowerSymbolLine: Path;
protected drawOffCenter(centerGroup: G) {
this.offPowerSymbolCircle = this.svgShape.path(powerCircleStroke).center(cx, cy).addTo(centerGroup);
this.offPowerSymbolLine = this.svgShape.path(powerLineStroke).center(cx, cy-12).addTo(centerGroup);
this.drawOffShape(centerGroup, false, '', true);
}
protected drawOnCenter() {
this.onPowerSymbolCircle = this.svgShape.path(powerCircleStroke).center(cx, cy);
this.onPowerSymbolLine = this.svgShape.path(powerLineStroke).center(cx, cy-12);
}
protected addOnCenterToMask(onCircleShape: Circle) {
this.createMask(onCircleShape, [this.onPowerSymbolCircle, this.onPowerSymbolLine]);
this.drawOnShape(null, false, '', true, this.onCircleShape);
}
protected addOnCenterTimeLine(pressedTimeline: Timeline) {
this.onPowerSymbolCircle.timeline(pressedTimeline);
this.onPowerSymbolLine.timeline(pressedTimeline);
this.onCenterTimeLine(pressedTimeline, false);
}
protected drawOffCenterColor(mainColor: PowerButtonColor) {
this.offPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offCenterColor(mainColor, false);
}
protected onCenterAnimation(scale: number) {
powerButtonAnimation(this.onPowerSymbolCircle).transform({scale, origin: {x: cx, y: cy}});
powerButtonAnimation(this.onPowerSymbolLine).transform({scale, origin: {x: cx, y: cy}});
this.buttonAnimation(scale, false);
}
}

43
ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.html

@ -0,0 +1,43 @@
<!--
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-value-stepper-panel" [style.padding]="padding" [style]="backgroundStyle$ | async">
<div class="tb-value-stepper-overlay" [style]="overlayStyle"></div>
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
<div #stepperContent class="tb-value-stepper-content">
<div [class.!hidden]="!showLeftButton"
#leftButton
class="tb-button-shape tb-button-shape-left"
[class.tb-button-pointer]="!leftDisabledState && (loading$ | async) === false">
</div>
<div #valueBoxContainer
class="tb-value-stepper-value-box"
[class.!hidden]="!showValueBox"
[class.tb-value-stepper-value-box-disabled]="(disabledState$ | async) === true">
<div #value
class="tb-value-stepper-value"
[style.color]="(disabledState$ | async) === true ? disabledColor : valueStyleColor"
[style]="valueStyle">{{ valueText }}</div>
</div>
<div [class.!hidden]="!showRightButton"
#rightButton
class="tb-button-shape tb-button-shape-right"
[class.tb-button-pointer]="!rightDisabledState && (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>

87
ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.scss

@ -0,0 +1,87 @@
/**
* 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-value-stepper-panel {
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
gap: 16px;
padding: 20px 24px 24px 24px;
> div:not(.tb-value-stepper-overlay) {
z-index: 1;
}
.tb-value-stepper-overlay {
position: absolute;
top: 12px;
left: 12px;
bottom: 12px;
right: 12px;
}
div.tb-widget-title {
padding: 0;
}
.tb-value-stepper-content {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
min-width: 0;
min-height: 0;
height: 100%;
.tb-value-stepper-value-box {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
height: 32px;
padding: 0 12px;
border-radius: 4px;
&-disabled {
border-color: rgba(0, 0, 0, 0.38);
}
}
.tb-button-shape {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
&-left {
margin-right: 12px;
}
&-right {
margin-left: 12px;
}
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-button-pointer {
svg {
.tb-hover-circle {
cursor: pointer;
}
}
}
}
}
}

391
ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.ts

@ -0,0 +1,391 @@
///
/// 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,
DestroyRef,
ElementRef,
inject,
NgZone,
OnDestroy,
OnInit,
Renderer2,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { BasicActionWidgetComponent, ValueSetter } from '@home/components/widget/lib/action/action-widget.models';
import { backgroundStyle, ComponentStyle, overlayStyle, textStyle } from '@shared/models/widget-settings.models';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import { ValueType } from '@shared/models/constants';
import {
PowerButtonLayout,
PowerButtonShape,
powerButtonShapeSize,
PowerButtonWidgetSettings
} from '@home/components/widget/lib/rpc/power-button-widget.models';
import { SVG, Svg } from '@svgdotjs/svg.js';
import { MatIconRegistry } from '@angular/material/icon';
import { formatValue, isDefinedAndNotNull, isNumeric } from '@core/utils';
import {
valueStepperDefaultSettings,
ValueStepperWidgetSettings
} from '@home/components/widget/lib/rpc/value-stepper-widget.models';
import { UtilsService } from '@core/services/utils.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'tb-value-stepper-widget',
templateUrl: './value-stepper-widget.component.html',
styleUrls: ['../action/action-widget.scss', './value-stepper-widget.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class ValueStepperWidgetComponent extends
BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('leftButton', {static: true})
leftButton: ElementRef<HTMLElement>;
@ViewChild('rightButton', {static: true})
rightButton: ElementRef<HTMLElement>;
@ViewChild('stepperContent', {static: true})
stepperContent: ElementRef<HTMLElement>;
@ViewChild('valueBoxContainer', {static: true})
valueBox: ElementRef<HTMLElement>;
@ViewChild('value', {static: true})
valueElement: ElementRef<HTMLElement>;
settings: ValueStepperWidgetSettings;
backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {};
padding: string;
valueStyle: ComponentStyle = {};
valueStyleColor = '';
disabledColor = 'rgba(0, 0, 0, 0.38)';
value: number = null;
autoScale = false;
showValueBox = true;
showLeftButton = true;
showRightButton = true;
valueText = 'N/A';
disabledState$ = new BehaviorSubject(false);
private prevValue: number = null;
private shapeResize$: ResizeObserver;
private drawSvgShapePending = false;
private svgShapeLeft: Svg;
private svgShapeRight: Svg;
private powerButtonSvgShapeLeft: PowerButtonShape;
private powerButtonSvgShapeRight: PowerButtonShape;
private disabledState = false;
public leftDisabledState = false;
public rightDisabledState = false;
private valueSetterLeft: ValueSetter<number>;
private valueSetterRight: ValueSetter<number>;
private leftDisabledState$ = new BehaviorSubject(false);
private rightDisabledState$ = new BehaviorSubject(false);
protected destroyRef = inject(DestroyRef);
constructor(protected imagePipe: ImagePipe,
protected sanitizer: DomSanitizer,
private renderer: Renderer2,
private iconRegistry: MatIconRegistry,
private utils: UtilsService,
private elementRef: ElementRef,
protected cd: ChangeDetectorRef,
protected zone: NgZone) {
super(cd);
}
ngOnInit(): void {
super.ngOnInit();
this.settings = {...valueStepperDefaultSettings, ...this.ctx.settings};
this.autoScale = this.settings.appearance.autoScale;
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer);
this.overlayStyle = overlayStyle(this.settings.background.overlay);
this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding;
this.showValueBox = this.settings.appearance.showValueBox;
this.showLeftButton = this.settings.buttonAppearance.leftButton.showButton;
this.showRightButton = this.settings.buttonAppearance.rightButton.showButton;
this.valueStyle = textStyle(this.settings.appearance.valueFont);
this.valueStyleColor = this.settings.appearance.valueColor;
if (this.showValueBox) {
const valueBoxCss = `.tb-value-stepper-value-box {\n`+
`border: ${this.settings.appearance.showBorder ?
`${this.settings.appearance.borderWidth}px solid ${this.settings.appearance.borderColor}` :
'none'};\n`+
`background-color: ${this.settings.appearance.valueBoxBackground}` +
`}`;
this.utils.applyCssToElement(this.renderer, this.elementRef.nativeElement, 'tb-value-stepper-value-box', valueBoxCss);
}
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.disabledState$.next(value)
});
const leftButtonClick = {...this.settings.leftButtonClick,
actionLabel: this.ctx.translate.instant('widgets.slider.on-value-change')};
this.valueSetterLeft = this.createValueSetter(leftButtonClick);
const rightButtonClick = {...this.settings.rightButtonClick,
actionLabel: this.ctx.translate.instant('widgets.slider.on-value-change')};
this.valueSetterRight = this.createValueSetter(rightButtonClick);
combineLatest([
this.loading$,
this.disabledState$.asObservable(),
this.leftDisabledState$.asObservable()
]).pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(value => {
const state = value.includes(true);
this.updateLeftDisabledState(state)
});
combineLatest([
this.loading$,
this.disabledState$.asObservable(),
this.rightDisabledState$.asObservable()
]).pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(value => {
const state = value.includes(true);
this.updateRightDisabledState(state)
});
}
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.leftButton || this.rightButton) {
this.drawSvg();
} else {
this.drawSvgShapePending = true;
}
this.cd.detectChanges();
}
private onValue(value: number): void {
this.value = value;
this.prevValue = value;
if ((this.value + this.settings.appearance.valueStep) > this.settings.appearance.maxValueRange) {
this.rightDisabledState$.next(true);
} else {
this.rightDisabledState$.next(false);
}
if ((this.value - this.settings.appearance.valueStep) < this.settings.appearance.minValueRange) {
this.leftDisabledState$.next(true);
} else {
this.leftDisabledState$.next(false);
}
this.updateValueText();
this.cd.markForCheck();
}
private updateValueText() {
if (isDefinedAndNotNull(this.value) && isNumeric(this.value)) {
this.valueText = formatValue(this.value, this.settings.appearance.valueDecimals, this.settings.appearance.valueUnits, false);
} else {
this.valueText = 'N/A';
}
}
private onClick(rightButtonClick: boolean = false) {
this.updateValueText();
if (!this.ctx.isEdit && !this.ctx.isPreview && !this.disabledState) {
const prevValue = this.prevValue;
const targetValue = rightButtonClick ?
(this.value + this.settings.appearance.valueStep) :
(this.value - this.settings.appearance.valueStep);
this.updateValue(rightButtonClick ? this.valueSetterRight : this.valueSetterLeft, targetValue, {
next: () => this.onValue(targetValue),
error: () => this.onValue(prevValue)
});
}
}
private drawSvg() {
let leftButtonSetting: PowerButtonWidgetSettings;
let rightButtonSetting: PowerButtonWidgetSettings;
if (this.showLeftButton) {
this.svgShapeLeft = SVG().addTo(this.leftButton.nativeElement).size(powerButtonShapeSize, powerButtonShapeSize);
this.renderer.setStyle(this.svgShapeLeft.node, 'overflow', 'visible');
this.renderer.setStyle(this.svgShapeLeft.node, 'user-select', 'none');
leftButtonSetting = {
layout: PowerButtonLayout[this.settings.appearance.type],
onButtonIcon: {
showIcon: true,
icon: this.settings.buttonAppearance.leftButton.icon,
iconSize: this.settings.buttonAppearance.leftButton.iconSize * 1.7,
iconSizeUnit: this.settings.buttonAppearance.leftButton.iconSizeUnit
},
offButtonIcon: {
showIcon: true,
icon: this.settings.buttonAppearance.leftButton.icon,
iconSize: this.settings.buttonAppearance.leftButton.iconSize * 1.7,
iconSizeUnit: this.settings.buttonAppearance.leftButton.iconSizeUnit
},
mainColorOn: this.settings.buttonAppearance.leftButton.mainColorOn,
backgroundColorOn: this.settings.buttonAppearance.leftButton.backgroundColorOn,
mainColorOff: this.settings.buttonAppearance.leftButton.mainColorOff,
backgroundColorOff: this.settings.buttonAppearance.leftButton.backgroundColorOff,
mainColorDisabled: this.settings.buttonAppearance.leftButton.mainColorDisabled,
backgroundColorDisabled: this.settings.buttonAppearance.leftButton.backgroundColorDisabled
};
}
if (this.showRightButton) {
this.svgShapeRight = SVG().addTo(this.rightButton.nativeElement).size(powerButtonShapeSize, powerButtonShapeSize);
this.renderer.setStyle(this.svgShapeRight.node, 'overflow', 'visible');
this.renderer.setStyle(this.svgShapeRight.node, 'user-select', 'none');
rightButtonSetting = {
layout: PowerButtonLayout[this.settings.appearance.type],
onButtonIcon: {
showIcon: true,
icon: this.settings.buttonAppearance.rightButton.icon,
iconSize: this.settings.buttonAppearance.rightButton.iconSize * 1.7,
iconSizeUnit: this.settings.buttonAppearance.rightButton.iconSizeUnit
},
offButtonIcon: {
showIcon: true,
icon: this.settings.buttonAppearance.rightButton.icon,
iconSize: this.settings.buttonAppearance.rightButton.iconSize * 1.7,
iconSizeUnit: this.settings.buttonAppearance.rightButton.iconSizeUnit
},
mainColorOn: this.settings.buttonAppearance.rightButton.mainColorOn,
backgroundColorOn: this.settings.buttonAppearance.rightButton.backgroundColorOn,
mainColorOff: this.settings.buttonAppearance.rightButton.mainColorOff,
backgroundColorOff: this.settings.buttonAppearance.rightButton.backgroundColorOff,
mainColorDisabled: this.settings.buttonAppearance.rightButton.mainColorDisabled,
backgroundColorDisabled: this.settings.buttonAppearance.rightButton.backgroundColorDisabled
};
}
this.zone.run(() => {
if (this.showLeftButton) {
this.powerButtonSvgShapeLeft = PowerButtonShape.fromSettings(this.ctx, this.svgShapeLeft, this.iconRegistry,
leftButtonSetting , true, this.leftDisabledState, () => this.onClick());
}
if (this.showRightButton) {
this.powerButtonSvgShapeRight = PowerButtonShape.fromSettings(this.ctx, this.svgShapeRight, this.iconRegistry,
rightButtonSetting, true, this.rightDisabledState, () => this.onClick(true));
}
});
this.shapeResize$ = new ResizeObserver(() => {
this.onResize();
});
if (this.autoScale) {
this.shapeResize$.observe(this.stepperContent.nativeElement);
}
if (this.showLeftButton) {
this.shapeResize$.observe(this.leftButton.nativeElement);
}
if (this.showRightButton) {
this.shapeResize$.observe(this.rightButton.nativeElement);
}
this.onResize();
}
private updateLeftDisabledState(disabled: boolean) {
this.leftDisabledState = disabled;
this.powerButtonSvgShapeLeft?.setDisabled(this.leftDisabledState);
this.cd.markForCheck();
}
private updateRightDisabledState(disabled: boolean) {
this.rightDisabledState = disabled;
this.powerButtonSvgShapeRight?.setDisabled(this.rightDisabledState);
this.cd.markForCheck();
}
private onResize() {
const panelWidth = this.stepperContent.nativeElement.getBoundingClientRect().width;
const panelHeight = this.stepperContent.nativeElement.getBoundingClientRect().height;
const minAspect = 0.2;
const avgContentHeight = 32;
const targetHeight = panelWidth * Math.min(panelHeight / panelWidth, minAspect);
const multiplier = targetHeight / avgContentHeight;
const size = avgContentHeight * multiplier;
if (this.showValueBox) {
this.renderer.setStyle(this.valueBox?.nativeElement, 'height', `${size}px`);
this.renderer.setStyle(this.valueElement?.nativeElement, 'font-size', `${this.settings.appearance.valueFont.size * multiplier}px`);
}
if (this.showLeftButton) {
this.renderer.setStyle(this.leftButton?.nativeElement, 'width', `${size}px`);
this.renderer.setStyle(this.leftButton?.nativeElement, 'height', `${size}px`);
}
if (this.showRightButton) {
this.renderer.setStyle(this.rightButton?.nativeElement, 'width', `${size}px`);
this.renderer.setStyle(this.rightButton?.nativeElement, 'height', `${size}px`);
}
if (size) {
const scale = size / powerButtonShapeSize;
if (this.showLeftButton) {
this.renderer.setStyle(this.svgShapeLeft?.node, 'transform', `scale(${scale})`);
}
if (this.showRightButton) {
this.renderer.setStyle(this.svgShapeRight?.node, 'transform', `scale(${scale})`);
}
}
}
}

252
ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.models.ts

@ -0,0 +1,252 @@
///
/// 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 { WidgetButtonCustomStyles } from '@shared/components/button/widget-button.models';
import { BackgroundSettings, BackgroundType, cssUnit, Font } from '@shared/models/widget-settings.models';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
const defaultMainColor = '#305680';
export enum ValueStepperType {
simplified = 'simplified',
default = 'default',
default_volume = 'default_volume'
}
export const valueStepperTypes = Object.keys(ValueStepperType) as ValueStepperType[];
export const valueStepperTypeTranslations = new Map<ValueStepperType, string>(
[
[ValueStepperType.simplified, 'widgets.value-stepper.simplified'],
[ValueStepperType.default, 'widgets.value-stepper.filled'],
[ValueStepperType.default_volume, 'widgets.value-stepper.volume']
]
);
export const valueStepperTypeImages = new Map<ValueStepperType, string>(
[
[ValueStepperType.simplified, 'assets/widget/value-stepper/simplified.svg'],
[ValueStepperType.default, 'assets/widget/value-stepper/filled.svg'],
[ValueStepperType.default_volume, 'assets/widget/value-stepper/volume.svg']
]
);
export interface ValueStepperWidgetSettings {
initialState: GetValueSettings<number>;
leftButtonClick: SetValueSettings;
rightButtonClick: SetValueSettings;
disabledState: GetValueSettings<boolean>;
appearance: ValueStepperAppearance;
buttonAppearance: {
leftButton: ValueStepperButtonAppearance;
rightButton: ValueStepperButtonAppearance;
}
background: BackgroundSettings;
padding: string;
}
export interface ValueStepperAppearance {
type: ValueStepperType;
autoScale: boolean;
minValueRange: number;
maxValueRange: number;
valueStep: number;
showValueBox: boolean;
valueUnits: string;
valueDecimals: number;
valueFont: Font;
valueColor: string;
valueBoxBackground: string;
showBorder: boolean;
borderWidth: number;
borderColor: string;
}
export interface ValueStepperButtonAppearance {
showButton: boolean;
icon: string;
iconSize: number;
iconSizeUnit: cssUnit;
mainColorOn: string;
backgroundColorOn: string;
mainColorOff: string;
backgroundColorOff: string;
mainColorDisabled: string;
backgroundColorDisabled: string;
customStyle: WidgetButtonCustomStyles;
}
export const valueStepperDefaultAppearance: ValueStepperAppearance = {
type: ValueStepperType.simplified,
autoScale: true,
minValueRange: -100,
maxValueRange: 100,
valueStep: 0.5,
showValueBox: true,
valueUnits: '',
valueDecimals: 1,
valueFont: {
family: 'Roboto',
weight: '500',
style: 'normal',
size: 16,
sizeUnit: 'px',
lineHeight: '24px'
},
valueColor: '#000',
valueBoxBackground: 'rgba(0, 0, 0, 0.12)',
showBorder: true,
borderWidth: 1,
borderColor: defaultMainColor
}
export const valueStepperButtonDefaultAppearance: ValueStepperButtonAppearance = {
showButton: true,
icon: '',
iconSize: 24,
iconSizeUnit: 'px',
mainColorOn: '#3F52DD',
backgroundColorOn: '#FFFFFF',
mainColorOff: '#A2A2A2',
backgroundColorOff: '#FFFFFF',
mainColorDisabled: 'rgba(0,0,0,0.12)',
backgroundColorDisabled: '#FFFFFF',
customStyle: {
enabled: null,
hovered: null,
pressed: null,
activated: null,
disabled: null
}
}
export const valueStepperDefaultSettings: ValueStepperWidgetSettings = {
initialState: {
action: GetValueAction.EXECUTE_RPC,
defaultValue: 0,
executeRpc: {
method: 'getState',
requestTimeout: 5000,
requestPersistent: false,
persistentPollingInterval: 1000
},
getAttribute: {
key: 'state',
scope: null
},
getTimeSeries: {
key: 'state'
},
getAlarmStatus: {
severityList: null,
typeList: null
},
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'
},
getAlarmStatus: {
severityList: null,
typeList: null
},
dataToValue: {
type: DataToValueType.NONE,
compareToValue: true,
dataToValueFunction: '/* Should return boolean value */\nreturn data;'
}
},
leftButtonClick: {
action: SetValueAction.EXECUTE_RPC,
executeRpc: {
method: 'setState',
requestTimeout: 5000,
requestPersistent: false,
persistentPollingInterval: 1000
},
setAttribute: {
key: 'state',
scope: AttributeScope.SERVER_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;'
}
},
rightButtonClick: {
action: SetValueAction.EXECUTE_RPC,
executeRpc: {
method: 'setState',
requestTimeout: 5000,
requestPersistent: false,
persistentPollingInterval: 1000
},
setAttribute: {
key: 'state',
scope: AttributeScope.SERVER_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;'
}
},
appearance: valueStepperDefaultAppearance,
buttonAppearance: {
leftButton: {...valueStepperButtonDefaultAppearance, icon: 'arrow_back_ios_new'},
rightButton: {...valueStepperButtonDefaultAppearance, icon: 'arrow_forward_ios'}
},
background: {
type: BackgroundType.color,
color: '#fff',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
},
padding: '12px'
};

266
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.html

@ -0,0 +1,266 @@
<!--
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]="valueStepperWidgetSettingsForm">
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.value-stepper.behavior</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-stepper.initial-state-hint' | translate}}" translate>widgets.value-stepper.initial-state</div>
<tb-get-value-action-settings class="flex-1"
panelTitle="{{ 'widgets.value-stepper.initial-state' | translate }}"
[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.value-stepper.left-button-click-hint' | translate}}" translate>widgets.value-stepper.left-button-click</div>
<tb-set-value-action-settings class="flex-1"
panelTitle="{{ 'widgets.value-stepper.left-button-click' | translate }}"
[valueType]="valueType.DOUBLE"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="leftButtonClick"></tb-set-value-action-settings>
</div>
<div class="tb-form-row space-between">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-stepper.right-button-click-hint' | translate}}" translate>widgets.value-stepper.right-button-click</div>
<tb-set-value-action-settings class="flex-1"
panelTitle="{{ 'widgets.value-stepper.right-button-click' | translate }}"
[valueType]="valueType.DOUBLE"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="rightButtonClick"></tb-set-value-action-settings>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.button-state.disabled-state-hint' | translate}}" translate>widgets.button-state.disabled-state</div>
<tb-get-value-action-settings class="flex-1"
panelTitle="{{ 'widgets.button-state.disabled-state' | translate }}"
[valueType]="valueType.BOOLEAN"
stateLabel="{{ 'widgets.button-state.disabled' | translate }}"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="disabledState"></tb-get-value-action-settings>
</div>
</div>
<div class="tb-form-panel" formGroupName="appearance">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-image-cards-select rowHeight="3:1"
[cols]="{columns: 2,
breakpoints: {
'lt-sm': 1
}}"
label="{{ 'widgets.button.layout' | translate }}" formControlName="type">
<tb-image-cards-select-option *ngFor="let type of valueStepperTypes"
[value]="type"
[image]="valueStepperTypeImageMap.get(type)">
{{ valueStepperTypeTranslationMap.get(type) | translate }}
</tb-image-cards-select-option>
</tb-image-cards-select>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="autoScale">
{{ 'widgets.value-stepper.auto-scale' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.value-stepper.value-range' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-2">
<div class="tb-small-label" translate>widgets.value-stepper.min-range</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="minValueRange" type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<div class="tb-small-label" translate>widgets.value-stepper.max-range</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="maxValueRange" type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.value-stepper.value-increment-decrement-step' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="valueStep" type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showValueBox">
{{ 'widgets.value-stepper.value' | translate }}
</mat-slide-toggle>
<div class="flex flex-1 flex-row items-center justify-start gap-2">
<tb-unit-input class="flex" formControlName="valueUnits"></tb-unit-input>
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput formControlName="valueDecimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix class="lt-md:!hidden" 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">
<div>{{ 'widgets.value-stepper.value-box-background' | translate }}</div>
<tb-color-input asBoxInput
colorClearButton
formControlName="valueBoxBackground">
</tb-color-input>
</div>
<div class="tb-form-row space-between column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showBorder">
{{ 'widgets.value-stepper.border' | translate }}
</mat-slide-toggle>
<div class="flex flex-1 flex-row items-center justify-end gap-2">
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="borderWidth" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix class="lt-md:!hidden">px</div>
</mat-form-field>
<tb-color-input asBoxInput
colorClearButton
formControlName="borderColor">
</tb-color-input>
</div>
</div>
</div>
<div class="tb-form-panel" formGroupName="buttonAppearance">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title" translate>widgets.value-stepper.button-appearance</div>
<tb-toggle-select [(ngModel)]="buttonAppearanceType" [ngModelOptions]="{standalone: true}">
<tb-toggle-option value="left">{{ 'widgets.value-stepper.left' | translate }}</tb-toggle-option>
<tb-toggle-option value="right">{{ 'widgets.value-stepper.right' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="tb-form-panel no-border no-padding" formGroupName="leftButton" [class.!hidden]="buttonAppearanceType !== 'left'">
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showButton">
{{ 'widgets.value-stepper.left-button' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<div>{{ 'widgets.value-stepper.icon' | translate }}</div>
<div class="flex flex-1 flex-row items-center justify-start gap-2">
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select class="flex-1" formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
formControlName="icon">
</tb-material-icon-select>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.power-button.power-on-colors' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-3">
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.power-button.main</div>
<tb-color-input asBoxInput
formControlName="mainColorOn">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div class="flex flex-row items-center justify-start gap-2">
<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.disabled-colors' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-3">
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.power-button.main</div>
<tb-color-input asBoxInput
formControlName="mainColorDisabled">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div class="flex flex-row items-center justify-start gap-2">
<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 no-border no-padding" formGroupName="rightButton" [class.!hidden]="buttonAppearanceType !== 'right'">
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showButton">
{{ 'widgets.value-stepper.right-button' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<div>{{ 'widgets.value-stepper.icon' | translate }}</div>
<div class="flex flex-1 flex-row items-center justify-start gap-2">
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select class="flex-1" formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
formControlName="icon">
</tb-material-icon-select>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.power-button.power-on-colors' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-3">
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.power-button.main</div>
<tb-color-input asBoxInput
formControlName="mainColorOn">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div class="flex flex-row items-center justify-start gap-2">
<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.disabled-colors' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-3">
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.power-button.main</div>
<tb-color-input asBoxInput
formControlName="mainColorDisabled">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.power-button.background</div>
<tb-color-input asBoxInput
formControlName="backgroundColorDisabled">
</tb-color-input>
</div>
</div>
</div>
</div>
</div>
</ng-container>

191
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.ts

@ -0,0 +1,191 @@
///
/// 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 { getTargetDeviceFromDatasources } from '@shared/models/widget-settings.models';
import {
valueStepperDefaultSettings,
valueStepperTypeImages,
valueStepperTypes,
valueStepperTypeTranslations
} from '@home/components/widget/lib/rpc/value-stepper-widget.models';
import { formatValue } from '@core/utils';
type ButtonAppearanceType = 'left' | 'right';
@Component({
selector: 'tb-value-stepper-widget-settings',
templateUrl: './value-stepper-widget-settings.component.html',
styleUrls: ['../widget-settings.scss']
})
export class ValueStepperWidgetSettingsComponent extends WidgetSettingsComponent {
get targetDevice(): TargetDevice {
const datasources = this.widgetConfig?.config?.datasources;
return getTargetDeviceFromDatasources(datasources);
}
get widgetType(): widgetType {
return this.widgetConfig?.widgetType;
}
get borderRadius(): string {
return this.widgetConfig?.config?.borderRadius;
}
valueType = ValueType;
valueStepperWidgetSettingsForm: UntypedFormGroup;
valueStepperTypeTranslationMap = valueStepperTypeTranslations;
valueStepperTypes = valueStepperTypes;
valueStepperTypeImageMap = valueStepperTypeImages;
buttonAppearanceType: ButtonAppearanceType = 'left';
valuePreviewFn = this._valuePreviewFn.bind(this);
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.valueStepperWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {...valueStepperDefaultSettings};
}
protected onSettingsSet(settings: WidgetSettings) {
this.valueStepperWidgetSettingsForm = this.fb.group({
initialState: [settings.initialState, []],
leftButtonClick: [settings.leftButtonClick, []],
rightButtonClick: [settings.rightButtonClick, []],
disabledState: [settings.disabledState, []],
appearance: this.fb.group({
type: [settings.appearance.type, []],
autoScale: [settings.appearance.autoScale, []],
minValueRange: [settings.appearance.minValueRange, []],
maxValueRange: [settings.appearance.maxValueRange, []],
valueStep: [settings.appearance.valueStep, [Validators.min(0)]],
showValueBox: [settings.appearance.showValueBox, []],
valueUnits: [settings.appearance.valueUnits, []],
valueDecimals: [settings.appearance.valueDecimals, []],
valueFont: [settings.appearance.valueFont, []],
valueColor: [settings.appearance.valueColor],
valueBoxBackground: [settings.appearance.valueBoxBackground, []],
showBorder: [settings.appearance.showBorder, []],
borderWidth: [settings.appearance.borderWidth, []],
borderColor: [settings.appearance.borderColor, []]
}),
buttonAppearance: this.fb.group({
leftButton: this.fb.group({
showButton: [settings.buttonAppearance.leftButton.showButton],
icon: [settings.buttonAppearance.leftButton.icon],
iconSize: [settings.buttonAppearance.leftButton.iconSize],
iconSizeUnit: [settings.buttonAppearance.leftButton.iconSizeUnit],
mainColorOn: [settings.buttonAppearance.leftButton.mainColorOn, []],
backgroundColorOn: [settings.buttonAppearance.leftButton.backgroundColorOn, []],
mainColorDisabled: [settings.buttonAppearance.leftButton.mainColorDisabled, []],
backgroundColorDisabled: [settings.buttonAppearance.leftButton.backgroundColorDisabled, []]
}),
rightButton: this.fb.group({
showButton: [settings.buttonAppearance.rightButton.showButton],
icon: [settings.buttonAppearance.rightButton.icon],
iconSize: [settings.buttonAppearance.rightButton.iconSize],
iconSizeUnit: [settings.buttonAppearance.rightButton.iconSizeUnit],
mainColorOn: [settings.buttonAppearance.rightButton.mainColorOn, []],
backgroundColorOn: [settings.buttonAppearance.rightButton.backgroundColorOn, []],
mainColorDisabled: [settings.buttonAppearance.rightButton.mainColorDisabled, []],
backgroundColorDisabled: [settings.buttonAppearance.rightButton.backgroundColorDisabled, []]
})
})
});
}
protected validatorTriggers(): string[] {
return ['appearance.showValueBox', 'appearance.showBorder',
'buttonAppearance.leftButton.showButton', 'buttonAppearance.rightButton.showButton'];
}
protected updateValidators(_emitEvent: boolean): void {
const showValueBox: boolean = this.valueStepperWidgetSettingsForm.get('appearance').get('showValueBox').value;
const showBorder: boolean = this.valueStepperWidgetSettingsForm.get('appearance').get('showBorder').value;
const showLeftButton: boolean = this.valueStepperWidgetSettingsForm.get('buttonAppearance').get('leftButton').get('showButton').value;
const showRightButton: boolean = this.valueStepperWidgetSettingsForm.get('buttonAppearance').get('rightButton').get('showButton').value;
if (showValueBox) {
this.valueStepperWidgetSettingsForm.get('appearance').get('valueUnits').enable();
this.valueStepperWidgetSettingsForm.get('appearance').get('valueDecimals').enable();
this.valueStepperWidgetSettingsForm.get('appearance').get('valueFont').enable();
this.valueStepperWidgetSettingsForm.get('appearance').get('valueColor').enable();
this.valueStepperWidgetSettingsForm.get('appearance').get('valueBoxBackground').enable();
this.valueStepperWidgetSettingsForm.get('appearance').get('showBorder').enable({emitEvent: false});
if (showBorder) {
this.valueStepperWidgetSettingsForm.get('appearance').get('borderWidth').enable();
this.valueStepperWidgetSettingsForm.get('appearance').get('borderColor').enable();
} else {
this.valueStepperWidgetSettingsForm.get('appearance').get('borderWidth').disable();
this.valueStepperWidgetSettingsForm.get('appearance').get('borderColor').disable();
}
} else {
this.valueStepperWidgetSettingsForm.get('appearance').get('valueUnits').disable();
this.valueStepperWidgetSettingsForm.get('appearance').get('valueDecimals').disable();
this.valueStepperWidgetSettingsForm.get('appearance').get('valueFont').disable();
this.valueStepperWidgetSettingsForm.get('appearance').get('valueColor').disable();
this.valueStepperWidgetSettingsForm.get('appearance').get('valueBoxBackground').disable();
this.valueStepperWidgetSettingsForm.get('appearance').get('showBorder').disable({emitEvent: false});
this.valueStepperWidgetSettingsForm.get('appearance').get('borderWidth').disable();
this.valueStepperWidgetSettingsForm.get('appearance').get('borderColor').disable();
}
this.buttonValidators(showLeftButton, 'leftButton');
this.buttonValidators(showRightButton, 'rightButton');
}
private buttonValidators(showButtonValue: boolean, button: string) {
if (showButtonValue) {
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('icon').enable()
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('iconSize').enable()
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('iconSizeUnit').enable()
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('mainColorOn').enable()
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('backgroundColorOn').enable()
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('mainColorDisabled').enable()
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('backgroundColorDisabled').enable()
} else {
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('icon').disable()
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('iconSize').disable()
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('iconSizeUnit').disable()
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('mainColorOn').disable()
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('backgroundColorOn').disable()
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('mainColorDisabled').disable()
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('backgroundColorDisabled').disable()
}
}
private _valuePreviewFn(): string {
const units: string = this.valueStepperWidgetSettingsForm.get('appearance').get('valueUnits').value;
const decimals: number = this.valueStepperWidgetSettingsForm.get('appearance').get('valueDecimals').value;
return formatValue(48, decimals, units, false);
}
}

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

@ -18,7 +18,7 @@
<ng-container *ngIf="levelCardWidgetSettingsForm" [formGroup]="levelCardWidgetSettingsForm">
<div class="tb-form-panel no-padding no-border">
<div class="tb-form-panel">
<div class="tb-form-row space-between flex flex-1 flex-col">
<div class="tb-form-row space-between flex flex-1 !flex-col">
<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>
<tb-toggle-select formControlName="tankSelectionType">

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

@ -371,6 +371,9 @@ import {
import {
SegmentedButtonWidgetSettingsComponent
} from '@home/components/widget/lib/settings/button/segmented-button-widget-settings.component';
import {
ValueStepperWidgetSettingsComponent
} from '@home/components/widget/lib/settings/control/value-stepper-widget-settings.component';
@NgModule({
declarations: [
@ -487,6 +490,7 @@ import {
SingleSwitchWidgetSettingsComponent,
ActionButtonWidgetSettingsComponent,
SegmentedButtonWidgetSettingsComponent,
ValueStepperWidgetSettingsComponent,
CommandButtonWidgetSettingsComponent,
PowerButtonWidgetSettingsComponent,
SliderWidgetSettingsComponent,
@ -624,6 +628,7 @@ import {
SingleSwitchWidgetSettingsComponent,
ActionButtonWidgetSettingsComponent,
SegmentedButtonWidgetSettingsComponent,
ValueStepperWidgetSettingsComponent,
CommandButtonWidgetSettingsComponent,
PowerButtonWidgetSettingsComponent,
SliderWidgetSettingsComponent,

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

@ -88,6 +88,7 @@ import {
import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive';
import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/scada-symbol-widget.component';
import { TwoSegmentButtonWidgetComponent } from '@home/components/widget/lib/button/two-segment-button-widget.component';
import { ValueStepperWidgetComponent } from '@home/components/widget/lib/rpc/value-stepper-widget.component';
@NgModule({
declarations: [
@ -128,6 +129,7 @@ import { TwoSegmentButtonWidgetComponent } from '@home/components/widget/lib/but
TwoSegmentButtonWidgetComponent,
CommandButtonWidgetComponent,
PowerButtonWidgetComponent,
ValueStepperWidgetComponent,
SliderWidgetComponent,
ToggleButtonWidgetComponent,
TimeSeriesChartWidgetComponent,
@ -190,6 +192,7 @@ import { TwoSegmentButtonWidgetComponent } from '@home/components/widget/lib/but
TwoSegmentButtonWidgetComponent,
CommandButtonWidgetComponent,
PowerButtonWidgetComponent,
ValueStepperWidgetComponent,
SliderWidgetComponent,
ToggleButtonWidgetComponent,
TimeSeriesChartWidgetComponent,

8
ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.html

@ -46,6 +46,14 @@
class="tb-error">
warning
</mat-icon>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
matTooltip="{{ 'mobile.page-name-max-length' | translate }}"
*ngIf="customMobilePageForm.get('label').hasError('maxlength') && customMobilePageForm.get('label').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">

2
ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.ts

@ -65,7 +65,7 @@ export class CustomMobilePageComponent implements ControlValueAccessor, Validato
customMobilePageForm = this.fb.group({
visible: [true],
icon: ['star'],
label: ['', [Validators.required, Validators.pattern(/\S/)]],
label: ['', [Validators.required, Validators.pattern(/\S/), Validators.maxLength(255)]],
type: [MobilePageType.DASHBOARD],
dashboardId: this.fb.control<string>(null, Validators.required),
url: [{value:'', disabled: true}, [Validators.required, Validators.pattern(WEB_URL_REGEX)]],

6
ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html

@ -44,6 +44,12 @@
<mat-error *ngIf="bundlesForms.get('title').hasError('required')">
{{ 'mobile.title-required' | translate }}
</mat-error>
<mat-error *ngIf="bundlesForms.get('title').hasError('pattern')">
{{ 'mobile.title-cannot-contain-only-spaces' | translate }}
</mat-error>
<mat-error *ngIf="bundlesForms.get('title').hasError('maxlength')">
{{ 'mobile.title-max-length' | translate }}
</mat-error>
</mat-form-field>
<tb-entity-autocomplete
allowCreateNew

2
ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.ts

@ -64,7 +64,7 @@ export class MobileBundleDialogComponent extends DialogComponent<MobileBundleDia
platformType = PlatformType;
bundlesForms = this.fb.group({
title: ['', Validators.required],
title: ['', [Validators.required, Validators.pattern(/\S/), Validators.maxLength(255)]],
androidAppId: [null],
iosAppId: [null],
description: [''],

6
ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-config.resolve.ts

@ -185,11 +185,7 @@ export class MobileBundleTableConfigResolver {
iosApp: data.iosApp
}
}).afterClosed()
.subscribe(() => {
if (afterAdd) {
this.config.updateData();
}
});
.subscribe();
})
}
}

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

@ -3218,6 +3218,7 @@
"bottom-fluid-color": "Bottom fluid color",
"display": "Display",
"value": "Value",
"decimals": "Decimals",
"units": "Units",
"flow-meter-value-hint": "Double value showing on flow meter display",
"value-hint": "Double value indicating the current value",
@ -3640,6 +3641,8 @@
"add-bundle": "Add bundle",
"title": "Title",
"title-required": "Title is required",
"title-cannot-contain-only-spaces": "Title cannot contain only spaces",
"title-max-length": "Title should be less than 256",
"oauth-clients": "OAuth 2.0 clients",
"android-app": "Android App",
"android-application": "Android Application",
@ -3671,6 +3674,7 @@
"page-name": "Page name",
"page-name-required": "Page name is required.",
"page-name-cannot-contain-only-spaces": "Page name cannot contain only spaces.",
"page-name-max-length": "Page name should be less than 256",
"page-type": "Page type",
"pages-types": {
"dashboard": "Dashboard",
@ -6618,6 +6622,8 @@
"layout-outlined-icon": "Outlined.Icon",
"main": "Main",
"background": "Background",
"button-icon-on": "Button icon 'On'",
"button-icon-off": "Button icon 'Off'",
"power-on-colors": "Power 'On' colors",
"power-off-colors": "Power 'Off' colors",
"disabled-colors": "Disabled colors",
@ -6670,6 +6676,41 @@
"preview": "Preview",
"copy-style-from": "Copy style from"
},
"value-stepper": {
"behavior": "Behavior",
"simplified": "Simplified",
"filled": "Filled",
"outlined": "Outlined",
"volume": "Volume",
"initial-state": "Initial state",
"initial-state-hint": "Action to get the initial value.",
"disabled-state": "Disabled state",
"disabled-state-hint": "Configure condition under which the component is disabled.",
"right-button-click": "Right button click",
"right-button-click-hint": "Action while pressing on right button.",
"left-button-click": "Left button click",
"left-button-click-hint": "Action while pressing on left button.",
"auto-scale": "Auto scale",
"value-range": "Range",
"min-range": "Min",
"max-range": "Max",
"value-increment-decrement-step": "Value increment/decrement step",
"value": "Value",
"value-box-background": "Value box background",
"border": "Border",
"button-appearance": "Button appearance",
"left": "Left",
"right": "Right",
"left-button": "Left button",
"right-button": "Right button",
"icon": "Icon",
"color-palette": "Color palette",
"main": "Main",
"background": "Background",
"button-icon-on": "Button icon 'On'",
"button-on-colors": "Power 'On' colors",
"disabled-colors": "Disabled colors"
},
"button-state": {
"activated-state": "Activated state",
"activated-state-hint": "Configure condition under which the button is active.",

1
ui-ngx/src/assets/widget/value-stepper/filled.svg

@ -0,0 +1 @@
<svg width="214" height="76" fill="none" xmlns="http://www.w3.org/2000/svg"><g filter="url(#a)"><rect x="8.5" y="4.5" width="197" height="59" rx="4" fill="#fff" shape-rendering="crispEdges"/><rect x="21" y="18.5" width="31" height="31" rx="15.5" stroke="#305680"/><rect x="24.5" y="22" width="24" height="24" rx="12" fill="#305680"/><path d="m38.5 40 1.41-1.41L35.33 34l4.58-4.59L38.5 28l-6 6 6 6Z" fill="#fff"/><path d="M65 22a3.5 3.5 0 0 1 3.5-3.5h77A3.5 3.5 0 0 1 149 22v24a3.5 3.5 0 0 1-3.5 3.5h-77A3.5 3.5 0 0 1 65 46V22Z" fill="#305680" fill-opacity=".04"/><path d="M65 22a3.5 3.5 0 0 1 3.5-3.5h77A3.5 3.5 0 0 1 149 22v24a3.5 3.5 0 0 1-3.5 3.5h-77A3.5 3.5 0 0 1 65 46V22Z" stroke="#305680"/><path d="M88.59 37.5V39h-7.63v-1.29l3.7-4.04c.41-.46.73-.85.97-1.19.23-.33.4-.63.49-.9a2.3 2.3 0 0 0-.06-1.73c-.13-.27-.32-.5-.58-.65a1.7 1.7 0 0 0-.93-.24c-.42 0-.77.1-1.06.27-.28.19-.5.44-.65.76a2.6 2.6 0 0 0-.22 1.1h-1.88c0-.67.15-1.27.46-1.82.3-.55.73-.99 1.3-1.3a4.1 4.1 0 0 1 2.08-.5c.76 0 1.4.13 1.94.38.53.26.93.62 1.2 1.09a3.36 3.36 0 0 1 .25 2.72 5 5 0 0 1-.49 1.04 9 9 0 0 1-.74 1.04c-.28.35-.6.7-.94 1.05l-2.46 2.71h5.25Zm9.27-9.88v1.04L93.3 39h-1.98l4.54-9.88h-5.9v-1.5h7.89Zm1.98 10.44c0-.29.1-.53.3-.73.2-.2.46-.3.8-.3s.6.1.8.3c.2.2.3.44.3.73a1 1 0 0 1-.3.74c-.2.2-.46.3-.8.3s-.6-.1-.8-.3a1 1 0 0 1-.3-.74Zm6.49-4.35-1.5-.37.61-5.72h6.14v1.6H107l-.32 2.79a3.67 3.67 0 0 1 1.81-.46c.54 0 1.02.09 1.44.26.42.17.79.43 1.08.76.3.33.53.73.68 1.2a5 5 0 0 1 0 3.07 3.17 3.17 0 0 1-1.85 2.03c-.46.19-1.01.29-1.65.29a4.6 4.6 0 0 1-1.36-.2 3.73 3.73 0 0 1-1.17-.62 3.14 3.14 0 0 1-1.19-2.42h1.85c.05.37.15.69.3.95.16.25.38.45.64.58a2 2 0 0 0 .92.2c.32 0 .6-.05.83-.16.23-.11.42-.27.57-.48.15-.22.27-.47.34-.75a3.62 3.62 0 0 0-.02-1.87 1.98 1.98 0 0 0-.38-.72c-.17-.2-.38-.36-.63-.47a2.13 2.13 0 0 0-.88-.17c-.45 0-.8.07-1.04.2-.23.13-.45.29-.65.48Zm11.75-4.16c0-.38.1-.72.28-1.04.19-.32.44-.57.75-.76a1.96 1.96 0 0 1 2.05 0c.31.19.56.44.74.76.19.32.28.66.28 1.04s-.1.73-.28 1.05c-.18.31-.43.56-.74.74a2.04 2.04 0 0 1-2.8-.74 2.03 2.03 0 0 1-.28-1.05Zm1.05 0a1 1 0 0 0 1 1 .97.97 0 0 0 .98-1 1 1 0 0 0-.27-.72.93.93 0 0 0-.7-.3c-.28 0-.51.1-.71.3-.2.2-.3.43-.3.72Zm12.19 5.75h1.95a4.5 4.5 0 0 1-.62 1.99 3.72 3.72 0 0 1-1.5 1.37 5 5 0 0 1-2.33.5 4.15 4.15 0 0 1-3.34-1.45c-.4-.48-.71-1.04-.92-1.7-.21-.66-.32-1.4-.32-2.22v-.95c0-.81.1-1.55.32-2.22.22-.66.53-1.22.94-1.69.4-.47.9-.84 1.46-1.09a4.78 4.78 0 0 1 1.93-.37c.9 0 1.67.17 2.3.5.62.33 1.1.8 1.45 1.38.35.59.56 1.26.64 2.02h-1.95c-.05-.48-.17-.9-.35-1.25a1.77 1.77 0 0 0-.76-.8 2.73 2.73 0 0 0-1.33-.28c-.45 0-.84.08-1.17.25a2.2 2.2 0 0 0-.84.73c-.22.33-.39.72-.5 1.2-.11.47-.17 1-.17 1.6v.97c0 .57.05 1.1.15 1.56.1.47.26.86.47 1.2.21.33.48.59.81.77.33.18.72.27 1.18.27.56 0 1-.08 1.35-.26.35-.18.61-.44.8-.78.17-.34.3-.76.35-1.25Z" fill="#000" fill-opacity=".87"/><rect x="162" y="18.5" width="31" height="31" rx="15.5" stroke="#305680"/><rect x="165.5" y="22" width="24" height="24" rx="12" fill="#305680"/><path d="m175.5 28-1.41 1.41 4.58 4.59-4.58 4.59L175.5 40l6-6-6-6Z" fill="#fff"/></g><defs><filter id="a" x=".5" y=".5" width="213" height="75" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/><feOffset dy="4"/><feGaussianBlur stdDeviation="4"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_5669_166705"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_5669_166705" result="shape"/></filter></defs></svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

1
ui-ngx/src/assets/widget/value-stepper/simplified.svg

@ -0,0 +1 @@
<svg width="214" height="76" fill="none" xmlns="http://www.w3.org/2000/svg"><g filter="url(#a)"><rect x="8.83" y="4.5" width="197" height="59" rx="4" fill="#fff" shape-rendering="crispEdges"/><rect x="20.83" y="18" width="32" height="32" rx="16" fill="#305680"/><path d="m38.83 40 1.41-1.41L35.66 34l4.58-4.59-1.4-1.41-6 6 6 6Z" fill="#fff"/><rect x="65.33" y="18.5" width="84" height="31" rx="3.5" fill="#305680" fill-opacity=".04"/><rect x="65.33" y="18.5" width="84" height="31" rx="3.5" stroke="#305680"/><path d="M88.92 37.5V39h-7.63v-1.29l3.7-4.04c.41-.46.73-.85.97-1.19.23-.33.4-.63.49-.9a2.3 2.3 0 0 0-.05-1.73c-.13-.27-.33-.5-.58-.65a1.7 1.7 0 0 0-.93-.24c-.42 0-.78.1-1.06.27-.3.19-.5.44-.65.76a2.6 2.6 0 0 0-.22 1.1h-1.88c0-.67.15-1.27.45-1.82.3-.55.74-.99 1.31-1.3a4.1 4.1 0 0 1 2.07-.5c.76 0 1.4.13 1.94.38.53.26.93.62 1.21 1.09.28.47.42 1.02.42 1.66 0 .36-.06.7-.17 1.06a5 5 0 0 1-.5 1.04c-.2.35-.45.7-.73 1.04-.3.35-.6.7-.95 1.05l-2.46 2.71h5.25Zm9.27-9.88v1.04L93.65 39h-1.99l4.54-9.88h-5.89v-1.5h7.88Zm1.98 10.44c0-.29.1-.53.3-.73.2-.2.46-.3.8-.3s.61.1.8.3c.2.2.3.44.3.73a1 1 0 0 1-.3.74c-.19.2-.46.3-.8.3s-.6-.1-.8-.3a1 1 0 0 1-.3-.74Zm6.5-4.35-1.5-.37.6-5.72h6.14v1.6h-4.57l-.31 2.79a3.67 3.67 0 0 1 1.8-.46c.54 0 1.02.09 1.44.26.43.17.79.43 1.09.76.3.33.52.73.68 1.2a5 5 0 0 1 0 3.07c-.16.45-.38.86-.7 1.2-.3.36-.69.63-1.16.83a4.6 4.6 0 0 1-3.02.09 3.73 3.73 0 0 1-1.16-.62 3.14 3.14 0 0 1-1.18-2.42h1.84c.05.37.15.69.3.95.17.25.38.45.65.58a2 2 0 0 0 .92.2c.32 0 .59-.05.82-.16.23-.11.42-.27.57-.48.16-.22.27-.47.34-.75a3.62 3.62 0 0 0-.01-1.87 1.98 1.98 0 0 0-.39-.72c-.16-.2-.37-.36-.63-.47a2.13 2.13 0 0 0-.88-.17c-.45 0-.8.07-1.03.2-.24.13-.46.29-.66.48Zm11.74-4.16c0-.38.1-.72.28-1.04.19-.32.44-.57.75-.76a1.96 1.96 0 0 1 2.06 0c.3.19.55.44.73.76.19.32.28.66.28 1.04s-.1.73-.28 1.05a1.97 1.97 0 0 1-1.76 1.02 2.04 2.04 0 0 1-1.78-1.02 2.03 2.03 0 0 1-.28-1.05Zm1.06 0a1 1 0 0 0 1 1 .97.97 0 0 0 .98-1 1 1 0 0 0-.28-.72.93.93 0 0 0-.7-.3c-.27 0-.5.1-.7.3-.2.2-.3.43-.3.72Zm12.18 5.75h1.96a4.5 4.5 0 0 1-.63 1.99 3.72 3.72 0 0 1-1.5 1.37 5 5 0 0 1-2.33.5 4.15 4.15 0 0 1-3.34-1.45c-.4-.48-.7-1.04-.92-1.7-.21-.66-.31-1.4-.31-2.22v-.95c0-.81.1-1.55.32-2.22a4.9 4.9 0 0 1 .93-1.69c.4-.47.9-.84 1.46-1.09a4.78 4.78 0 0 1 1.93-.37c.91 0 1.68.17 2.3.5.63.33 1.11.8 1.45 1.38.35.59.57 1.26.64 2.02h-1.95c-.05-.48-.17-.9-.34-1.25a1.77 1.77 0 0 0-.77-.8 2.73 2.73 0 0 0-1.33-.28c-.44 0-.84.08-1.17.25a2.2 2.2 0 0 0-.83.73c-.23.33-.4.72-.51 1.2-.11.47-.17 1-.17 1.6v.97c0 .57.05 1.1.15 1.56.1.47.26.86.47 1.2.22.33.49.59.81.77.33.18.72.27 1.18.27.56 0 1.01-.08 1.35-.26.35-.18.62-.44.8-.78.18-.34.3-.76.35-1.25Z" fill="#000" fill-opacity=".87"/><rect x="161.83" y="18" width="32" height="32" rx="16" fill="#305680"/><path d="m175.83 28-1.4 1.41L179 34l-4.58 4.59 1.41 1.41 6-6-6-6Z" fill="#fff"/></g><defs><filter id="a" x=".83" y=".5" width="213" height="75" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/><feOffset dy="4"/><feGaussianBlur stdDeviation="4"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_5669_166701"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_5669_166701" result="shape"/></filter></defs></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

1
ui-ngx/src/assets/widget/value-stepper/volume.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

Loading…
Cancel
Save